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.js CHANGED
@@ -8,7 +8,7 @@ import { Command } from "commander";
8
8
  import * as fs3 from "fs";
9
9
  import * as path3 from "path";
10
10
  import { loadConfig } from "@viberails/config";
11
- import chalk from "chalk";
11
+ import chalk2 from "chalk";
12
12
 
13
13
  // src/utils/find-project-root.ts
14
14
  import * as fs from "fs";
@@ -29,6 +29,7 @@ function findProjectRoot(startDir) {
29
29
 
30
30
  // src/utils/prompt.ts
31
31
  import * as readline from "readline";
32
+ import chalk from "chalk";
32
33
  async function confirm(message) {
33
34
  const rl = readline.createInterface({
34
35
  input: process.stdin,
@@ -42,6 +43,29 @@ async function confirm(message) {
42
43
  });
43
44
  });
44
45
  }
46
+ async function selectIntegrations(hookManager) {
47
+ const result = { preCommitHook: true, claudeCodeHook: true };
48
+ const hookLabel = hookManager ? `Pre-commit hook (detected: ${hookManager})` : "Pre-commit hook (git hook)";
49
+ console.log("Set up integrations:");
50
+ const rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout
53
+ });
54
+ const askYn = (label, defaultYes) => new Promise((resolve4) => {
55
+ const hint = defaultYes ? "Y/n" : "y/N";
56
+ rl.question(` ${label}? (${hint}) `, (answer) => {
57
+ const trimmed = answer.trim().toLowerCase();
58
+ if (trimmed === "") resolve4(defaultYes);
59
+ else resolve4(trimmed === "y" || trimmed === "yes");
60
+ });
61
+ });
62
+ console.log(` ${chalk.dim("Runs viberails check automatically when you commit")}`);
63
+ result.preCommitHook = await askYn(hookLabel, true);
64
+ console.log(` ${chalk.dim("Checks files against your rules when Claude edits them")}`);
65
+ result.claudeCodeHook = await askYn("Claude Code hook", true);
66
+ rl.close();
67
+ return result;
68
+ }
45
69
 
46
70
  // src/utils/resolve-workspace-packages.ts
47
71
  import * as fs2 from "fs";
@@ -66,7 +90,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
66
90
  ];
67
91
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
68
92
  }
69
- const packageNames = new Set(packages.map((p3) => p3.name));
93
+ const packageNames = new Set(packages.map((p) => p.name));
70
94
  for (const pkg of packages) {
71
95
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
72
96
  }
@@ -96,80 +120,68 @@ async function boundariesCommand(options, cwd) {
96
120
  }
97
121
  displayRules(config);
98
122
  }
