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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import chalk11 from "chalk";
4
+ import chalk10 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/boundaries.ts
@@ -28,19 +28,104 @@ function findProjectRoot(startDir) {
28
28
  }
29
29
 
30
30
  // src/utils/prompt.ts
31
- import * as readline from "readline";
32
- async function confirm(message) {
33
- const rl = readline.createInterface({
34
- input: process.stdin,
35
- output: process.stdout
31
+ import * as clack from "@clack/prompts";
32
+ function assertNotCancelled(value) {
33
+ if (clack.isCancel(value)) {
34
+ clack.cancel("Setup cancelled.");
35
+ process.exit(0);
36
+ }
37
+ }
38
+ async function confirm2(message) {
39
+ const result = await clack.confirm({ message, initialValue: true });
40
+ assertNotCancelled(result);
41
+ return result;
42
+ }
43
+ async function confirmDangerous(message) {
44
+ const result = await clack.confirm({ message, initialValue: false });
45
+ assertNotCancelled(result);
46
+ return result;
47
+ }
48
+ async function promptInitDecision() {
49
+ const result = await clack.select({
50
+ message: "Accept these settings?",
51
+ options: [
52
+ { value: "accept", label: "Yes, looks good", hint: "recommended" },
53
+ { value: "customize", label: "Let me customize" }
54
+ ]
36
55
  });
37
- return new Promise((resolve4) => {
38
- rl.question(`${message} (Y/n) `, (answer) => {
39
- rl.close();
40
- const trimmed = answer.trim().toLowerCase();
41
- resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
42
- });
56
+ assertNotCancelled(result);
57
+ return result;
58
+ }
59
+ async function promptRuleCustomization(defaults) {
60
+ const maxFileLinesResult = await clack.text({
61
+ message: "Maximum lines per source file?",
62
+ placeholder: String(defaults.maxFileLines),
63
+ initialValue: String(defaults.maxFileLines),
64
+ validate: (v) => {
65
+ const n = Number.parseInt(v, 10);
66
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
67
+ }
68
+ });
69
+ assertNotCancelled(maxFileLinesResult);
70
+ const requireTestsResult = await clack.confirm({
71
+ message: "Require matching test files for source files?",
72
+ initialValue: defaults.requireTests
73
+ });
74
+ assertNotCancelled(requireTestsResult);
75
+ const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
76
+ const enforceNamingResult = await clack.confirm({
77
+ message: namingLabel,
78
+ initialValue: defaults.enforceNaming
79
+ });
80
+ assertNotCancelled(enforceNamingResult);
81
+ const enforcementResult = await clack.select({
82
+ message: "Enforcement mode",
83
+ options: [
84
+ {
85
+ value: "warn",
86
+ label: "warn",
87
+ hint: "show violations but don't block commits (recommended)"
88
+ },
89
+ {
90
+ value: "enforce",
91
+ label: "enforce",
92
+ hint: "block commits with violations"
93
+ }
94
+ ],
95
+ initialValue: defaults.enforcement
96
+ });
97
+ assertNotCancelled(enforcementResult);
98
+ return {
99
+ maxFileLines: Number.parseInt(maxFileLinesResult, 10),
100
+ requireTests: requireTestsResult,
101
+ enforceNaming: enforceNamingResult,
102
+ enforcement: enforcementResult
103
+ };
104
+ }
105
+ async function promptIntegrations(hookManager) {
106
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
107
+ const result = await clack.multiselect({
108
+ message: "Set up integrations?",
109
+ options: [
110
+ {
111
+ value: "preCommit",
112
+ label: hookLabel,
113
+ hint: "runs checks when you commit"
114
+ },
115
+ {
116
+ value: "claude",
117
+ label: "Claude Code hook",
118
+ hint: "checks files when Claude edits them"
119
+ }
120
+ ],
121
+ initialValues: ["preCommit", "claude"],
122
+ required: false
43
123
  });
124
+ assertNotCancelled(result);
125
+ return {
126
+ preCommitHook: result.includes("preCommit"),
127
+ claudeCodeHook: result.includes("claude")
128
+ };
44
129
  }
45
130
 
46
131
  // src/utils/resolve-workspace-packages.ts
@@ -66,7 +151,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
66
151
  ];
67
152
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
68
153
  }
69
- const packageNames = new Set(packages.map((p3) => p3.name));
154
+ const packageNames = new Set(packages.map((p) => p.name));
70
155
  for (const pkg of packages) {
71
156
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
72
157
  }
@@ -96,37 +181,23 @@ async function boundariesCommand(options, cwd) {
96
181
  }
97
182
  displayRules(config);
98
183
  }
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
184
  function displayRules(config) {
105
- const total = countBoundaries(config.boundaries);
106
- if (total === 0) {
185
+ if (!config.boundaries || config.boundaries.length === 0) {
107
186
  console.log(chalk.yellow("No boundary rules configured."));
108
187
  console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
109
188
  return;
110
189
  }
190
+ const allowRules = config.boundaries.filter((r) => r.allow);
191
+ const denyRules = config.boundaries.filter((r) => !r.allow);
111
192
  console.log(`
112
- ${chalk.bold(`Boundary rules (${total} rules):`)}
193
+ ${chalk.bold(`Boundary rules (${config.boundaries.length} rules):`)}
113
194
  `);
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
- }
195
+ for (const r of allowRules) {
196
+ console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
197
+ }
198
+ for (const r of denyRules) {
199
+ const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
200
+ console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
130
201
  }
131
202
  console.log(
132
203
  `
@@ -143,29 +214,32 @@ async function inferAndDisplay(projectRoot, config, configPath) {
143
214
  });
144
215
  console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
145
216
  const inferred = inferBoundaries(graph);
146
- const entries = Object.entries(inferred);
147
- if (entries.length === 0) {
217
+ if (inferred.length === 0) {
148
218
  console.log(chalk.yellow("No boundary rules could be inferred."));
149
219
  return;
150
220
  }
151
- const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
221
+ const allow = inferred.filter((r) => r.allow);
222
+ const deny = inferred.filter((r) => !r.allow);
152
223
  console.log(`
153
224
  ${chalk.bold("Inferred boundary rules:")}
154
225
  `);
155
- for (const [from, denied] of entries) {
156
- for (const to of denied) {
157
- console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
158
- }
226
+ for (const r of allow) {
227
+ console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
228
+ }
229
+ for (const r of deny) {
230
+ const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
231
+ console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
159
232
  }
160
233
  console.log(`
161
- ${totalRules} deny rules`);
162
- const shouldSave = await confirm("\nSave to viberails.config.json?");
234
+ ${allow.length} allowed, ${deny.length} denied`);
235
+ console.log("");
236
+ const shouldSave = await confirm2("Save to viberails.config.json?");
163
237
  if (shouldSave) {
164
238
  config.boundaries = inferred;
165
239
  config.rules.enforceBoundaries = true;
166
240
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
167
241
  `);
168
- console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
242
+ console.log(`${chalk.green("\u2713")} Saved ${inferred.length} rules`);
169
243
  }
170
244
  }
171
245
  async function showGraph(projectRoot, config) {
@@ -235,6 +309,7 @@ function resolveIgnoreForFile(relPath, config) {
235
309
  import { execSync } from "child_process";
236
310
  import * as fs4 from "fs";
237
311
  import * as path4 from "path";
312
+ import picomatch from "picomatch";
238
313
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
239
314
  "node_modules",
240
315
  ".git",
@@ -270,25 +345,9 @@ var NAMING_PATTERNS = {
270
345
  snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
271
346
  };
272
347
  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;
348
+ if (ignorePatterns.length === 0) return false;
349
+ const isMatch = picomatch(ignorePatterns, { dot: true });
350
+ return isMatch(relPath);
292
351
  }
293
352
  function countFileLines(filePath) {
294
353
  try {
@@ -539,8 +598,7 @@ async function checkCommand(options, cwd) {
539
598
  const testViolations = checkMissingTests(projectRoot, config, severity);
540
599
  violations.push(...testViolations);
541
600
  }
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) {
601
+ if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
544
602
  const startTime = Date.now();
545
603
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
546
604
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -563,6 +621,16 @@ async function checkCommand(options, cwd) {
563
621
  const elapsed = Date.now() - startTime;
564
622
  console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
565
623
  }
624
+ if (options.format === "json") {
625
+ console.log(
626
+ JSON.stringify({
627
+ violations,
628
+ checkedFiles: filesToCheck.length,
629
+ enforcement: config.enforcement
630
+ })
631
+ );
632
+ return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
633
+ }
566
634
  if (violations.length === 0) {
567
635
  console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
568
636
  return 0;
@@ -586,7 +654,6 @@ import chalk4 from "chalk";
586
654
 
587
655
  // src/commands/fix-helpers.ts
588
656
  import { execSync as execSync2 } from "child_process";
589
- import { createInterface as createInterface2 } from "readline";
590
657
  import chalk3 from "chalk";
591
658
  function printPlan(renames, stubs) {
592
659
  if (renames.length > 0) {
@@ -620,15 +687,6 @@ function getConventionValue(convention) {
620
687
  }
621
688
  return void 0;
622
689
  }
623
- function promptConfirm(question) {
624
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
625
- return new Promise((resolve4) => {
626
- rl.question(`${question} (y/N) `, (answer) => {
627
- rl.close();
628
- resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
629
- });
630
- });
631
- }
632
690
 
633
691
  // src/commands/fix-imports.ts
634
692
  import * as path7 from "path";
@@ -662,7 +720,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
662
720
  const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
663
721
  for (const sourceFile of project.getSourceFiles()) {
664
722
  const filePath = sourceFile.getFilePath();
665
- if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
723
+ const segments = filePath.split(path7.sep);
724
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
666
725
  const fileDir = path7.dirname(filePath);
667
726
  for (const decl of sourceFile.getImportDeclarations()) {
668
727
  const specifier = decl.getModuleSpecifierValue();
@@ -902,7 +961,7 @@ async function fixCommand(options, cwd) {
902
961
  return 0;
903
962
  }
904
963
  if (!options.yes) {
905
- const confirmed = await promptConfirm("Apply these fixes?");
964
+ const confirmed = await confirmDangerous("Apply these fixes?");
906
965
  if (!confirmed) {
907
966
  console.log("Aborted.");
908
967
  return 0;
@@ -945,10 +1004,422 @@ async function fixCommand(options, cwd) {
945
1004
  // src/commands/init.ts
946
1005
  import * as fs12 from "fs";
947
1006
  import * as path13 from "path";
948
- import * as p2 from "@clack/prompts";
1007
+ import * as clack2 from "@clack/prompts";
949
1008
  import { generateConfig } from "@viberails/config";
950
1009
  import { scan } from "@viberails/scanner";
951
- import chalk9 from "chalk";
1010
+ import chalk8 from "chalk";
1011
+
1012
+ // src/display.ts
1013
+ import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, ORM_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
1014
+ import chalk6 from "chalk";
1015
+
1016
+ // src/display-helpers.ts
1017
+ import { ROLE_DESCRIPTIONS } from "@viberails/types";
1018
+ function groupByRole(directories) {
1019
+ const map = /* @__PURE__ */ new Map();
1020
+ for (const dir of directories) {
1021
+ if (dir.role === "unknown") continue;
1022
+ const existing = map.get(dir.role);
1023
+ if (existing) {
1024
+ existing.dirs.push(dir);
1025
+ } else {
1026
+ map.set(dir.role, { dirs: [dir] });
1027
+ }
1028
+ }
1029
+ const groups = [];
1030
+ for (const [role, { dirs }] of map) {
1031
+ const label = ROLE_DESCRIPTIONS[role] ?? role;
1032
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1033
+ groups.push({
1034
+ role,
1035
+ label,
1036
+ dirCount: dirs.length,
1037
+ totalFiles,
1038
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1039
+ });
1040
+ }
1041
+ return groups;
1042
+ }
1043
+ function formatSummary(stats, packageCount) {
1044
+ const parts = [];
1045
+ if (packageCount && packageCount > 1) {
1046
+ parts.push(`${packageCount} packages`);
1047
+ }
1048
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1049
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1050
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1051
+ return parts.join(" \xB7 ");
1052
+ }
1053
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1054
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1055
+ }
1056
+ function formatRoleGroup(group) {
1057
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1058
+ if (group.singlePath) {
1059
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1060
+ }
1061
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1062
+ return `${group.label} \u2014 ${dirs} (${files})`;
1063
+ }
1064
+
1065
+ // src/display-monorepo.ts
1066
+ import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1067
+ import chalk5 from "chalk";
1068
+ function formatPackageSummary(pkg) {
1069
+ const parts = [];
1070
+ if (pkg.stack.framework) {
1071
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1072
+ }
1073
+ if (pkg.stack.styling) {
1074
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1075
+ }
1076
+ const files = `${pkg.statistics.totalFiles} files`;
1077
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1078
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1079
+ }
1080
+ function displayMonorepoResults(scanResult) {
1081
+ const { stack, packages } = scanResult;
1082
+ console.log(`
1083
+ ${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1084
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
1085
+ if (stack.packageManager) {
1086
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
1087
+ }
1088
+ if (stack.linter) {
1089
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
1090
+ }
1091
+ if (stack.formatter) {
1092
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
1093
+ }
1094
+ if (stack.testRunner) {
1095
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
1096
+ }
1097
+ console.log("");
1098
+ for (const pkg of packages) {
1099
+ console.log(formatPackageSummary(pkg));
1100
+ }
1101
+ const packagesWithDirs = packages.filter(
1102
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1103
+ );
1104
+ if (packagesWithDirs.length > 0) {
1105
+ console.log(`
1106
+ ${chalk5.bold("Structure:")}`);
1107
+ for (const pkg of packagesWithDirs) {
1108
+ const groups = groupByRole(pkg.structure.directories);
1109
+ if (groups.length === 0) continue;
1110
+ console.log(` ${pkg.relativePath}:`);
1111
+ for (const group of groups) {
1112
+ console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
1113
+ }
1114
+ }
1115
+ }
1116
+ displayConventions(scanResult);
1117
+ displaySummarySection(scanResult);
1118
+ console.log("");
1119
+ }
1120
+ function formatPackageSummaryPlain(pkg) {
1121
+ const parts = [];
1122
+ if (pkg.stack.framework) {
1123
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1124
+ }
1125
+ if (pkg.stack.styling) {
1126
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1127
+ }
1128
+ const files = `${pkg.statistics.totalFiles} files`;
1129
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1130
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1131
+ }
1132
+ function formatMonorepoResultsText(scanResult, config) {
1133
+ const lines = [];
1134
+ const { stack, packages } = scanResult;
1135
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1136
+ const sharedParts = [formatItem(stack.language)];
1137
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1138
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1139
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1140
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1141
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1142
+ lines.push("");
1143
+ for (const pkg of packages) {
1144
+ lines.push(formatPackageSummaryPlain(pkg));
1145
+ }
1146
+ const packagesWithDirs = packages.filter(
1147
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1148
+ );
1149
+ if (packagesWithDirs.length > 0) {
1150
+ lines.push("");
1151
+ lines.push("Structure:");
1152
+ for (const pkg of packagesWithDirs) {
1153
+ const groups = groupByRole(pkg.structure.directories);
1154
+ if (groups.length === 0) continue;
1155
+ lines.push(` ${pkg.relativePath}:`);
1156
+ for (const group of groups) {
1157
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1158
+ }
1159
+ }
1160
+ }
1161
+ lines.push(...formatConventionsText(scanResult));
1162
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1163
+ lines.push("");
1164
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1165
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1166
+ if (ext) {
1167
+ lines.push(ext);
1168
+ }
1169
+ lines.push(...formatRulesText(config));
1170
+ return lines.join("\n");
1171
+ }
1172
+
1173
+ // src/display.ts
1174
+ var CONVENTION_LABELS = {
1175
+ fileNaming: "File naming",
1176
+ componentNaming: "Component naming",
1177
+ hookNaming: "Hook naming",
1178
+ importAlias: "Import alias"
1179
+ };
1180
+ function formatItem(item, nameMap) {
1181
+ const name = nameMap?.[item.name] ?? item.name;
1182
+ return item.version ? `${name} ${item.version}` : name;
1183
+ }
1184
+ function confidenceLabel(convention) {
1185
+ const pct = Math.round(convention.consistency);
1186
+ if (convention.confidence === "high") {
1187
+ return `${pct}% \u2014 high confidence, will enforce`;
1188
+ }
1189
+ return `${pct}% \u2014 medium confidence, suggested only`;
1190
+ }
1191
+ function displayConventions(scanResult) {
1192
+ const conventionEntries = Object.entries(scanResult.conventions);
1193
+ if (conventionEntries.length === 0) return;
1194
+ console.log(`
1195
+ ${chalk6.bold("Conventions:")}`);
1196
+ for (const [key, convention] of conventionEntries) {
1197
+ if (convention.confidence === "low") continue;
1198
+ const label = CONVENTION_LABELS[key] ?? key;
1199
+ if (scanResult.packages.length > 1) {
1200
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1201
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1202
+ if (allSame || pkgValues.length <= 1) {
1203
+ const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1204
+ const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1205
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1206
+ } else {
1207
+ console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
1208
+ for (const pv of pkgValues) {
1209
+ const pct = Math.round(pv.convention.consistency);
1210
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1211
+ }
1212
+ }
1213
+ } else {
1214
+ const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1215
+ const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1216
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1217
+ }
1218
+ }
1219
+ }
1220
+ function displaySummarySection(scanResult) {
1221
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1222
+ console.log(`
1223
+ ${chalk6.bold("Summary:")}`);
1224
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1225
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1226
+ if (ext) {
1227
+ console.log(` ${ext}`);
1228
+ }
1229
+ }
1230
+ function displayScanResults(scanResult) {
1231
+ if (scanResult.packages.length > 1) {
1232
+ displayMonorepoResults(scanResult);
1233
+ return;
1234
+ }
1235
+ const { stack } = scanResult;
1236
+ console.log(`
1237
+ ${chalk6.bold("Detected:")}`);
1238
+ if (stack.framework) {
1239
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1240
+ }
1241
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
1242
+ if (stack.styling) {
1243
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1244
+ }
1245
+ if (stack.backend) {
1246
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1247
+ }
1248
+ if (stack.orm) {
1249
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
1250
+ }
1251
+ if (stack.linter) {
1252
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
1253
+ }
1254
+ if (stack.formatter) {
1255
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
1256
+ }
1257
+ if (stack.testRunner) {
1258
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
1259
+ }
1260
+ if (stack.packageManager) {
1261
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
1262
+ }
1263
+ if (stack.libraries.length > 0) {
1264
+ for (const lib of stack.libraries) {
1265
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1266
+ }
1267
+ }
1268
+ const groups = groupByRole(scanResult.structure.directories);
1269
+ if (groups.length > 0) {
1270
+ console.log(`
1271
+ ${chalk6.bold("Structure:")}`);
1272
+ for (const group of groups) {
1273
+ console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
1274
+ }
1275
+ }
1276
+ displayConventions(scanResult);
1277
+ displaySummarySection(scanResult);
1278
+ console.log("");
1279
+ }
1280
+ function getConventionStr(cv) {
1281
+ return typeof cv === "string" ? cv : cv.value;
1282
+ }
1283
+ function displayRulesPreview(config) {
1284
+ console.log(`${chalk6.bold("Rules:")}`);
1285
+ console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1286
+ if (config.rules.requireTests && config.structure.testPattern) {
1287
+ console.log(
1288
+ ` ${chalk6.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1289
+ );
1290
+ } else if (config.rules.requireTests) {
1291
+ console.log(` ${chalk6.dim("\u2022")} Require test files: yes`);
1292
+ } else {
1293
+ console.log(` ${chalk6.dim("\u2022")} Require test files: no`);
1294
+ }
1295
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1296
+ console.log(
1297
+ ` ${chalk6.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1298
+ );
1299
+ } else {
1300
+ console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
1301
+ }
1302
+ console.log(
1303
+ ` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1304
+ );
1305
+ console.log("");
1306
+ if (config.enforcement === "enforce") {
1307
+ console.log(`${chalk6.bold("Enforcement mode:")} enforce (violations will block commits)`);
1308
+ } else {
1309
+ console.log(
1310
+ `${chalk6.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1311
+ );
1312
+ }
1313
+ console.log("");
1314
+ }
1315
+ function plainConfidenceLabel(convention) {
1316
+ const pct = Math.round(convention.consistency);
1317
+ if (convention.confidence === "high") {
1318
+ return `${pct}%`;
1319
+ }
1320
+ return `${pct}%, suggested only`;
1321
+ }
1322
+ function formatConventionsText(scanResult) {
1323
+ const lines = [];
1324
+ const conventionEntries = Object.entries(scanResult.conventions);
1325
+ if (conventionEntries.length === 0) return lines;
1326
+ lines.push("");
1327
+ lines.push("Conventions:");
1328
+ for (const [key, convention] of conventionEntries) {
1329
+ if (convention.confidence === "low") continue;
1330
+ const label = CONVENTION_LABELS[key] ?? key;
1331
+ if (scanResult.packages.length > 1) {
1332
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1333
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1334
+ if (allSame || pkgValues.length <= 1) {
1335
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1336
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1337
+ } else {
1338
+ lines.push(` ~ ${label}: varies by package`);
1339
+ for (const pv of pkgValues) {
1340
+ const pct = Math.round(pv.convention.consistency);
1341
+ lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1342
+ }
1343
+ }
1344
+ } else {
1345
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1346
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1347
+ }
1348
+ }
1349
+ return lines;
1350
+ }
1351
+ function formatRulesText(config) {
1352
+ const lines = [];
1353
+ lines.push("");
1354
+ lines.push("Rules:");
1355
+ lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
1356
+ if (config.rules.requireTests && config.structure.testPattern) {
1357
+ lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
1358
+ } else if (config.rules.requireTests) {
1359
+ lines.push(" \u2022 Require test files: yes");
1360
+ } else {
1361
+ lines.push(" \u2022 Require test files: no");
1362
+ }
1363
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1364
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
1365
+ } else {
1366
+ lines.push(" \u2022 Enforce file naming: no");
1367
+ }
1368
+ lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
1369
+ return lines;
1370
+ }
1371
+ function formatScanResultsText(scanResult, config) {
1372
+ if (scanResult.packages.length > 1) {
1373
+ return formatMonorepoResultsText(scanResult, config);
1374
+ }
1375
+ const lines = [];
1376
+ const { stack } = scanResult;
1377
+ lines.push("Detected:");
1378
+ if (stack.framework) {
1379
+ lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1380
+ }
1381
+ lines.push(` \u2713 ${formatItem(stack.language)}`);
1382
+ if (stack.styling) {
1383
+ lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES2)}`);
1384
+ }
1385
+ if (stack.backend) {
1386
+ lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1387
+ }
1388
+ if (stack.orm) {
1389
+ lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES)}`);
1390
+ }
1391
+ const secondaryParts = [];
1392
+ if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
1393
+ if (stack.linter) secondaryParts.push(formatItem(stack.linter));
1394
+ if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
1395
+ if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
1396
+ if (secondaryParts.length > 0) {
1397
+ lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
1398
+ }
1399
+ if (stack.libraries.length > 0) {
1400
+ for (const lib of stack.libraries) {
1401
+ lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES)}`);
1402
+ }
1403
+ }
1404
+ const groups = groupByRole(scanResult.structure.directories);
1405
+ if (groups.length > 0) {
1406
+ lines.push("");
1407
+ lines.push("Structure:");
1408
+ for (const group of groups) {
1409
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1410
+ }
1411
+ }
1412
+ lines.push(...formatConventionsText(scanResult));
1413
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1414
+ lines.push("");
1415
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1416
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1417
+ if (ext) {
1418
+ lines.push(ext);
1419
+ }
1420
+ lines.push(...formatRulesText(config));
1421
+ return lines.join("\n");
1422
+ }
952
1423
 
953
1424
  // src/utils/write-generated-files.ts
954
1425
  import * as fs10 from "fs";
@@ -979,60 +1450,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
979
1450
  // src/commands/init-hooks.ts
980
1451
  import * as fs11 from "fs";
981
1452
  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
- }
1453
+ import chalk7 from "chalk";
1025
1454
  function setupPreCommitHook(projectRoot) {
1026
1455
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1027
1456
  if (fs11.existsSync(lefthookPath)) {
1028
1457
  addLefthookPreCommit(lefthookPath);
1029
- console.log(` ${chalk5.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1458
+ console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1030
1459
  return;
1031
1460
  }
1032
1461
  const huskyDir = path12.join(projectRoot, ".husky");
1033
1462
  if (fs11.existsSync(huskyDir)) {
1034
1463
  writeHuskyPreCommit(huskyDir);
1035
- console.log(` ${chalk5.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1464
+ console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1036
1465
  return;
1037
1466
  }
1038
1467
  const gitDir = path12.join(projectRoot, ".git");
@@ -1042,7 +1471,7 @@ function setupPreCommitHook(projectRoot) {
1042
1471
  fs11.mkdirSync(hooksDir, { recursive: true });
1043
1472
  }
1044
1473
  writeGitHookPreCommit(hooksDir);
1045
- console.log(` ${chalk5.green("\u2713")} .git/hooks/pre-commit`);
1474
+ console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
1046
1475
  }
1047
1476
  }
1048
1477
  function writeGitHookPreCommit(hooksDir) {
@@ -1072,10 +1501,72 @@ npx viberails check --staged
1072
1501
  function addLefthookPreCommit(lefthookPath) {
1073
1502
  const content = fs11.readFileSync(lefthookPath, "utf-8");
1074
1503
  if (content.includes("viberails")) return;
1075
- const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
1076
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1077
- ${addition}
1504
+ const hasPreCommit = /^pre-commit:/m.test(content);
1505
+ if (hasPreCommit) {
1506
+ const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1507
+ "\n"
1508
+ );
1509
+ const updated = `${content.trimEnd()}
1510
+ ${commandBlock}
1511
+ `;
1512
+ fs11.writeFileSync(lefthookPath, updated);
1513
+ } else {
1514
+ const section = [
1515
+ "",
1516
+ "pre-commit:",
1517
+ " commands:",
1518
+ " viberails:",
1519
+ " run: npx viberails check --staged"
1520
+ ].join("\n");
1521
+ fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1522
+ ${section}
1078
1523
  `);
1524
+ }
1525
+ }
1526
+ function detectHookManager(projectRoot) {
1527
+ if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1528
+ if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1529
+ if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1530
+ return void 0;
1531
+ }
1532
+ function setupClaudeCodeHook(projectRoot) {
1533
+ const claudeDir = path12.join(projectRoot, ".claude");
1534
+ if (!fs11.existsSync(claudeDir)) {
1535
+ fs11.mkdirSync(claudeDir, { recursive: true });
1536
+ }
1537
+ const settingsPath = path12.join(claudeDir, "settings.json");
1538
+ let settings = {};
1539
+ if (fs11.existsSync(settingsPath)) {
1540
+ try {
1541
+ settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1542
+ } catch {
1543
+ console.warn(
1544
+ ` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1545
+ );
1546
+ settings = {};
1547
+ }
1548
+ }
1549
+ const hooks = settings.hooks ?? {};
1550
+ const existing = hooks.PostToolUse ?? [];
1551
+ if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1552
+ const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1553
+ const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1554
+ hooks.PostToolUse = [
1555
+ ...existing,
1556
+ {
1557
+ matcher: "Edit|Write",
1558
+ hooks: [
1559
+ {
1560
+ type: "command",
1561
+ command: hookCommand
1562
+ }
1563
+ ]
1564
+ }
1565
+ ];
1566
+ settings.hooks = hooks;
1567
+ fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1568
+ `);
1569
+ console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1079
1570
  }
1080
1571
  function writeHuskyPreCommit(huskyDir) {
1081
1572
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1091,187 +1582,6 @@ npx viberails check --staged
1091
1582
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1092
1583
  }
1093
1584
 
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
1585
  // src/commands/init.ts
1276
1586
  var CONFIG_FILE4 = "viberails.config.json";
1277
1587
  function filterHighConfidence(conventions) {
@@ -1286,12 +1596,13 @@ function filterHighConfidence(conventions) {
1286
1596
  }
1287
1597
  return filtered;
1288
1598
  }
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;
1599
+ function getConventionStr2(cv) {
1600
+ if (!cv) return void 0;
1601
+ return typeof cv === "string" ? cv : cv.value;
1602
+ }
1603
+ function hasConventionOverrides(config) {
1604
+ if (!config.packages || config.packages.length === 0) return false;
1605
+ return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1295
1606
  }
1296
1607
  async function initCommand(options, cwd) {
1297
1608
  const startDir = cwd ?? process.cwd();
@@ -1304,69 +1615,137 @@ async function initCommand(options, cwd) {
1304
1615
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1305
1616
  if (fs12.existsSync(configPath)) {
1306
1617
  console.log(
1307
- chalk9.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk9.cyan("viberails sync") + " to update the generated files."
1618
+ `${chalk8.yellow("!")} viberails is already initialized.
1619
+ Run ${chalk8.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
1308
1620
  );
1309
1621
  return;
1310
1622
  }
1311
- p2.intro("viberails");
1312
- const s = p2.spinner();
1623
+ if (options.yes) {
1624
+ console.log(chalk8.dim("Scanning project..."));
1625
+ const scanResult2 = await scan(projectRoot);
1626
+ const config2 = generateConfig(scanResult2);
1627
+ config2.conventions = filterHighConfidence(config2.conventions);
1628
+ displayScanResults(scanResult2);
1629
+ displayRulesPreview(config2);
1630
+ if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
1631
+ console.log(chalk8.dim("Building import graph..."));
1632
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1633
+ const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
1634
+ const graph = await buildImportGraph(projectRoot, {
1635
+ packages,
1636
+ ignore: config2.ignore
1637
+ });
1638
+ const inferred = inferBoundaries(graph);
1639
+ if (inferred.length > 0) {
1640
+ config2.boundaries = inferred;
1641
+ config2.rules.enforceBoundaries = true;
1642
+ console.log(` Inferred ${inferred.length} boundary rules`);
1643
+ }
1644
+ }
1645
+ fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1646
+ `);
1647
+ writeGeneratedFiles(projectRoot, config2, scanResult2);
1648
+ updateGitignore(projectRoot);
1649
+ console.log(`
1650
+ Created:`);
1651
+ console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
1652
+ console.log(` ${chalk8.green("\u2713")} .viberails/context.md`);
1653
+ console.log(` ${chalk8.green("\u2713")} .viberails/scan-result.json`);
1654
+ return;
1655
+ }
1656
+ clack2.intro("viberails");
1657
+ const s = clack2.spinner();
1313
1658
  s.start("Scanning project...");
1314
1659
  const scanResult = await scan(projectRoot);
1660
+ const config = generateConfig(scanResult);
1315
1661
  s.stop("Scan complete");
1316
1662
  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.`
1663
+ clack2.log.warn(
1664
+ "No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
1320
1665
  );
1321
1666
  }
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;
1667
+ const resultsText = formatScanResultsText(scanResult, config);
1668
+ clack2.note(resultsText, "Scan results");
1669
+ const decision = await promptInitDecision();
1670
+ if (decision === "customize") {
1671
+ clack2.note(
1672
+ "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1673
+ "Rules"
1674
+ );
1675
+ const overrides = await promptRuleCustomization({
1676
+ maxFileLines: config.rules.maxFileLines,
1677
+ requireTests: config.rules.requireTests,
1678
+ enforceNaming: config.rules.enforceNaming,
1679
+ enforcement: config.enforcement,
1680
+ fileNamingValue: getConventionStr2(config.conventions.fileNaming)
1681
+ });
1682
+ config.rules.maxFileLines = overrides.maxFileLines;
1683
+ config.rules.requireTests = overrides.requireTests;
1684
+ config.rules.enforceNaming = overrides.enforceNaming;
1685
+ config.enforcement = overrides.enforcement;
1686
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1687
+ clack2.note(
1688
+ 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1689
+ "Per-package overrides"
1690
+ );
1691
+ }
1329
1692
  }
1330
- const config = generateConfig(scanResult);
1331
- if (options.yes) {
1332
- config.conventions = filterHighConfidence(config.conventions);
1333
- }
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;
1693
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1694
+ clack2.note(
1695
+ "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
1696
+ "Boundaries"
1697
+ );
1698
+ const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
1699
+ if (shouldInfer) {
1700
+ const bs = clack2.spinner();
1701
+ bs.start("Building import graph...");
1702
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1703
+ const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1704
+ const graph = await buildImportGraph(projectRoot, {
1705
+ packages,
1706
+ ignore: config.ignore
1707
+ });
1708
+ const inferred = inferBoundaries(graph);
1709
+ if (inferred.length > 0) {
1710
+ config.boundaries = inferred;
1711
+ config.rules.enforceBoundaries = true;
1712
+ bs.stop(`Inferred ${inferred.length} boundary rules`);
1713
+ } else {
1714
+ bs.stop("No boundary rules inferred");
1715
+ }
1348
1716
  }
1349
1717
  }
1718
+ const hookManager = detectHookManager(projectRoot);
1719
+ const integrations = await promptIntegrations(hookManager);
1720
+ if (hasConventionOverrides(config)) {
1721
+ clack2.note(
1722
+ "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1723
+ "Per-package conventions"
1724
+ );
1725
+ }
1350
1726
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1351
1727
  `);
1352
1728
  writeGeneratedFiles(projectRoot, config, scanResult);
1353
1729
  updateGitignore(projectRoot);
1354
- if (wizard.integration.includes("pre-commit")) {
1730
+ const createdFiles = [
1731
+ CONFIG_FILE4,
1732
+ ".viberails/context.md",
1733
+ ".viberails/scan-result.json"
1734
+ ];
1735
+ if (integrations.preCommitHook) {
1355
1736
  setupPreCommitHook(projectRoot);
1737
+ const hookMgr = detectHookManager(projectRoot);
1738
+ if (hookMgr) {
1739
+ createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1740
+ }
1356
1741
  }
1357
- if (wizard.integration.includes("claude-hook")) {
1742
+ if (integrations.claudeCodeHook) {
1358
1743
  setupClaudeCodeHook(projectRoot);
1744
+ createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1359
1745
  }
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
- );
1746
+ clack2.log.success(`Created:
1747
+ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1748
+ clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
1370
1749
  }
1371
1750
  function updateGitignore(projectRoot) {
1372
1751
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1376,8 +1755,9 @@ function updateGitignore(projectRoot) {
1376
1755
  }
1377
1756
  if (!content.includes(".viberails/scan-result.json")) {
1378
1757
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1379
- fs12.writeFileSync(gitignorePath, `${content.trimEnd()}
1380
- ${block}`);
1758
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1759
+ `;
1760
+ fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1381
1761
  }
1382
1762
  }
1383
1763
 
@@ -1386,7 +1766,7 @@ import * as fs13 from "fs";
1386
1766
  import * as path14 from "path";
1387
1767
  import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
1388
1768
  import { scan as scan2 } from "@viberails/scanner";
1389
- import chalk10 from "chalk";
1769
+ import chalk9 from "chalk";
1390
1770
  var CONFIG_FILE5 = "viberails.config.json";
1391
1771
  async function syncCommand(cwd) {
1392
1772
  const startDir = cwd ?? process.cwd();
@@ -1398,21 +1778,33 @@ async function syncCommand(cwd) {
1398
1778
  }
1399
1779
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1400
1780
  const existing = await loadConfig4(configPath);
1401
- console.log(chalk10.dim("Scanning project..."));
1781
+ console.log(chalk9.dim("Scanning project..."));
1402
1782
  const scanResult = await scan2(projectRoot);
1403
1783
  const merged = mergeConfig(existing, scanResult);
1404
- fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1784
+ const existingJson = JSON.stringify(existing, null, 2);
1785
+ const mergedJson = JSON.stringify(merged, null, 2);
1786
+ const configChanged = existingJson !== mergedJson;
1787
+ if (configChanged) {
1788
+ console.log(
1789
+ ` ${chalk9.yellow("!")} Config updated \u2014 review ${chalk9.cyan(CONFIG_FILE5)} for changes`
1790
+ );
1791
+ }
1792
+ fs13.writeFileSync(configPath, `${mergedJson}
1405
1793
  `);
1406
1794
  writeGeneratedFiles(projectRoot, merged, scanResult);
1407
1795
  console.log(`
1408
- ${chalk10.bold("Synced:")}`);
1409
- console.log(` ${chalk10.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1410
- console.log(` ${chalk10.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1411
- console.log(` ${chalk10.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1796
+ ${chalk9.bold("Synced:")}`);
1797
+ if (configChanged) {
1798
+ console.log(` ${chalk9.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1799
+ } else {
1800
+ console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1801
+ }
1802
+ console.log(` ${chalk9.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1803
+ console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1412
1804
  }
1413
1805
 
1414
1806
  // src/index.ts
1415
- var VERSION = "0.3.0";
1807
+ var VERSION = "0.3.2";
1416
1808
  var program = new Command();
1417
1809
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1418
1810
  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) => {
@@ -1420,7 +1812,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
1420
1812
  await initCommand(options);
1421
1813
  } catch (err) {
1422
1814
  const message = err instanceof Error ? err.message : String(err);
1423
- console.error(`${chalk11.red("Error:")} ${message}`);
1815
+ console.error(`${chalk10.red("Error:")} ${message}`);
1424
1816
  process.exit(1);
1425
1817
  }
1426
1818
  });
@@ -1429,21 +1821,22 @@ program.command("sync").description("Re-scan and update generated files").action
1429
1821
  await syncCommand();
1430
1822
  } catch (err) {
1431
1823
  const message = err instanceof Error ? err.message : String(err);
1432
- console.error(`${chalk11.red("Error:")} ${message}`);
1824
+ console.error(`${chalk10.red("Error:")} ${message}`);
1433
1825
  process.exit(1);
1434
1826
  }
1435
1827
  });
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(
1828
+ 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
1829
  async (options) => {
1438
1830
  try {
1439
1831
  const exitCode = await checkCommand({
1440
1832
  ...options,
1441
- noBoundaries: options.boundaries === false
1833
+ noBoundaries: options.boundaries === false,
1834
+ format: options.format === "json" ? "json" : "text"
1442
1835
  });
1443
1836
  process.exit(exitCode);
1444
1837
  } catch (err) {
1445
1838
  const message = err instanceof Error ? err.message : String(err);
1446
- console.error(`${chalk11.red("Error:")} ${message}`);
1839
+ console.error(`${chalk10.red("Error:")} ${message}`);
1447
1840
  process.exit(1);
1448
1841
  }
1449
1842
  }
@@ -1454,7 +1847,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1454
1847
  process.exit(exitCode);
1455
1848
  } catch (err) {
1456
1849
  const message = err instanceof Error ? err.message : String(err);
1457
- console.error(`${chalk11.red("Error:")} ${message}`);
1850
+ console.error(`${chalk10.red("Error:")} ${message}`);
1458
1851
  process.exit(1);
1459
1852
  }
1460
1853
  });
@@ -1463,7 +1856,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1463
1856
  await boundariesCommand(options);
1464
1857
  } catch (err) {
1465
1858
  const message = err instanceof Error ? err.message : String(err);
1466
- console.error(`${chalk11.red("Error:")} ${message}`);
1859
+ console.error(`${chalk10.red("Error:")} ${message}`);
1467
1860
  process.exit(1);
1468
1861
  }
1469
1862
  });