viberails 0.3.0 → 0.3.2

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
@@ -34,7 +34,7 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk11 = __toESM(require("chalk"), 1);
37
+ var import_chalk10 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -61,19 +61,104 @@ function findProjectRoot(startDir) {
61
61
  }
62
62
 
63
63
  // src/utils/prompt.ts
64
- var readline = __toESM(require("readline"), 1);
65
- async function confirm(message) {
66
- const rl = readline.createInterface({
67
- input: process.stdin,
68
- output: process.stdout
64
+ var clack = __toESM(require("@clack/prompts"), 1);
65
+ function assertNotCancelled(value) {
66
+ if (clack.isCancel(value)) {
67
+ clack.cancel("Setup cancelled.");
68
+ process.exit(0);
69
+ }
70
+ }
71
+ async function confirm2(message) {
72
+ const result = await clack.confirm({ message, initialValue: true });
73
+ assertNotCancelled(result);
74
+ return result;
75
+ }
76
+ async function confirmDangerous(message) {
77
+ const result = await clack.confirm({ message, initialValue: false });
78
+ assertNotCancelled(result);
79
+ return result;
80
+ }
81
+ async function promptInitDecision() {
82
+ const result = await clack.select({
83
+ message: "Accept these settings?",
84
+ options: [
85
+ { value: "accept", label: "Yes, looks good", hint: "recommended" },
86
+ { value: "customize", label: "Let me customize" }
87
+ ]
69
88
  });
70
- return new Promise((resolve4) => {
71
- rl.question(`${message} (Y/n) `, (answer) => {
72
- rl.close();
73
- const trimmed = answer.trim().toLowerCase();
74
- resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
75
- });
89
+ assertNotCancelled(result);
90
+ return result;
91
+ }
92
+ async function promptRuleCustomization(defaults) {
93
+ const maxFileLinesResult = await clack.text({
94
+ message: "Maximum lines per source file?",
95
+ placeholder: String(defaults.maxFileLines),
96
+ initialValue: String(defaults.maxFileLines),
97
+ validate: (v) => {
98
+ const n = Number.parseInt(v, 10);
99
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
100
+ }
101
+ });
102
+ assertNotCancelled(maxFileLinesResult);
103
+ const requireTestsResult = await clack.confirm({
104
+ message: "Require matching test files for source files?",
105
+ initialValue: defaults.requireTests
106
+ });
107
+ assertNotCancelled(requireTestsResult);
108
+ const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
109
+ const enforceNamingResult = await clack.confirm({
110
+ message: namingLabel,
111
+ initialValue: defaults.enforceNaming
112
+ });
113
+ assertNotCancelled(enforceNamingResult);
114
+ const enforcementResult = await clack.select({
115
+ message: "Enforcement mode",
116
+ options: [
117
+ {
118
+ value: "warn",
119
+ label: "warn",
120
+ hint: "show violations but don't block commits (recommended)"
121
+ },
122
+ {
123
+ value: "enforce",
124
+ label: "enforce",
125
+ hint: "block commits with violations"
126
+ }
127
+ ],
128
+ initialValue: defaults.enforcement
129
+ });
130
+ assertNotCancelled(enforcementResult);
131
+ return {
132
+ maxFileLines: Number.parseInt(maxFileLinesResult, 10),
133
+ requireTests: requireTestsResult,
134
+ enforceNaming: enforceNamingResult,
135
+ enforcement: enforcementResult
136
+ };
137
+ }
138
+ async function promptIntegrations(hookManager) {
139
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
140
+ const result = await clack.multiselect({
141
+ message: "Set up integrations?",
142
+ options: [
143
+ {
144
+ value: "preCommit",
145
+ label: hookLabel,
146
+ hint: "runs checks when you commit"
147
+ },
148
+ {
149
+ value: "claude",
150
+ label: "Claude Code hook",
151
+ hint: "checks files when Claude edits them"
152
+ }
153
+ ],
154
+ initialValues: ["preCommit", "claude"],
155
+ required: false
76
156
  });
157
+ assertNotCancelled(result);
158
+ return {
159
+ preCommitHook: result.includes("preCommit"),
160
+ claudeCodeHook: result.includes("claude")
161
+ };
77
162
  }
78
163
 
79
164
  // src/utils/resolve-workspace-packages.ts
@@ -99,7 +184,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
99
184
  ];
100
185
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
101
186
  }
102
- const packageNames = new Set(packages.map((p3) => p3.name));
187
+ const packageNames = new Set(packages.map((p) => p.name));
103
188
  for (const pkg of packages) {
104
189
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
105
190
  }
@@ -129,37 +214,23 @@ async function boundariesCommand(options, cwd) {
129
214
  }
130
215
  displayRules(config);
131
216
  }
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
217
  function displayRules(config) {
138
- const total = countBoundaries(config.boundaries);
139
- if (total === 0) {
218
+ if (!config.boundaries || config.boundaries.length === 0) {
140
219
  console.log(import_chalk.default.yellow("No boundary rules configured."));
141
220
  console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
142
221
  return;
143
222
  }
223
+ const allowRules = config.boundaries.filter((r) => r.allow);
224
+ const denyRules = config.boundaries.filter((r) => !r.allow);
144
225
  console.log(`
145
- ${import_chalk.default.bold(`Boundary rules (${total} rules):`)}
226
+ ${import_chalk.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
146
227
  `);
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
- }
228
+ for (const r of allowRules) {
229
+ console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
230
+ }
231
+ for (const r of denyRules) {
232
+ const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
233
+ console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
163
234
  }
164
235
  console.log(
165
236
  `
@@ -176,29 +247,32 @@ async function inferAndDisplay(projectRoot, config, configPath) {
176
247
  });
177
248
  console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
178
249
  const inferred = inferBoundaries(graph);
179
- const entries = Object.entries(inferred);
180
- if (entries.length === 0) {
250
+ if (inferred.length === 0) {
181
251
  console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
182
252
  return;
183
253
  }
184
- const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
254
+ const allow = inferred.filter((r) => r.allow);
255
+ const deny = inferred.filter((r) => !r.allow);
185
256
  console.log(`
186
257
  ${import_chalk.default.bold("Inferred boundary rules:")}
187
258
  `);
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
- }
259
+ for (const r of allow) {
260
+ console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
261
+ }
262
+ for (const r of deny) {
263
+ const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
264
+ console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
192
265
  }
193
266
  console.log(`
194
- ${totalRules} deny rules`);
195
- const shouldSave = await confirm("\nSave to viberails.config.json?");
267
+ ${allow.length} allowed, ${deny.length} denied`);
268
+ console.log("");
269
+ const shouldSave = await confirm2("Save to viberails.config.json?");
196
270
  if (shouldSave) {
197
271
  config.boundaries = inferred;
198
272
  config.rules.enforceBoundaries = true;
199
273
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
200
274
  `);
201
- console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
275
+ console.log(`${import_chalk.default.green("\u2713")} Saved ${inferred.length} rules`);
202
276
  }
203
277
  }
204
278
  async function showGraph(projectRoot, config) {
@@ -268,6 +342,7 @@ function resolveIgnoreForFile(relPath, config) {
268
342
  var import_node_child_process = require("child_process");
269
343
  var fs4 = __toESM(require("fs"), 1);
270
344
  var path4 = __toESM(require("path"), 1);
345
+ var import_picomatch = __toESM(require("picomatch"), 1);
271
346
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
272
347
  "node_modules",
273
348
  ".git",
@@ -303,25 +378,9 @@ var NAMING_PATTERNS = {
303
378
  snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
304
379
  };
305
380
  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;
381
+ if (ignorePatterns.length === 0) return false;
382
+ const isMatch = (0, import_picomatch.default)(ignorePatterns, { dot: true });
383
+ return isMatch(relPath);
325
384
  }
326
385
  function countFileLines(filePath) {
327
386
  try {
@@ -572,8 +631,7 @@ async function checkCommand(options, cwd) {
572
631
  const testViolations = checkMissingTests(projectRoot, config, severity);
573
632
  violations.push(...testViolations);
574
633
  }
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) {
634
+ if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
577
635
  const startTime = Date.now();
578
636
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
579
637
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -596,6 +654,16 @@ async function checkCommand(options, cwd) {
596
654
  const elapsed = Date.now() - startTime;
597
655
  console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
598
656
  }
657
+ if (options.format === "json") {
658
+ console.log(
659
+ JSON.stringify({
660
+ violations,
661
+ checkedFiles: filesToCheck.length,
662
+ enforcement: config.enforcement
663
+ })
664
+ );
665
+ return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
666
+ }
599
667
  if (violations.length === 0) {
600
668
  console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
601
669
  return 0;
@@ -619,7 +687,6 @@ var import_chalk4 = __toESM(require("chalk"), 1);
619
687
 
620
688
  // src/commands/fix-helpers.ts
621
689
  var import_node_child_process2 = require("child_process");
622
- var import_node_readline = require("readline");
623
690
  var import_chalk3 = __toESM(require("chalk"), 1);
624
691
  function printPlan(renames, stubs) {
625
692
  if (renames.length > 0) {
@@ -653,15 +720,6 @@ function getConventionValue(convention) {
653
720
  }
654
721
  return void 0;
655
722
  }
656
- function promptConfirm(question) {
657
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
658
- return new Promise((resolve4) => {
659
- rl.question(`${question} (y/N) `, (answer) => {
660
- rl.close();
661
- resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
662
- });
663
- });
664
- }
665
723
 
666
724
  // src/commands/fix-imports.ts
667
725
  var path7 = __toESM(require("path"), 1);
@@ -695,7 +753,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
695
753
  const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
696
754
  for (const sourceFile of project.getSourceFiles()) {
697
755
  const filePath = sourceFile.getFilePath();
698
- if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
756
+ const segments = filePath.split(path7.sep);
757
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
699
758
  const fileDir = path7.dirname(filePath);
700
759
  for (const decl of sourceFile.getImportDeclarations()) {
701
760
  const specifier = decl.getModuleSpecifierValue();
@@ -935,7 +994,7 @@ async function fixCommand(options, cwd) {
935
994
  return 0;
936
995
  }
937
996
  if (!options.yes) {
938
- const confirmed = await promptConfirm("Apply these fixes?");
997
+ const confirmed = await confirmDangerous("Apply these fixes?");
939
998
  if (!confirmed) {
940
999
  console.log("Aborted.");
941
1000
  return 0;
@@ -978,10 +1037,422 @@ async function fixCommand(options, cwd) {
978
1037
  // src/commands/init.ts
979
1038
  var fs12 = __toESM(require("fs"), 1);
980
1039
  var path13 = __toESM(require("path"), 1);
981
- var p2 = __toESM(require("@clack/prompts"), 1);
1040
+ var clack2 = __toESM(require("@clack/prompts"), 1);
982
1041
  var import_config4 = require("@viberails/config");
983
1042
  var import_scanner = require("@viberails/scanner");
984
- var import_chalk9 = __toESM(require("chalk"), 1);
1043
+ var import_chalk8 = __toESM(require("chalk"), 1);
1044
+
1045
+ // src/display.ts
1046
+ var import_types3 = require("@viberails/types");
1047
+ var import_chalk6 = __toESM(require("chalk"), 1);
1048
+
1049
+ // src/display-helpers.ts
1050
+ var import_types = require("@viberails/types");
1051
+ function groupByRole(directories) {
1052
+ const map = /* @__PURE__ */ new Map();
1053
+ for (const dir of directories) {
1054
+ if (dir.role === "unknown") continue;
1055
+ const existing = map.get(dir.role);
1056
+ if (existing) {
1057
+ existing.dirs.push(dir);
1058
+ } else {
1059
+ map.set(dir.role, { dirs: [dir] });
1060
+ }
1061
+ }
1062
+ const groups = [];
1063
+ for (const [role, { dirs }] of map) {
1064
+ const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1065
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1066
+ groups.push({
1067
+ role,
1068
+ label,
1069
+ dirCount: dirs.length,
1070
+ totalFiles,
1071
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1072
+ });
1073
+ }
1074
+ return groups;
1075
+ }
1076
+ function formatSummary(stats, packageCount) {
1077
+ const parts = [];
1078
+ if (packageCount && packageCount > 1) {
1079
+ parts.push(`${packageCount} packages`);
1080
+ }
1081
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1082
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1083
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1084
+ return parts.join(" \xB7 ");
1085
+ }
1086
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1087
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1088
+ }
1089
+ function formatRoleGroup(group) {
1090
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1091
+ if (group.singlePath) {
1092
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1093
+ }
1094
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1095
+ return `${group.label} \u2014 ${dirs} (${files})`;
1096
+ }
1097
+
1098
+ // src/display-monorepo.ts
1099
+ var import_types2 = require("@viberails/types");
1100
+ var import_chalk5 = __toESM(require("chalk"), 1);
1101
+ function formatPackageSummary(pkg) {
1102
+ const parts = [];
1103
+ if (pkg.stack.framework) {
1104
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1105
+ }
1106
+ if (pkg.stack.styling) {
1107
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1108
+ }
1109
+ const files = `${pkg.statistics.totalFiles} files`;
1110
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1111
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1112
+ }
1113
+ function displayMonorepoResults(scanResult) {
1114
+ const { stack, packages } = scanResult;
1115
+ console.log(`
1116
+ ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1117
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
1118
+ if (stack.packageManager) {
1119
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1120
+ }
1121
+ if (stack.linter) {
1122
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
1123
+ }
1124
+ if (stack.formatter) {
1125
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1126
+ }
1127
+ if (stack.testRunner) {
1128
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1129
+ }
1130
+ console.log("");
1131
+ for (const pkg of packages) {
1132
+ console.log(formatPackageSummary(pkg));
1133
+ }
1134
+ const packagesWithDirs = packages.filter(
1135
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1136
+ );
1137
+ if (packagesWithDirs.length > 0) {
1138
+ console.log(`
1139
+ ${import_chalk5.default.bold("Structure:")}`);
1140
+ for (const pkg of packagesWithDirs) {
1141
+ const groups = groupByRole(pkg.structure.directories);
1142
+ if (groups.length === 0) continue;
1143
+ console.log(` ${pkg.relativePath}:`);
1144
+ for (const group of groups) {
1145
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
1146
+ }
1147
+ }
1148
+ }
1149
+ displayConventions(scanResult);
1150
+ displaySummarySection(scanResult);
1151
+ console.log("");
1152
+ }
1153
+ function formatPackageSummaryPlain(pkg) {
1154
+ const parts = [];
1155
+ if (pkg.stack.framework) {
1156
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1157
+ }
1158
+ if (pkg.stack.styling) {
1159
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1160
+ }
1161
+ const files = `${pkg.statistics.totalFiles} files`;
1162
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1163
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1164
+ }
1165
+ function formatMonorepoResultsText(scanResult, config) {
1166
+ const lines = [];
1167
+ const { stack, packages } = scanResult;
1168
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1169
+ const sharedParts = [formatItem(stack.language)];
1170
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1171
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1172
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1173
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1174
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1175
+ lines.push("");
1176
+ for (const pkg of packages) {
1177
+ lines.push(formatPackageSummaryPlain(pkg));
1178
+ }
1179
+ const packagesWithDirs = packages.filter(
1180
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1181
+ );
1182
+ if (packagesWithDirs.length > 0) {
1183
+ lines.push("");
1184
+ lines.push("Structure:");
1185
+ for (const pkg of packagesWithDirs) {
1186
+ const groups = groupByRole(pkg.structure.directories);
1187
+ if (groups.length === 0) continue;
1188
+ lines.push(` ${pkg.relativePath}:`);
1189
+ for (const group of groups) {
1190
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1191
+ }
1192
+ }
1193
+ }
1194
+ lines.push(...formatConventionsText(scanResult));
1195
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1196
+ lines.push("");
1197
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1198
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1199
+ if (ext) {
1200
+ lines.push(ext);
1201
+ }
1202
+ lines.push(...formatRulesText(config));
1203
+ return lines.join("\n");
1204
+ }
1205
+
1206
+ // src/display.ts
1207
+ var CONVENTION_LABELS = {
1208
+ fileNaming: "File naming",
1209
+ componentNaming: "Component naming",
1210
+ hookNaming: "Hook naming",
1211
+ importAlias: "Import alias"
1212
+ };
1213
+ function formatItem(item, nameMap) {
1214
+ const name = nameMap?.[item.name] ?? item.name;
1215
+ return item.version ? `${name} ${item.version}` : name;
1216
+ }
1217
+ function confidenceLabel(convention) {
1218
+ const pct = Math.round(convention.consistency);
1219
+ if (convention.confidence === "high") {
1220
+ return `${pct}% \u2014 high confidence, will enforce`;
1221
+ }
1222
+ return `${pct}% \u2014 medium confidence, suggested only`;
1223
+ }
1224
+ function displayConventions(scanResult) {
1225
+ const conventionEntries = Object.entries(scanResult.conventions);
1226
+ if (conventionEntries.length === 0) return;
1227
+ console.log(`
1228
+ ${import_chalk6.default.bold("Conventions:")}`);
1229
+ for (const [key, convention] of conventionEntries) {
1230
+ if (convention.confidence === "low") continue;
1231
+ const label = CONVENTION_LABELS[key] ?? key;
1232
+ if (scanResult.packages.length > 1) {
1233
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1234
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1235
+ if (allSame || pkgValues.length <= 1) {
1236
+ const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1237
+ const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1238
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1239
+ } else {
1240
+ console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
1241
+ for (const pv of pkgValues) {
1242
+ const pct = Math.round(pv.convention.consistency);
1243
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1244
+ }
1245
+ }
1246
+ } else {
1247
+ const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1248
+ const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1249
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1250
+ }
1251
+ }
1252
+ }
1253
+ function displaySummarySection(scanResult) {
1254
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1255
+ console.log(`
1256
+ ${import_chalk6.default.bold("Summary:")}`);
1257
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1258
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1259
+ if (ext) {
1260
+ console.log(` ${ext}`);
1261
+ }
1262
+ }
1263
+ function displayScanResults(scanResult) {
1264
+ if (scanResult.packages.length > 1) {
1265
+ displayMonorepoResults(scanResult);
1266
+ return;
1267
+ }
1268
+ const { stack } = scanResult;
1269
+ console.log(`
1270
+ ${import_chalk6.default.bold("Detected:")}`);
1271
+ if (stack.framework) {
1272
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1273
+ }
1274
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1275
+ if (stack.styling) {
1276
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1277
+ }
1278
+ if (stack.backend) {
1279
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1280
+ }
1281
+ if (stack.orm) {
1282
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1283
+ }
1284
+ if (stack.linter) {
1285
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1286
+ }
1287
+ if (stack.formatter) {
1288
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1289
+ }
1290
+ if (stack.testRunner) {
1291
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1292
+ }
1293
+ if (stack.packageManager) {
1294
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1295
+ }
1296
+ if (stack.libraries.length > 0) {
1297
+ for (const lib of stack.libraries) {
1298
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1299
+ }
1300
+ }
1301
+ const groups = groupByRole(scanResult.structure.directories);
1302
+ if (groups.length > 0) {
1303
+ console.log(`
1304
+ ${import_chalk6.default.bold("Structure:")}`);
1305
+ for (const group of groups) {
1306
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1307
+ }
1308
+ }
1309
+ displayConventions(scanResult);
1310
+ displaySummarySection(scanResult);
1311
+ console.log("");
1312
+ }
1313
+ function getConventionStr(cv) {
1314
+ return typeof cv === "string" ? cv : cv.value;
1315
+ }
1316
+ function displayRulesPreview(config) {
1317
+ console.log(`${import_chalk6.default.bold("Rules:")}`);
1318
+ console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1319
+ if (config.rules.requireTests && config.structure.testPattern) {
1320
+ console.log(
1321
+ ` ${import_chalk6.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1322
+ );
1323
+ } else if (config.rules.requireTests) {
1324
+ console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: yes`);
1325
+ } else {
1326
+ console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: no`);
1327
+ }
1328
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1329
+ console.log(
1330
+ ` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1331
+ );
1332
+ } else {
1333
+ console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
1334
+ }
1335
+ console.log(
1336
+ ` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1337
+ );
1338
+ console.log("");
1339
+ if (config.enforcement === "enforce") {
1340
+ console.log(`${import_chalk6.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
1341
+ } else {
1342
+ console.log(
1343
+ `${import_chalk6.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1344
+ );
1345
+ }
1346
+ console.log("");
1347
+ }
1348
+ function plainConfidenceLabel(convention) {
1349
+ const pct = Math.round(convention.consistency);
1350
+ if (convention.confidence === "high") {
1351
+ return `${pct}%`;
1352
+ }
1353
+ return `${pct}%, suggested only`;
1354
+ }
1355
+ function formatConventionsText(scanResult) {
1356
+ const lines = [];
1357
+ const conventionEntries = Object.entries(scanResult.conventions);
1358
+ if (conventionEntries.length === 0) return lines;
1359
+ lines.push("");
1360
+ lines.push("Conventions:");
1361
+ for (const [key, convention] of conventionEntries) {
1362
+ if (convention.confidence === "low") continue;
1363
+ const label = CONVENTION_LABELS[key] ?? key;
1364
+ if (scanResult.packages.length > 1) {
1365
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1366
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1367
+ if (allSame || pkgValues.length <= 1) {
1368
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1369
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1370
+ } else {
1371
+ lines.push(` ~ ${label}: varies by package`);
1372
+ for (const pv of pkgValues) {
1373
+ const pct = Math.round(pv.convention.consistency);
1374
+ lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1375
+ }
1376
+ }
1377
+ } else {
1378
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1379
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1380
+ }
1381
+ }
1382
+ return lines;
1383
+ }
1384
+ function formatRulesText(config) {
1385
+ const lines = [];
1386
+ lines.push("");
1387
+ lines.push("Rules:");
1388
+ lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
1389
+ if (config.rules.requireTests && config.structure.testPattern) {
1390
+ lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
1391
+ } else if (config.rules.requireTests) {
1392
+ lines.push(" \u2022 Require test files: yes");
1393
+ } else {
1394
+ lines.push(" \u2022 Require test files: no");
1395
+ }
1396
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1397
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
1398
+ } else {
1399
+ lines.push(" \u2022 Enforce file naming: no");
1400
+ }
1401
+ lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
1402
+ return lines;
1403
+ }
1404
+ function formatScanResultsText(scanResult, config) {
1405
+ if (scanResult.packages.length > 1) {
1406
+ return formatMonorepoResultsText(scanResult, config);
1407
+ }
1408
+ const lines = [];
1409
+ const { stack } = scanResult;
1410
+ lines.push("Detected:");
1411
+ if (stack.framework) {
1412
+ lines.push(` \u2713 ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1413
+ }
1414
+ lines.push(` \u2713 ${formatItem(stack.language)}`);
1415
+ if (stack.styling) {
1416
+ lines.push(` \u2713 ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1417
+ }
1418
+ if (stack.backend) {
1419
+ lines.push(` \u2713 ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1420
+ }
1421
+ if (stack.orm) {
1422
+ lines.push(` \u2713 ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1423
+ }
1424
+ const secondaryParts = [];
1425
+ if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
1426
+ if (stack.linter) secondaryParts.push(formatItem(stack.linter));
1427
+ if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
1428
+ if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
1429
+ if (secondaryParts.length > 0) {
1430
+ lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
1431
+ }
1432
+ if (stack.libraries.length > 0) {
1433
+ for (const lib of stack.libraries) {
1434
+ lines.push(` \u2713 ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1435
+ }
1436
+ }
1437
+ const groups = groupByRole(scanResult.structure.directories);
1438
+ if (groups.length > 0) {
1439
+ lines.push("");
1440
+ lines.push("Structure:");
1441
+ for (const group of groups) {
1442
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1443
+ }
1444
+ }
1445
+ lines.push(...formatConventionsText(scanResult));
1446
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1447
+ lines.push("");
1448
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1449
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1450
+ if (ext) {
1451
+ lines.push(ext);
1452
+ }
1453
+ lines.push(...formatRulesText(config));
1454
+ return lines.join("\n");
1455
+ }
985
1456
 
986
1457
  // src/utils/write-generated-files.ts
987
1458
  var fs10 = __toESM(require("fs"), 1);
@@ -1012,60 +1483,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1012
1483
  // src/commands/init-hooks.ts
1013
1484
  var fs11 = __toESM(require("fs"), 1);
1014
1485
  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
- }
1486
+ var import_chalk7 = __toESM(require("chalk"), 1);
1058
1487
  function setupPreCommitHook(projectRoot) {
1059
1488
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1060
1489
  if (fs11.existsSync(lefthookPath)) {
1061
1490
  addLefthookPreCommit(lefthookPath);
1062
- console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1491
+ console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1063
1492
  return;
1064
1493
  }
1065
1494
  const huskyDir = path12.join(projectRoot, ".husky");
1066
1495
  if (fs11.existsSync(huskyDir)) {
1067
1496
  writeHuskyPreCommit(huskyDir);
1068
- console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1497
+ console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1069
1498
  return;
1070
1499
  }
1071
1500
  const gitDir = path12.join(projectRoot, ".git");
@@ -1075,7 +1504,7 @@ function setupPreCommitHook(projectRoot) {
1075
1504
  fs11.mkdirSync(hooksDir, { recursive: true });
1076
1505
  }
1077
1506
  writeGitHookPreCommit(hooksDir);
1078
- console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
1507
+ console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
1079
1508
  }
1080
1509
  }
1081
1510
  function writeGitHookPreCommit(hooksDir) {
@@ -1105,10 +1534,72 @@ npx viberails check --staged
1105
1534
  function addLefthookPreCommit(lefthookPath) {
1106
1535
  const content = fs11.readFileSync(lefthookPath, "utf-8");
1107
1536
  if (content.includes("viberails")) return;
1108
- const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
1109
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1110
- ${addition}
1537
+ const hasPreCommit = /^pre-commit:/m.test(content);
1538
+ if (hasPreCommit) {
1539
+ const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1540
+ "\n"
1541
+ );
1542
+ const updated = `${content.trimEnd()}
1543
+ ${commandBlock}
1544
+ `;
1545
+ fs11.writeFileSync(lefthookPath, updated);
1546
+ } else {
1547
+ const section = [
1548
+ "",
1549
+ "pre-commit:",
1550
+ " commands:",
1551
+ " viberails:",
1552
+ " run: npx viberails check --staged"
1553
+ ].join("\n");
1554
+ fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1555
+ ${section}
1556
+ `);
1557
+ }
1558
+ }
1559
+ function detectHookManager(projectRoot) {
1560
+ if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1561
+ if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1562
+ if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1563
+ return void 0;
1564
+ }
1565
+ function setupClaudeCodeHook(projectRoot) {
1566
+ const claudeDir = path12.join(projectRoot, ".claude");
1567
+ if (!fs11.existsSync(claudeDir)) {
1568
+ fs11.mkdirSync(claudeDir, { recursive: true });
1569
+ }
1570
+ const settingsPath = path12.join(claudeDir, "settings.json");
1571
+ let settings = {};
1572
+ if (fs11.existsSync(settingsPath)) {
1573
+ try {
1574
+ settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1575
+ } catch {
1576
+ console.warn(
1577
+ ` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1578
+ );
1579
+ settings = {};
1580
+ }
1581
+ }
1582
+ const hooks = settings.hooks ?? {};
1583
+ const existing = hooks.PostToolUse ?? [];
1584
+ if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1585
+ const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1586
+ const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1587
+ hooks.PostToolUse = [
1588
+ ...existing,
1589
+ {
1590
+ matcher: "Edit|Write",
1591
+ hooks: [
1592
+ {
1593
+ type: "command",
1594
+ command: hookCommand
1595
+ }
1596
+ ]
1597
+ }
1598
+ ];
1599
+ settings.hooks = hooks;
1600
+ fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1111
1601
  `);
1602
+ console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1112
1603
  }
1113
1604
  function writeHuskyPreCommit(huskyDir) {
1114
1605
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1124,187 +1615,6 @@ npx viberails check --staged
1124
1615
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1125
1616
  }
1126
1617
 
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
1618
  // src/commands/init.ts
1309
1619
  var CONFIG_FILE4 = "viberails.config.json";
1310
1620
  function filterHighConfidence(conventions) {
@@ -1319,12 +1629,13 @@ function filterHighConfidence(conventions) {
1319
1629
  }
1320
1630
  return filtered;
1321
1631
  }
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;
1632
+ function getConventionStr2(cv) {
1633
+ if (!cv) return void 0;
1634
+ return typeof cv === "string" ? cv : cv.value;
1635
+ }
1636
+ function hasConventionOverrides(config) {
1637
+ if (!config.packages || config.packages.length === 0) return false;
1638
+ return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1328
1639
  }
1329
1640
  async function initCommand(options, cwd) {
1330
1641
  const startDir = cwd ?? process.cwd();
@@ -1337,69 +1648,137 @@ async function initCommand(options, cwd) {
1337
1648
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1338
1649
  if (fs12.existsSync(configPath)) {
1339
1650
  console.log(
1340
- import_chalk9.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk9.default.cyan("viberails sync") + " to update the generated files."
1651
+ `${import_chalk8.default.yellow("!")} viberails is already initialized.
1652
+ Run ${import_chalk8.default.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
1341
1653
  );
1342
1654
  return;
1343
1655
  }
1344
- p2.intro("viberails");
1345
- const s = p2.spinner();
1656
+ if (options.yes) {
1657
+ console.log(import_chalk8.default.dim("Scanning project..."));
1658
+ const scanResult2 = await (0, import_scanner.scan)(projectRoot);
1659
+ const config2 = (0, import_config4.generateConfig)(scanResult2);
1660
+ config2.conventions = filterHighConfidence(config2.conventions);
1661
+ displayScanResults(scanResult2);
1662
+ displayRulesPreview(config2);
1663
+ if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
1664
+ console.log(import_chalk8.default.dim("Building import graph..."));
1665
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1666
+ const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
1667
+ const graph = await buildImportGraph(projectRoot, {
1668
+ packages,
1669
+ ignore: config2.ignore
1670
+ });
1671
+ const inferred = inferBoundaries(graph);
1672
+ if (inferred.length > 0) {
1673
+ config2.boundaries = inferred;
1674
+ config2.rules.enforceBoundaries = true;
1675
+ console.log(` Inferred ${inferred.length} boundary rules`);
1676
+ }
1677
+ }
1678
+ fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1679
+ `);
1680
+ writeGeneratedFiles(projectRoot, config2, scanResult2);
1681
+ updateGitignore(projectRoot);
1682
+ console.log(`
1683
+ Created:`);
1684
+ console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
1685
+ console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
1686
+ console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
1687
+ return;
1688
+ }
1689
+ clack2.intro("viberails");
1690
+ const s = clack2.spinner();
1346
1691
  s.start("Scanning project...");
1347
1692
  const scanResult = await (0, import_scanner.scan)(projectRoot);
1693
+ const config = (0, import_config4.generateConfig)(scanResult);
1348
1694
  s.stop("Scan complete");
1349
1695
  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.`
1696
+ clack2.log.warn(
1697
+ "No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
1353
1698
  );
1354
1699
  }
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;
1700
+ const resultsText = formatScanResultsText(scanResult, config);
1701
+ clack2.note(resultsText, "Scan results");
1702
+ const decision = await promptInitDecision();
1703
+ if (decision === "customize") {
1704
+ clack2.note(
1705
+ "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1706
+ "Rules"
1707
+ );
1708
+ const overrides = await promptRuleCustomization({
1709
+ maxFileLines: config.rules.maxFileLines,
1710
+ requireTests: config.rules.requireTests,
1711
+ enforceNaming: config.rules.enforceNaming,
1712
+ enforcement: config.enforcement,
1713
+ fileNamingValue: getConventionStr2(config.conventions.fileNaming)
1714
+ });
1715
+ config.rules.maxFileLines = overrides.maxFileLines;
1716
+ config.rules.requireTests = overrides.requireTests;
1717
+ config.rules.enforceNaming = overrides.enforceNaming;
1718
+ config.enforcement = overrides.enforcement;
1719
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1720
+ clack2.note(
1721
+ 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1722
+ "Per-package overrides"
1723
+ );
1724
+ }
1362
1725
  }