99
- function countBoundaries(boundaries) {
100
- if (!boundaries) return 0;
101
- if (Array.isArray(boundaries)) return boundaries.length;
102
- return Object.values(boundaries).reduce((sum, denied) => sum + denied.length, 0);
103
- }
104
123
  function displayRules(config) {
105
- const total = countBoundaries(config.boundaries);
106
- if (total === 0) {
107
- console.log(chalk.yellow("No boundary rules configured."));
108
- console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
124
+ if (!config.boundaries || config.boundaries.length === 0) {
125
+ console.log(chalk2.yellow("No boundary rules configured."));
126
+ console.log(`Run ${chalk2.cyan("viberails boundaries --infer")} to generate rules.`);
109
127
  return;
110
128
  }
129
+ const allowRules = config.boundaries.filter((r) => r.allow);
130
+ const denyRules = config.boundaries.filter((r) => !r.allow);
111
131
  console.log(`
112
- ${chalk.bold(`Boundary rules (${total} rules):`)}
132
+ ${chalk2.bold(`Boundary rules (${config.boundaries.length} rules):`)}
113
133
  `);
114
- if (Array.isArray(config.boundaries)) {
115
- const allowRules = config.boundaries.filter((r) => r.allow);
116
- const denyRules = config.boundaries.filter((r) => !r.allow);
117
- for (const r of allowRules) {
118
- console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
119
- }
120
- for (const r of denyRules) {
121
- const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
122
- console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
123
- }
124
- } else if (config.boundaries) {
125
- for (const [from, denied] of Object.entries(config.boundaries)) {
126
- for (const to of denied) {
127
- console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
128
- }
129
- }
134
+ for (const r of allowRules) {
135
+ console.log(` ${chalk2.green("\u2713")} ${r.from} \u2192 ${r.to}`);
136
+ }
137
+ for (const r of denyRules) {
138
+ const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
139
+ console.log(` ${chalk2.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
130
140
  }
131
141
  console.log(
132
142
  `
133
- Enforcement: ${config.rules.enforceBoundaries ? chalk.green("on") : chalk.yellow("off")}`
143
+ Enforcement: ${config.rules.enforceBoundaries ? chalk2.green("on") : chalk2.yellow("off")}`
134
144
  );
135
145
  }
136
146
  async function inferAndDisplay(projectRoot, config, configPath) {
137
- console.log(chalk.dim("Analyzing imports..."));
147
+ console.log(chalk2.dim("Analyzing imports..."));
138
148
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
139
149
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
140
150
  const graph = await buildImportGraph(projectRoot, {
141
151
  packages,
142
152
  ignore: config.ignore
143
153
  });
144
- console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
154
+ console.log(chalk2.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
145
155
  const inferred = inferBoundaries(graph);
146
- const entries = Object.entries(inferred);
147
- if (entries.length === 0) {
148
- console.log(chalk.yellow("No boundary rules could be inferred."));
156
+ if (inferred.length === 0) {
157
+ console.log(chalk2.yellow("No boundary rules could be inferred."));
149
158
  return;
150
159
  }
151
- const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
160
+ const allow = inferred.filter((r) => r.allow);
161
+ const deny = inferred.filter((r) => !r.allow);
152
162
  console.log(`
153
- ${chalk.bold("Inferred boundary rules:")}
163
+ ${chalk2.bold("Inferred boundary rules:")}
154
164
  `);
155
- for (const [from, denied] of entries) {
156
- for (const to of denied) {
157
- console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
158
- }
165
+ for (const r of allow) {
166
+ console.log(` ${chalk2.green("\u2713")} ${r.from} \u2192 ${r.to}`);
167
+ }
168
+ for (const r of deny) {
169
+ const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
170
+ console.log(` ${chalk2.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
159
171
  }
160
172
  console.log(`
161
- ${totalRules} deny rules`);
173
+ ${allow.length} allowed, ${deny.length} denied`);
162
174
  const shouldSave = await confirm("\nSave to viberails.config.json?");
163
175
  if (shouldSave) {
164
176
  config.boundaries = inferred;
165
177
  config.rules.enforceBoundaries = true;
166
178
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
167
179
  `);
168
- console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
180
+ console.log(`${chalk2.green("\u2713")} Saved ${inferred.length} rules`);
169
181
  }
170
182
  }
171
183
  async function showGraph(projectRoot, config) {
172
- console.log(chalk.dim("Building import graph..."));
184
+ console.log(chalk2.dim("Building import graph..."));
173
185
  const { buildImportGraph } = await import("@viberails/graph");
174
186
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
175
187
  const graph = await buildImportGraph(projectRoot, {
@@ -177,20 +189,20 @@ async function showGraph(projectRoot, config) {
177
189
  ignore: config.ignore
178
190
  });
179
191
  console.log(`
180
- ${chalk.bold("Import dependency graph:")}
192
+ ${chalk2.bold("Import dependency graph:")}
181
193
  `);
182
194
  console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
183
195
  `);
184
196
  if (graph.packages.length > 0) {
185
197
  for (const pkg of graph.packages) {
186
198
  const deps = pkg.internalDeps.length > 0 ? `
187
- ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : chalk.dim(" (no internal deps)");
199
+ ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : chalk2.dim(" (no internal deps)");
188
200
  console.log(` ${pkg.name}${deps}`);
189
201
  }
190
202
  }
191
203
  if (graph.cycles.length > 0) {
192
204
  console.log(`
193
- ${chalk.yellow("Cycles detected:")}`);
205
+ ${chalk2.yellow("Cycles detected:")}`);
194
206
  for (const cycle of graph.cycles) {
195
207
  const paths = cycle.map((f) => path3.relative(projectRoot, f));
196
208
  console.log(` ${paths.join(" \u2192 ")}`);
@@ -202,7 +214,7 @@ ${chalk.yellow("Cycles detected:")}`);
202
214
  import * as fs6 from "fs";
203
215
  import * as path6 from "path";
204
216
  import { loadConfig as loadConfig2 } from "@viberails/config";
205
- import chalk2 from "chalk";
217
+ import chalk3 from "chalk";
206
218
 
207
219
  // src/commands/check-config.ts
208
220
  function resolveConfigForFile(relPath, config) {
@@ -235,6 +247,7 @@ function resolveIgnoreForFile(relPath, config) {
235
247
  import { execSync } from "child_process";
236
248
  import * as fs4 from "fs";
237
249
  import * as path4 from "path";
250
+ import picomatch from "picomatch";
238
251
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
239
252
  "node_modules",
240
253
  ".git",
@@ -270,25 +283,9 @@ var NAMING_PATTERNS = {
270
283
  snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
271
284
  };
272
285
  function isIgnored(relPath, ignorePatterns) {
273
- for (const pattern of ignorePatterns) {
274
- const startsGlob = pattern.startsWith("**/");
275
- const endsGlob = pattern.endsWith("/**");
276
- if (startsGlob && endsGlob) {
277
- const middle = pattern.slice(3, -3);
278
- if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
279
- return true;
280
- }
281
- } else if (endsGlob) {
282
- const prefix = pattern.slice(0, -3);
283
- if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
284
- } else if (startsGlob) {
285
- const suffix = pattern.slice(3);
286
- if (relPath.endsWith(suffix) || relPath === suffix) return true;
287
- } else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
288
- return true;
289
- }
290
- }
291
- return false;
286
+ if (ignorePatterns.length === 0) return false;
287
+ const isMatch = picomatch(ignorePatterns, { dot: true });
288
+ return isMatch(relPath);
292
289
  }
293
290
  function countFileLines(filePath) {
294
291
  try {
@@ -455,12 +452,12 @@ function printGroupedViolations(violations, limit) {
455
452
  const toShow = group.slice(0, remaining);
456
453
  const hidden = group.length - toShow.length;
457
454
  for (const v of toShow) {
458
- const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
459
- console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
455
+ const icon = v.severity === "error" ? chalk3.red("\u2717") : chalk3.yellow("!");
456
+ console.log(`${icon} ${chalk3.dim(v.rule)} ${v.file}: ${v.message}`);
460
457
  }
461
458
  totalShown += toShow.length;
462
459
  if (hidden > 0) {
463
- console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
460
+ console.log(chalk3.dim(` ... and ${hidden} more ${rule} violations`));
464
461
  }
465
462
  }
466
463
  }
@@ -478,13 +475,13 @@ async function checkCommand(options, cwd) {
478
475
  const startDir = cwd ?? process.cwd();
479
476
  const projectRoot = findProjectRoot(startDir);
480
477
  if (!projectRoot) {
481
- console.error(`${chalk2.red("Error:")} No package.json found. Are you in a JS/TS project?`);
478
+ console.error(`${chalk3.red("Error:")} No package.json found. Are you in a JS/TS project?`);
482
479
  return 1;
483
480
  }
484
481
  const configPath = path6.join(projectRoot, CONFIG_FILE2);
485
482
  if (!fs6.existsSync(configPath)) {
486
483
  console.error(
487
- `${chalk2.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
484
+ `${chalk3.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
488
485
  );
489
486
  return 1;
490
487
  }
@@ -498,7 +495,7 @@ async function checkCommand(options, cwd) {
498
495
  filesToCheck = getAllSourceFiles(projectRoot, config);
499
496
  }
500
497
  if (filesToCheck.length === 0) {
501
- console.log(`${chalk2.green("\u2713")} No files to check.`);
498
+ console.log(`${chalk3.green("\u2713")} No files to check.`);
502
499
  return 0;
503
500
  }
504
501
  const violations = [];
@@ -539,8 +536,7 @@ async function checkCommand(options, cwd) {
539
536
  const testViolations = checkMissingTests(projectRoot, config, severity);
540
537
  violations.push(...testViolations);
541
538
  }
542
- const hasBoundaries = config.boundaries ? Array.isArray(config.boundaries) ? config.boundaries.length > 0 : Object.keys(config.boundaries).length > 0 : false;
543
- if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
539
+ if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
544
540
  const startTime = Date.now();
545
541
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
546
542
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -561,10 +557,20 @@ async function checkCommand(options, cwd) {
561
557
  });
562
558
  }
563
559
  const elapsed = Date.now() - startTime;
564
- console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
560
+ console.log(chalk3.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
561
+ }
562
+ if (options.format === "json") {
563
+ console.log(
564
+ JSON.stringify({
565
+ violations,
566
+ checkedFiles: filesToCheck.length,
567
+ enforcement: config.enforcement
568
+ })
569
+ );
570
+ return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
565
571
  }
566
572
  if (violations.length === 0) {
567
- console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
573
+ console.log(`${chalk3.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
568
574
  return 0;
569
575
  }
570
576
  if (!options.quiet) {
@@ -572,7 +578,7 @@ async function checkCommand(options, cwd) {
572
578
  }
573
579
  printSummary(violations);
574
580
  if (config.enforcement === "enforce") {
575
- console.log(chalk2.red("Fix violations before committing."));
581
+ console.log(chalk3.red("Fix violations before committing."));
576
582
  return 1;
577
583
  }
578
584
  return 0;
@@ -582,23 +588,23 @@ async function checkCommand(options, cwd) {
582
588
  import * as fs9 from "fs";
583
589
  import * as path10 from "path";
584
590
  import { loadConfig as loadConfig3 } from "@viberails/config";
585
- import chalk4 from "chalk";
591
+ import chalk5 from "chalk";
586
592
 
587
593
  // src/commands/fix-helpers.ts
588
594
  import { execSync as execSync2 } from "child_process";
589
595
  import { createInterface as createInterface2 } from "readline";
590
- import chalk3 from "chalk";
596
+ import chalk4 from "chalk";
591
597
  function printPlan(renames, stubs) {
592
598
  if (renames.length > 0) {
593
- console.log(chalk3.bold("\nFile renames:"));
599
+ console.log(chalk4.bold("\nFile renames:"));
594
600
  for (const r of renames) {
595
- console.log(` ${chalk3.red(r.oldPath)} \u2192 ${chalk3.green(r.newPath)}`);
601
+ console.log(` ${chalk4.red(r.oldPath)} \u2192 ${chalk4.green(r.newPath)}`);
596
602
  }
597
603
  }
598
604
  if (stubs.length > 0) {
599
- console.log(chalk3.bold("\nTest stubs to create:"));
605
+ console.log(chalk4.bold("\nTest stubs to create:"));
600
606
  for (const s of stubs) {
601
- console.log(` ${chalk3.green("+")} ${s.path}`);
607
+ console.log(` ${chalk4.green("+")} ${s.path}`);
602
608
  }
603
609
  }
604
610
  }
@@ -662,7 +668,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
662
668
  const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
663
669
  for (const sourceFile of project.getSourceFiles()) {
664
670
  const filePath = sourceFile.getFilePath();
665
- if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
671
+ const segments = filePath.split(path7.sep);
672
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
666
673
  const fileDir = path7.dirname(filePath);
667
674
  for (const decl of sourceFile.getImportDeclarations()) {
668
675
  const specifier = decl.getModuleSpecifierValue();
@@ -848,13 +855,13 @@ async function fixCommand(options, cwd) {
848
855
  const startDir = cwd ?? process.cwd();
849
856
  const projectRoot = findProjectRoot(startDir);
850
857
  if (!projectRoot) {
851
- console.error(`${chalk4.red("Error:")} No package.json found. Are you in a JS/TS project?`);
858
+ console.error(`${chalk5.red("Error:")} No package.json found. Are you in a JS/TS project?`);
852
859
  return 1;
853
860
  }
854
861
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
855
862
  if (!fs9.existsSync(configPath)) {
856
863
  console.error(
857
- `${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
864
+ `${chalk5.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
858
865
  );
859
866
  return 1;
860
867
  }
@@ -863,7 +870,7 @@ async function fixCommand(options, cwd) {
863
870
  const isDirty = checkGitDirty(projectRoot);
864
871
  if (isDirty) {
865
872
  console.log(
866
- chalk4.yellow("Warning: You have uncommitted changes. Consider committing first.")
873
+ chalk5.yellow("Warning: You have uncommitted changes. Consider committing first.")
867
874
  );
868
875
  }
869
876
  }
@@ -893,12 +900,12 @@ async function fixCommand(options, cwd) {
893
900
  }
894
901
  }
895
902
  if (dedupedRenames.length === 0 && testStubs.length === 0) {
896
- console.log(`${chalk4.green("\u2713")} No fixable violations found.`);
903
+ console.log(`${chalk5.green("\u2713")} No fixable violations found.`);
897
904
  return 0;
898
905
  }
899
906
  printPlan(dedupedRenames, testStubs);
900
907
  if (options.dryRun) {
901
- console.log(chalk4.dim("\nDry run \u2014 no changes applied."));
908
+ console.log(chalk5.dim("\nDry run \u2014 no changes applied."));
902
909
  return 0;
903
910
  }
904
911
  if (!options.yes) {
@@ -929,15 +936,15 @@ async function fixCommand(options, cwd) {
929
936
  }
930
937
  console.log("");
931
938
  if (renameCount > 0) {
932
- console.log(`${chalk4.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
939
+ console.log(`${chalk5.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
933
940
  }
934
941
  if (importUpdateCount > 0) {
935
942
  console.log(
936
- `${chalk4.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
943
+ `${chalk5.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
937
944
  );
938
945
  }
939
946
  if (stubCount > 0) {
940
- console.log(`${chalk4.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
947
+ console.log(`${chalk5.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
941
948
  }
942
949
  return 0;
943
950
  }
@@ -945,11 +952,262 @@ async function fixCommand(options, cwd) {
945
952
  // src/commands/init.ts
946
953
  import * as fs12 from "fs";
947
954
  import * as path13 from "path";
948
- import * as p2 from "@clack/prompts";
949
955
  import { generateConfig } from "@viberails/config";
950
956
  import { scan } from "@viberails/scanner";
951
957
  import chalk9 from "chalk";
952
958
 
959
+ // src/display.ts
960
+ import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, ORM_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
961
+ import chalk7 from "chalk";
962
+
963
+ // src/display-helpers.ts
964
+ import { ROLE_DESCRIPTIONS } from "@viberails/types";
965
+ function groupByRole(directories) {
966
+ const map = /* @__PURE__ */ new Map();
967
+ for (const dir of directories) {
968
+ if (dir.role === "unknown") continue;
969
+ const existing = map.get(dir.role);
970
+ if (existing) {
971
+ existing.dirs.push(dir);
972
+ } else {
973
+ map.set(dir.role, { dirs: [dir] });
974
+ }
975
+ }
976
+ const groups = [];
977
+ for (const [role, { dirs }] of map) {
978
+ const label = ROLE_DESCRIPTIONS[role] ?? role;
979
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
980
+ groups.push({
981
+ role,
982
+ label,
983
+ dirCount: dirs.length,
984
+ totalFiles,
985
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
986
+ });
987
+ }
988
+ return groups;
989
+ }
990
+ function formatSummary(stats, packageCount) {
991
+ const parts = [];
992
+ if (packageCount && packageCount > 1) {
993
+ parts.push(`${packageCount} packages`);
994
+ }
995
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
996
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
997
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
998
+ return parts.join(" \xB7 ");
999
+ }
1000
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1001
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1002
+ }
1003
+ function formatRoleGroup(group) {
1004
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1005
+ if (group.singlePath) {
1006
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1007
+ }
1008
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1009
+ return `${group.label} \u2014 ${dirs} (${files})`;
1010
+ }
1011
+
1012
+ // src/display-monorepo.ts
1013
+ import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1014
+ import chalk6 from "chalk";
1015
+ function formatPackageSummary(pkg) {
1016
+ const parts = [];
1017
+ if (pkg.stack.framework) {
1018
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1019
+ }
1020
+ if (pkg.stack.styling) {
1021
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1022
+ }
1023
+ const files = `${pkg.statistics.totalFiles} files`;
1024
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1025
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1026
+ }
1027
+ function displayMonorepoResults(scanResult) {
1028
+ const { stack, packages } = scanResult;
1029
+ console.log(`
1030
+ ${chalk6.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1031
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
1032
+ if (stack.packageManager) {
1033
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
1034
+ }
1035
+ if (stack.linter) {
1036
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
1037
+ }
1038
+ if (stack.formatter) {
1039
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
1040
+ }
1041
+ if (stack.testRunner) {
1042
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
1043
+ }
1044
+ console.log("");
1045
+ for (const pkg of packages) {
1046
+ console.log(formatPackageSummary(pkg));
1047
+ }
1048
+ const packagesWithDirs = packages.filter(
1049
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1050
+ );
1051
+ if (packagesWithDirs.length > 0) {
1052
+ console.log(`
1053
+ ${chalk6.bold("Structure:")}`);
1054
+ for (const pkg of packagesWithDirs) {
1055
+ const groups = groupByRole(pkg.structure.directories);
1056
+ if (groups.length === 0) continue;
1057
+ console.log(` ${pkg.relativePath}:`);
1058
+ for (const group of groups) {
1059
+ console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
1060
+ }
1061
+ }
1062
+ }
1063
+ displayConventions(scanResult);
1064
+ displaySummarySection(scanResult);
1065
+ console.log("");
1066
+ }
1067
+
1068
+ // src/display.ts
1069
+ var CONVENTION_LABELS = {
1070
+ fileNaming: "File naming",
1071
+ componentNaming: "Component naming",
1072
+ hookNaming: "Hook naming",
1073
+ importAlias: "Import alias"
1074
+ };
1075
+ function formatItem(item, nameMap) {
1076
+ const name = nameMap?.[item.name] ?? item.name;
1077
+ return item.version ? `${name} ${item.version}` : name;
1078
+ }
1079
+ function confidenceLabel(convention) {
1080
+ const pct = Math.round(convention.consistency);
1081
+ if (convention.confidence === "high") {
1082
+ return `${pct}% \u2014 high confidence, will enforce`;
1083
+ }
1084
+ return `${pct}% \u2014 medium confidence, suggested only`;
1085
+ }
1086
+ function displayConventions(scanResult) {
1087
+ const conventionEntries = Object.entries(scanResult.conventions);
1088
+ if (conventionEntries.length === 0) return;
1089
+ console.log(`
1090
+ ${chalk7.bold("Conventions:")}`);
1091
+ for (const [key, convention] of conventionEntries) {
1092
+ if (convention.confidence === "low") continue;
1093
+ const label = CONVENTION_LABELS[key] ?? key;
1094
+ if (scanResult.packages.length > 1) {
1095
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1096
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1097
+ if (allSame || pkgValues.length <= 1) {
1098
+ const ind = convention.confidence === "high" ? chalk7.green("\u2713") : chalk7.yellow("~");
1099
+ const detail = chalk7.dim(`(${confidenceLabel(convention)})`);
1100
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1101
+ } else {
1102
+ console.log(` ${chalk7.yellow("~")} ${label}: varies by package`);
1103
+ for (const pv of pkgValues) {
1104
+ const pct = Math.round(pv.convention.consistency);
1105
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1106
+ }
1107
+ }
1108
+ } else {
1109
+ const ind = convention.confidence === "high" ? chalk7.green("\u2713") : chalk7.yellow("~");
1110
+ const detail = chalk7.dim(`(${confidenceLabel(convention)})`);
1111
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1112
+ }
1113
+ }
1114
+ }
1115
+ function displaySummarySection(scanResult) {
1116
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1117
+ console.log(`
1118
+ ${chalk7.bold("Summary:")}`);
1119
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1120
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1121
+ if (ext) {
1122
+ console.log(` ${ext}`);
1123
+ }
1124
+ }
1125
+ function displayScanResults(scanResult) {
1126
+ if (scanResult.packages.length > 1) {
1127
+ displayMonorepoResults(scanResult);
1128
+ return;
1129
+ }
1130
+ const { stack } = scanResult;
1131
+ console.log(`
1132
+ ${chalk7.bold("Detected:")}`);
1133
+ if (stack.framework) {
1134
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1135
+ }
1136
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.language)}`);
1137
+ if (stack.styling) {
1138
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1139
+ }
1140
+ if (stack.backend) {
1141
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1142
+ }
1143
+ if (stack.orm) {
1144
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
1145
+ }
1146
+ if (stack.linter) {
1147
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.linter)}`);
1148
+ }
1149
+ if (stack.formatter) {
1150
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.formatter)}`);
1151
+ }
1152
+ if (stack.testRunner) {
1153
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.testRunner)}`);
1154
+ }
1155
+ if (stack.packageManager) {
1156
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.packageManager)}`);
1157
+ }
1158
+ if (stack.libraries.length > 0) {
1159
+ for (const lib of stack.libraries) {
1160
+ console.log(` ${chalk7.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1161
+ }
1162
+ }
1163
+ const groups = groupByRole(scanResult.structure.directories);
1164
+ if (groups.length > 0) {
1165
+ console.log(`
1166
+ ${chalk7.bold("Structure:")}`);
1167
+ for (const group of groups) {
1168
+ console.log(` ${chalk7.green("\u2713")} ${formatRoleGroup(group)}`);
1169
+ }
1170
+ }
1171
+ displayConventions(scanResult);
1172
+ displaySummarySection(scanResult);
1173
+ console.log("");
1174
+ }
1175
+ function getConventionStr(cv) {
1176
+ return typeof cv === "string" ? cv : cv.value;
1177
+ }
1178
+ function displayRulesPreview(config) {
1179
+ console.log(`${chalk7.bold("Rules:")}`);
1180
+ console.log(` ${chalk7.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1181
+ if (config.rules.requireTests && config.structure.testPattern) {
1182
+ console.log(
1183
+ ` ${chalk7.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1184
+ );
1185
+ } else if (config.rules.requireTests) {
1186
+ console.log(` ${chalk7.dim("\u2022")} Require test files: yes`);
1187
+ } else {
1188
+ console.log(` ${chalk7.dim("\u2022")} Require test files: no`);
1189
+ }
1190
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1191
+ console.log(
1192
+ ` ${chalk7.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1193
+ );
1194
+ } else {
1195
+ console.log(` ${chalk7.dim("\u2022")} Enforce file naming: no`);
1196
+ }
1197
+ console.log(
1198
+ ` ${chalk7.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1199
+ );
1200
+ console.log("");
1201
+ if (config.enforcement === "enforce") {
1202
+ console.log(`${chalk7.bold("Enforcement mode:")} enforce (violations will block commits)`);
1203
+ } else {
1204
+ console.log(
1205
+ `${chalk7.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1206
+ );
1207
+ }
1208
+ console.log("");
1209
+ }
1210
+
953
1211
  // src/utils/write-generated-files.ts
954
1212
  import * as fs10 from "fs";
955
1213
  import * as path11 from "path";
@@ -979,60 +1237,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
979
1237
  // src/commands/init-hooks.ts
980
1238
  import * as fs11 from "fs";
981
1239
  import * as path12 from "path";
982
- import chalk5 from "chalk";
983
- function setupClaudeCodeHook(projectRoot) {
984
- const claudeDir = path12.join(projectRoot, ".claude");
985
- if (!fs11.existsSync(claudeDir)) {
986
- fs11.mkdirSync(claudeDir, { recursive: true });
987
- }
988
- const settingsPath = path12.join(claudeDir, "settings.json");
989
- let settings = {};
990
- if (fs11.existsSync(settingsPath)) {
991
- try {
992
- settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
993
- } catch {
994
- }
995
- }
996
- const hooks = settings.hooks ?? {};
997
- const postToolUse = hooks.PostToolUse;
998
- if (Array.isArray(postToolUse)) {
999
- const hasViberails = postToolUse.some(
1000
- (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.some(
1001
- (h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes("viberails")
1002
- )
1003
- );
1004
- if (hasViberails) return;
1005
- }
1006
- const viberailsHook = {
1007
- matcher: "Edit|Write",
1008
- hooks: [
1009
- {
1010
- type: "command",
1011
- command: "jq -r '.tool_input.file_path' | xargs npx viberails check --files"
1012
- }
1013
- ]
1014
- };
1015
- if (!hooks.PostToolUse) {
1016
- hooks.PostToolUse = [viberailsHook];
1017
- } else if (Array.isArray(hooks.PostToolUse)) {
1018
- hooks.PostToolUse.push(viberailsHook);
1019
- }
1020
- settings.hooks = hooks;
1021
- fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1022
- `);
1023
- console.log(` ${chalk5.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1024
- }
1240
+ import chalk8 from "chalk";
1025
1241
  function setupPreCommitHook(projectRoot) {
1026
1242
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1027
1243
  if (fs11.existsSync(lefthookPath)) {
1028
1244
  addLefthookPreCommit(lefthookPath);
1029
- console.log(` ${chalk5.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1245
+ console.log(` ${chalk8.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1030
1246
  return;
1031
1247
  }
1032
1248
  const huskyDir = path12.join(projectRoot, ".husky");
1033
1249
  if (fs11.existsSync(huskyDir)) {
1034
1250
  writeHuskyPreCommit(huskyDir);
1035
- console.log(` ${chalk5.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1251
+ console.log(` ${chalk8.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1036
1252
  return;
1037
1253
  }
1038
1254
  const gitDir = path12.join(projectRoot, ".git");
@@ -1042,7 +1258,7 @@ function setupPreCommitHook(projectRoot) {
1042
1258
  fs11.mkdirSync(hooksDir, { recursive: true });
1043
1259
  }
1044
1260
  writeGitHookPreCommit(hooksDir);
1045
- console.log(` ${chalk5.green("\u2713")} .git/hooks/pre-commit`);
1261
+ console.log(` ${chalk8.green("\u2713")} .git/hooks/pre-commit`);
1046
1262
  }
1047
1263
  }
1048
1264
  function writeGitHookPreCommit(hooksDir) {
@@ -1072,10 +1288,72 @@ npx viberails check --staged
1072
1288
  function addLefthookPreCommit(lefthookPath) {
1073
1289
  const content = fs11.readFileSync(lefthookPath, "utf-8");
1074
1290
  if (content.includes("viberails")) return;
1075
- const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
1076
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1077
- ${addition}
1291
+ const hasPreCommit = /^pre-commit:/m.test(content);
1292
+ if (hasPreCommit) {
1293
+ const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1294
+ "\n"
1295
+ );
1296
+ const updated = `${content.trimEnd()}
1297
+ ${commandBlock}
1298
+ `;
1299
+ fs11.writeFileSync(lefthookPath, updated);
1300
+ } else {
1301
+ const section = [
1302
+ "",
1303
+ "pre-commit:",
1304
+ " commands:",
1305
+ " viberails:",
1306
+ " run: npx viberails check --staged"
1307
+ ].join("\n");
1308
+ fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1309
+ ${section}
1310
+ `);
1311
+ }
1312
+ }
1313
+ function detectHookManager(projectRoot) {
1314
+ if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1315
+ if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1316
+ if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1317
+ return void 0;
1318
+ }
1319
+ function setupClaudeCodeHook(projectRoot) {
1320
+ const claudeDir = path12.join(projectRoot, ".claude");
1321
+ if (!fs11.existsSync(claudeDir)) {
1322
+ fs11.mkdirSync(claudeDir, { recursive: true });
1323
+ }
1324
+ const settingsPath = path12.join(claudeDir, "settings.json");
1325
+ let settings = {};
1326
+ if (fs11.existsSync(settingsPath)) {
1327
+ try {
1328
+ settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1329
+ } catch {
1330
+ console.warn(
1331
+ ` ${chalk8.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1332
+ );
1333
+ settings = {};
1334
+ }
1335
+ }
1336
+ const hooks = settings.hooks ?? {};
1337
+ const existing = hooks.PostToolUse ?? [];
1338
+ if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1339
+ const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1340
+ const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1341
+ hooks.PostToolUse = [
1342
+ ...existing,
1343
+ {
1344
+ matcher: "Edit|Write",
1345
+ hooks: [
1346
+ {
1347
+ type: "command",
1348
+ command: hookCommand
1349
+ }
1350
+ ]
1351
+ }
1352
+ ];
1353
+ settings.hooks = hooks;
1354
+ fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1078
1355
  `);
1356
+ console.log(` ${chalk8.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1079
1357
  }
1080
1358
  function writeHuskyPreCommit(huskyDir) {
1081
1359
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1091,187 +1369,6 @@ npx viberails check --staged
1091
1369
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1092
1370
  }
1093
1371
 
1094
- // src/commands/init-wizard.ts
1095
- import * as p from "@clack/prompts";
1096
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES3, LIBRARY_NAMES as LIBRARY_NAMES2, STYLING_NAMES as STYLING_NAMES3 } from "@viberails/types";
1097
- import chalk8 from "chalk";
1098
-
1099
- // src/display.ts
1100
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
1101
- import chalk7 from "chalk";
1102
-
1103
- // src/display-helpers.ts
1104
- import { ROLE_DESCRIPTIONS } from "@viberails/types";
1105
- function groupByRole(directories) {
1106
- const map = /* @__PURE__ */ new Map();
1107
- for (const dir of directories) {
1108
- if (dir.role === "unknown") continue;
1109
- const existing = map.get(dir.role);
1110
- if (existing) {
1111
- existing.dirs.push(dir);
1112
- } else {
1113
- map.set(dir.role, { dirs: [dir] });
1114
- }
1115
- }
1116
- const groups = [];
1117
- for (const [role, { dirs }] of map) {
1118
- const label = ROLE_DESCRIPTIONS[role] ?? role;
1119
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1120
- groups.push({
1121
- role,
1122
- label,
1123
- dirCount: dirs.length,
1124
- totalFiles,
1125
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
1126
- });
1127
- }
1128
- return groups;
1129
- }
1130
- function formatSummary(stats, packageCount) {
1131
- const parts = [];
1132
- if (packageCount && packageCount > 1) {
1133
- parts.push(`${packageCount} packages`);
1134
- }
1135
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1136
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1137
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1138
- return parts.join(" \xB7 ");
1139
- }
1140
- function formatRoleGroup(group) {
1141
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1142
- if (group.singlePath) {
1143
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1144
- }
1145
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1146
- return `${group.label} \u2014 ${dirs} (${files})`;
1147
- }
1148
-
1149
- // src/display-monorepo.ts
1150
- import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1151
- import chalk6 from "chalk";
1152
-
1153
- // src/display.ts
1154
- function formatItem(item, nameMap) {
1155
- const name = nameMap?.[item.name] ?? item.name;
1156
- return item.version ? `${name} ${item.version}` : name;
1157
- }
1158
-
1159
- // src/commands/init-wizard.ts
1160
- var DEFAULT_WIZARD_RESULT = {
1161
- enforcement: "warn",
1162
- checks: {
1163
- fileSize: true,
1164
- naming: true,
1165
- tests: true,
1166
- boundaries: false
1167
- },
1168
- integration: ["pre-commit"]
1169
- };
1170
- async function runWizard(scanResult) {
1171
- const isMonorepo = scanResult.packages.length > 1;
1172
- displayScanSummary(scanResult);
1173
- const enforcement = await p.select({
1174
- message: "How strict should viberails be?",
1175
- initialValue: "warn",
1176
- options: [
1177
- { value: "warn", label: "Warn", hint: "show issues, never block commits" },
1178
- { value: "enforce", label: "Enforce", hint: "block commits with violations" }
1179
- ]
1180
- });
1181
- if (p.isCancel(enforcement)) {
1182
- p.cancel("Setup cancelled.");
1183
- return null;
1184
- }
1185
- const checkOptions = [
1186
- { value: "fileSize", label: "File size limit (300 lines)" },
1187
- { value: "naming", label: "File naming conventions" },
1188
- { value: "tests", label: "Missing test files" }
1189
- ];
1190
- if (isMonorepo) {
1191
- checkOptions.push({ value: "boundaries", label: "Import boundaries" });
1192
- }
1193
- const enabledChecks = await p.multiselect({
1194
- message: "Which checks should viberails run?",
1195
- options: checkOptions,
1196
- initialValues: ["fileSize", "naming", "tests"],
1197
- required: false
1198
- });
1199
- if (p.isCancel(enabledChecks)) {
1200
- p.cancel("Setup cancelled.");
1201
- return null;
1202
- }
1203
- const checks = {
1204
- fileSize: enabledChecks.includes("fileSize"),
1205
- naming: enabledChecks.includes("naming"),
1206
- tests: enabledChecks.includes("tests"),
1207
- boundaries: enabledChecks.includes("boundaries")
1208
- };
1209
- const integrationOptions = [
1210
- { value: "pre-commit", label: "Git pre-commit hook", hint: "runs on every commit" },
1211
- {
1212
- value: "claude-hook",
1213
- label: "Claude Code hook",
1214
- hint: "checks files as Claude edits them"
1215
- },
1216
- { value: "context-only", label: "Context files only", hint: "no hooks" }
1217
- ];
1218
- const integration = await p.multiselect({
1219
- message: "Where should checks run?",
1220
- options: integrationOptions,
1221
- initialValues: ["pre-commit"],
1222
- required: true
1223
- });
1224
- if (p.isCancel(integration)) {
1225
- p.cancel("Setup cancelled.");
1226
- return null;
1227
- }
1228
- const finalIntegration = integration.includes("context-only") ? ["context-only"] : integration;
1229
- return {
1230
- enforcement,
1231
- checks,
1232
- integration: finalIntegration
1233
- };
1234
- }
1235
- function displayScanSummary(scanResult) {
1236
- const { stack } = scanResult;
1237
- const parts = [];
1238
- if (stack.framework) parts.push(formatItem(stack.framework, FRAMEWORK_NAMES3));
1239
- parts.push(formatItem(stack.language));
1240
- if (stack.styling) parts.push(formatItem(stack.styling, STYLING_NAMES3));
1241
- if (stack.backend) parts.push(formatItem(stack.backend, FRAMEWORK_NAMES3));
1242
- p.log.info(`${chalk8.bold("Stack:")} ${parts.join(", ")}`);
1243
- if (stack.linter || stack.formatter || stack.testRunner || stack.packageManager) {
1244
- const tools = [];
1245
- if (stack.linter) tools.push(formatItem(stack.linter));
1246
- if (stack.formatter && stack.formatter !== stack.linter)
1247
- tools.push(formatItem(stack.formatter));
1248
- if (stack.testRunner) tools.push(formatItem(stack.testRunner));
1249
- if (stack.packageManager) tools.push(formatItem(stack.packageManager));
1250
- p.log.info(`${chalk8.bold("Tools:")} ${tools.join(", ")}`);
1251
- }
1252
- if (stack.libraries.length > 0) {
1253
- const libs = stack.libraries.map((lib) => formatItem(lib, LIBRARY_NAMES2)).join(", ");
1254
- p.log.info(`${chalk8.bold("Libraries:")} ${libs}`);
1255
- }
1256
- const groups = groupByRole(scanResult.structure.directories);
1257
- if (groups.length > 0) {
1258
- const structParts = groups.map((g) => formatRoleGroup(g));
1259
- p.log.info(`${chalk8.bold("Structure:")} ${structParts.join(", ")}`);
1260
- }
1261
- const conventionEntries = Object.entries(scanResult.conventions).filter(
1262
- ([, c]) => c.confidence !== "low"
1263
- );
1264
- if (conventionEntries.length > 0) {
1265
- const convParts = conventionEntries.map(([, c]) => {
1266
- const pct = Math.round(c.consistency);
1267
- return `${c.value} (${pct}%)`;
1268
- });
1269
- p.log.info(`${chalk8.bold("Conventions:")} ${convParts.join(", ")}`);
1270
- }
1271
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1272
- p.log.info(`${chalk8.bold("Summary:")} ${formatSummary(scanResult.statistics, pkgCount)}`);
1273
- }
1274
-
1275
1372
  // src/commands/init.ts
1276
1373
  var CONFIG_FILE4 = "viberails.config.json";
1277
1374
  function filterHighConfidence(conventions) {
@@ -1286,13 +1383,6 @@ function filterHighConfidence(conventions) {
1286
1383
  }
1287
1384
  return filtered;
1288
1385
  }
1289
- function applyWizardResult(config, wizard) {
1290
- config.enforcement = wizard.enforcement;
1291
- if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
1292
- config.rules.enforceNaming = wizard.checks.naming;
1293
- config.rules.requireTests = wizard.checks.tests;
1294
- config.rules.enforceBoundaries = wizard.checks.boundaries;
1295
- }
1296
1386
  async function initCommand(options, cwd) {
1297
1387
  const startDir = cwd ?? process.cwd();
1298
1388
  const projectRoot = findProjectRoot(startDir);
@@ -1308,65 +1398,78 @@ async function initCommand(options, cwd) {
1308
1398
  );
1309
1399
  return;
1310
1400
  }
1311
- p2.intro("viberails");
1312
- const s = p2.spinner();
1313
- s.start("Scanning project...");
1401
+ console.log(chalk9.dim("Scanning project..."));
1314
1402
  const scanResult = await scan(projectRoot);
1315
- s.stop("Scan complete");
1316
- if (scanResult.statistics.totalFiles === 0) {
1317
- p2.log.warn(
1318
- `No source files detected. viberails will generate context with minimal content.
1319
- Run ${chalk9.cyan("viberails sync")} after adding source files.`
1320
- );
1321
- }
1322
- let wizard;
1323
- if (options.yes) {
1324
- wizard = { ...DEFAULT_WIZARD_RESULT };
1325
- } else {
1326
- const result = await runWizard(scanResult);
1327
- if (!result) return;
1328
- wizard = result;
1329
- }
1330
1403
  const config = generateConfig(scanResult);
1331
1404
  if (options.yes) {
1332
1405
  config.conventions = filterHighConfidence(config.conventions);
1333
1406
  }
1334
- applyWizardResult(config, wizard);
1335
- if (wizard.checks.boundaries && config.workspace && config.workspace.packages.length > 0) {
1336
- s.start("Inferring boundary rules...");
1337
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1338
- const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1339
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1340
- const inferred = inferBoundaries(graph);
1341
- const ruleCount = Object.values(inferred).reduce((sum, denied) => sum + denied.length, 0);
1342
- if (ruleCount > 0) {
1343
- config.boundaries = inferred;
1344
- s.stop(`Inferred ${ruleCount} boundary rules`);
1345
- } else {
1346
- s.stop("No boundary rules could be inferred");
1347
- config.rules.enforceBoundaries = false;
1407
+ displayScanResults(scanResult);
1408
+ if (scanResult.statistics.totalFiles === 0) {
1409
+ console.log(
1410
+ chalk9.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + chalk9.cyan("viberails sync") + " after adding source files.\n"
1411
+ );
1412
+ }
1413
+ displayRulesPreview(config);
1414
+ if (!options.yes) {
1415
+ const accepted = await confirm("Proceed with these settings?");
1416
+ if (!accepted) {
1417
+ console.log("Aborted.");
1418
+ return;
1348
1419
  }
1349
1420
  }
1421
+ if (config.workspace && config.workspace.packages.length > 0) {
1422
+ let shouldInfer = options.yes;
1423
+ if (!options.yes) {
1424
+ console.log(chalk9.dim(" Scans imports between packages to suggest dependency rules"));
1425
+ shouldInfer = await confirm("Infer boundary rules from import patterns?");
1426
+ }
1427
+ if (shouldInfer) {
1428
+ console.log(chalk9.dim("Building import graph..."));
1429
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1430
+ const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1431
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1432
+ const inferred = inferBoundaries(graph);
1433
+ if (inferred.length > 0) {
1434
+ config.boundaries = inferred;
1435
+ config.rules.enforceBoundaries = true;
1436
+ console.log(` ${chalk9.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1437
+ }
1438
+ }
1439
+ }
1440
+ const hookManager = detectHookManager(projectRoot);
1441
+ let integrations = { preCommitHook: true, claudeCodeHook: true };
1442
+ if (!options.yes) {
1443
+ console.log("");
1444
+ integrations = await selectIntegrations(hookManager);
1445
+ }
1350
1446
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1351
1447
  `);
1352
1448
  writeGeneratedFiles(projectRoot, config, scanResult);
1353
1449
  updateGitignore(projectRoot);
1354
- if (wizard.integration.includes("pre-commit")) {
1450
+ console.log(`
1451
+ ${chalk9.bold("Created:")}`);
1452
+ console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE4}`);
1453
+ console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
1454
+ console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
1455
+ if (integrations.preCommitHook) {
1355
1456
  setupPreCommitHook(projectRoot);
1356
1457
  }
1357
- if (wizard.integration.includes("claude-hook")) {
1458
+ if (integrations.claudeCodeHook) {
1358
1459
  setupClaudeCodeHook(projectRoot);
1359
1460
  }
1360
- p2.log.success(`${chalk9.bold("Created:")}`);
1361
- console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE4}`);
1362
- console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
1363
- console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
1364
- p2.outro(
1365
- `${chalk9.bold("Next steps:")}
1366
- 1. Review ${chalk9.cyan("viberails.config.json")} and adjust rules
1367
- 2. Commit ${chalk9.cyan("viberails.config.json")} and ${chalk9.cyan(".viberails/context.md")}
1368
- 3. Run ${chalk9.cyan("viberails check")} to verify your project passes`
1369
- );
1461
+ const filesToCommit = [
1462
+ `${chalk9.cyan("viberails.config.json")}`,
1463
+ chalk9.cyan(".viberails/context.md")
1464
+ ];
1465
+ if (integrations.claudeCodeHook) {
1466
+ filesToCommit.push(chalk9.cyan(".claude/settings.json"));
1467
+ }
1468
+ console.log(`
1469
+ ${chalk9.bold("Next steps:")}`);
1470
+ console.log(` 1. Review ${chalk9.cyan("viberails.config.json")} and adjust rules`);
1471
+ console.log(` 2. Commit ${filesToCommit.join(", ")}`);
1472
+ console.log(` 3. Run ${chalk9.cyan("viberails check")} to verify your project passes`);
1370
1473
  }
1371
1474
  function updateGitignore(projectRoot) {
1372
1475
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1376,8 +1479,9 @@ function updateGitignore(projectRoot) {
1376
1479
  }
1377
1480
  if (!content.includes(".viberails/scan-result.json")) {
1378
1481
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1379
- fs12.writeFileSync(gitignorePath, `${content.trimEnd()}
1380
- ${block}`);
1482
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1483
+ `;
1484
+ fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1381
1485
  }
1382
1486
  }
1383
1487
 
@@ -1401,18 +1505,30 @@ async function syncCommand(cwd) {
1401
1505
  console.log(chalk10.dim("Scanning project..."));
1402
1506
  const scanResult = await scan2(projectRoot);
1403
1507
  const merged = mergeConfig(existing, scanResult);
1404
- fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1508
+ const existingJson = JSON.stringify(existing, null, 2);
1509
+ const mergedJson = JSON.stringify(merged, null, 2);
1510
+ const configChanged = existingJson !== mergedJson;
1511
+ if (configChanged) {
1512
+ console.log(
1513
+ ` ${chalk10.yellow("!")} Config updated \u2014 review ${chalk10.cyan(CONFIG_FILE5)} for changes`
1514
+ );
1515
+ }
1516
+ fs13.writeFileSync(configPath, `${mergedJson}
1405
1517
  `);
1406
1518
  writeGeneratedFiles(projectRoot, merged, scanResult);
1407
1519
  console.log(`
1408
1520
  ${chalk10.bold("Synced:")}`);
1409
- console.log(` ${chalk10.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1521
+ if (configChanged) {
1522
+ console.log(` ${chalk10.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1523
+ } else {
1524
+ console.log(` ${chalk10.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1525
+ }
1410
1526
  console.log(` ${chalk10.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1411
1527
  console.log(` ${chalk10.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1412
1528
  }
1413
1529
 
1414
1530
  // src/index.ts
1415
- var VERSION = "0.3.0";
1531
+ var VERSION = "0.3.1";
1416
1532
  var program = new Command();
1417
1533
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1418
1534
  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) => {
@@ -1433,12 +1549,13 @@ program.command("sync").description("Re-scan and update generated files").action
1433
1549
  process.exit(1);
1434
1550
  }
1435
1551
  });
1436
- 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(
1552
+ 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(
1437
1553
  async (options) => {
1438
1554
  try {
1439
1555
  const exitCode = await checkCommand({
1440
1556
  ...options,
1441
- noBoundaries: options.boundaries === false
1557
+ noBoundaries: options.boundaries === false,
1558
+ format: options.format === "json" ? "json" : "text"
1442
1559
  });
1443
1560
  process.exit(exitCode);
1444
1561
  } catch (err) {