1363
- const config = (0, import_config4.generateConfig)(scanResult);
1364
- if (options.yes) {
1365
- config.conventions = filterHighConfidence(config.conventions);
1366
- }
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;
1726
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1727
+ clack2.note(
1728
+ "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
1729
+ "Boundaries"
1730
+ );
1731
+ const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
1732
+ if (shouldInfer) {
1733
+ const bs = clack2.spinner();
1734
+ bs.start("Building import graph...");
1735
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1736
+ const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1737
+ const graph = await buildImportGraph(projectRoot, {
1738
+ packages,
1739
+ ignore: config.ignore
1740
+ });
1741
+ const inferred = inferBoundaries(graph);
1742
+ if (inferred.length > 0) {
1743
+ config.boundaries = inferred;
1744
+ config.rules.enforceBoundaries = true;
1745
+ bs.stop(`Inferred ${inferred.length} boundary rules`);
1746
+ } else {
1747
+ bs.stop("No boundary rules inferred");
1748
+ }
1381
1749
  }
1382
1750
  }
1751
+ const hookManager = detectHookManager(projectRoot);
1752
+ const integrations = await promptIntegrations(hookManager);
1753
+ if (hasConventionOverrides(config)) {
1754
+ clack2.note(
1755
+ "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1756
+ "Per-package conventions"
1757
+ );
1758
+ }
1383
1759
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1384
1760
  `);
1385
1761
  writeGeneratedFiles(projectRoot, config, scanResult);
1386
1762
  updateGitignore(projectRoot);
1387
- if (wizard.integration.includes("pre-commit")) {
1763
+ const createdFiles = [
1764
+ CONFIG_FILE4,
1765
+ ".viberails/context.md",
1766
+ ".viberails/scan-result.json"
1767
+ ];
1768
+ if (integrations.preCommitHook) {
1388
1769
  setupPreCommitHook(projectRoot);
1770
+ const hookMgr = detectHookManager(projectRoot);
1771
+ if (hookMgr) {
1772
+ createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1773
+ }
1389
1774
  }
1390
- if (wizard.integration.includes("claude-hook")) {
1775
+ if (integrations.claudeCodeHook) {
1391
1776
  setupClaudeCodeHook(projectRoot);
1777
+ createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1392
1778
  }
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
- );
1779
+ clack2.log.success(`Created:
1780
+ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1781
+ clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
1403
1782
  }
1404
1783
  function updateGitignore(projectRoot) {
1405
1784
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1409,8 +1788,9 @@ function updateGitignore(projectRoot) {
1409
1788
  }
1410
1789
  if (!content.includes(".viberails/scan-result.json")) {
1411
1790
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1412
- fs12.writeFileSync(gitignorePath, `${content.trimEnd()}
1413
- ${block}`);
1791
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1792
+ `;
1793
+ fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1414
1794
  }
1415
1795
  }
1416
1796
 
@@ -1419,7 +1799,7 @@ var fs13 = __toESM(require("fs"), 1);
1419
1799
  var path14 = __toESM(require("path"), 1);
1420
1800
  var import_config5 = require("@viberails/config");
1421
1801
  var import_scanner2 = require("@viberails/scanner");
1422
- var import_chalk10 = __toESM(require("chalk"), 1);
1802
+ var import_chalk9 = __toESM(require("chalk"), 1);
1423
1803
  var CONFIG_FILE5 = "viberails.config.json";
1424
1804
  async function syncCommand(cwd) {
1425
1805
  const startDir = cwd ?? process.cwd();
@@ -1431,21 +1811,33 @@ async function syncCommand(cwd) {
1431
1811
  }
1432
1812
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1433
1813
  const existing = await (0, import_config5.loadConfig)(configPath);
1434
- console.log(import_chalk10.default.dim("Scanning project..."));
1814
+ console.log(import_chalk9.default.dim("Scanning project..."));
1435
1815
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
1436
1816
  const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1437
- fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1817
+ const existingJson = JSON.stringify(existing, null, 2);
1818
+ const mergedJson = JSON.stringify(merged, null, 2);
1819
+ const configChanged = existingJson !== mergedJson;
1820
+ if (configChanged) {
1821
+ console.log(
1822
+ ` ${import_chalk9.default.yellow("!")} Config updated \u2014 review ${import_chalk9.default.cyan(CONFIG_FILE5)} for changes`
1823
+ );
1824
+ }
1825
+ fs13.writeFileSync(configPath, `${mergedJson}
1438
1826
  `);
1439
1827
  writeGeneratedFiles(projectRoot, merged, scanResult);
1440
1828
  console.log(`
1441
- ${import_chalk10.default.bold("Synced:")}`);
1442
- console.log(` ${import_chalk10.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1443
- console.log(` ${import_chalk10.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1444
- console.log(` ${import_chalk10.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1829
+ ${import_chalk9.default.bold("Synced:")}`);
1830
+ if (configChanged) {
1831
+ console.log(` ${import_chalk9.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1832
+ } else {
1833
+ console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1834
+ }
1835
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1836
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1445
1837
  }
1446
1838
 
1447
1839
  // src/index.ts
1448
- var VERSION = "0.3.0";
1840
+ var VERSION = "0.3.2";
1449
1841
  var program = new import_commander.Command();
1450
1842
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1451
1843
  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) => {
@@ -1453,7 +1845,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
1453
1845
  await initCommand(options);
1454
1846
  } catch (err) {
1455
1847
  const message = err instanceof Error ? err.message : String(err);
1456
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1848
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1457
1849
  process.exit(1);
1458
1850
  }
1459
1851
  });
@@ -1462,21 +1854,22 @@ program.command("sync").description("Re-scan and update generated files").action
1462
1854
  await syncCommand();
1463
1855
  } catch (err) {
1464
1856
  const message = err instanceof Error ? err.message : String(err);
1465
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1857
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1466
1858
  process.exit(1);
1467
1859
  }
1468
1860
  });
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(
1861
+ 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
1862
  async (options) => {
1471
1863
  try {
1472
1864
  const exitCode = await checkCommand({
1473
1865
  ...options,
1474
- noBoundaries: options.boundaries === false
1866
+ noBoundaries: options.boundaries === false,
1867
+ format: options.format === "json" ? "json" : "text"
1475
1868
  });
1476
1869
  process.exit(exitCode);
1477
1870
  } catch (err) {
1478
1871
  const message = err instanceof Error ? err.message : String(err);
1479
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1872
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1480
1873
  process.exit(1);
1481
1874
  }
1482
1875
  }
@@ -1487,7 +1880,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1487
1880
  process.exit(exitCode);
1488
1881
  } catch (err) {
1489
1882
  const message = err instanceof Error ? err.message : String(err);
1490
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1883
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1491
1884
  process.exit(1);
1492
1885
  }
1493
1886
  });
@@ -1496,7 +1889,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1496
1889
  await boundariesCommand(options);
1497
1890
  } catch (err) {
1498
1891
  const message = err instanceof Error ? err.message : String(err);
1499
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1892
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1500
1893
  process.exit(1);
1501
1894
  }
1502
1895
  });