viberails 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,7 +34,7 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk10 = __toESM(require("chalk"), 1);
37
+ var import_chalk11 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -99,7 +99,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
99
99
  ];
100
100
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
101
101
  }
102
- const packageNames = new Set(packages.map((p) => p.name));
102
+ const packageNames = new Set(packages.map((p3) => p3.name));
103
103
  for (const pkg of packages) {
104
104
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
105
105
  }
@@ -129,23 +129,37 @@ async function boundariesCommand(options, cwd) {
129
129
  }
130
130
  displayRules(config);
131
131
  }
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
+ }
132
137
  function displayRules(config) {
133
- if (!config.boundaries || config.boundaries.length === 0) {
138
+ const total = countBoundaries(config.boundaries);
139
+ if (total === 0) {
134
140
  console.log(import_chalk.default.yellow("No boundary rules configured."));
135
141
  console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
136
142
  return;
137
143
  }
138
- const allowRules = config.boundaries.filter((r) => r.allow);
139
- const denyRules = config.boundaries.filter((r) => !r.allow);
140
144
  console.log(`
141
- ${import_chalk.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
145
+ ${import_chalk.default.bold(`Boundary rules (${total} rules):`)}
142
146
  `);
143
- for (const r of allowRules) {
144
- console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
145
- }
146
- for (const r of denyRules) {
147
- const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
148
- console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
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
+ }
149
163
  }
150
164
  console.log(
151
165
  `
@@ -162,31 +176,29 @@ async function inferAndDisplay(projectRoot, config, configPath) {
162
176
  });
163
177
  console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
164
178
  const inferred = inferBoundaries(graph);
165
- if (inferred.length === 0) {
179
+ const entries = Object.entries(inferred);
180
+ if (entries.length === 0) {
166
181
  console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
167
182
  return;
168
183
  }
169
- const allow = inferred.filter((r) => r.allow);
170
- const deny = inferred.filter((r) => !r.allow);
184
+ const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
171
185
  console.log(`
172
186
  ${import_chalk.default.bold("Inferred boundary rules:")}
173
187
  `);
174
- for (const r of allow) {
175
- console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
176
- }
177
- for (const r of deny) {
178
- const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
179
- console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
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
+ }
180
192
  }
181
193
  console.log(`
182
- ${allow.length} allowed, ${deny.length} denied`);
194
+ ${totalRules} deny rules`);
183
195
  const shouldSave = await confirm("\nSave to viberails.config.json?");
184
196
  if (shouldSave) {
185
197
  config.boundaries = inferred;
186
198
  config.rules.enforceBoundaries = true;
187
199
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
188
200
  `);
189
- console.log(`${import_chalk.default.green("\u2713")} Saved ${inferred.length} rules`);
201
+ console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
190
202
  }
191
203
  }
192
204
  async function showGraph(projectRoot, config) {
@@ -560,7 +572,8 @@ async function checkCommand(options, cwd) {
560
572
  const testViolations = checkMissingTests(projectRoot, config, severity);
561
573
  violations.push(...testViolations);
562
574
  }
563
- if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
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) {
564
577
  const startTime = Date.now();
565
578
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
566
579
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -965,223 +978,10 @@ async function fixCommand(options, cwd) {
965
978
  // src/commands/init.ts
966
979
  var fs12 = __toESM(require("fs"), 1);
967
980
  var path13 = __toESM(require("path"), 1);
981
+ var p2 = __toESM(require("@clack/prompts"), 1);
968
982
  var import_config4 = require("@viberails/config");
969
983
  var import_scanner = require("@viberails/scanner");
970
- var import_chalk8 = __toESM(require("chalk"), 1);
971
-
972
- // src/display.ts
973
- var import_types3 = require("@viberails/types");
974
- var import_chalk6 = __toESM(require("chalk"), 1);
975
-
976
- // src/display-helpers.ts
977
- var import_types = require("@viberails/types");
978
- function groupByRole(directories) {
979
- const map = /* @__PURE__ */ new Map();
980
- for (const dir of directories) {
981
- if (dir.role === "unknown") continue;
982
- const existing = map.get(dir.role);
983
- if (existing) {
984
- existing.dirs.push(dir);
985
- } else {
986
- map.set(dir.role, { dirs: [dir] });
987
- }
988
- }
989
- const groups = [];
990
- for (const [role, { dirs }] of map) {
991
- const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
992
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
993
- groups.push({
994
- role,
995
- label,
996
- dirCount: dirs.length,
997
- totalFiles,
998
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
999
- });
1000
- }
1001
- return groups;
1002
- }
1003
- function formatSummary(stats, packageCount) {
1004
- const parts = [];
1005
- if (packageCount && packageCount > 1) {
1006
- parts.push(`${packageCount} packages`);
1007
- }
1008
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1009
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1010
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1011
- return parts.join(" \xB7 ");
1012
- }
1013
- function formatExtensions(filesByExtension, maxEntries = 4) {
1014
- return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1015
- }
1016
- function formatRoleGroup(group) {
1017
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1018
- if (group.singlePath) {
1019
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1020
- }
1021
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1022
- return `${group.label} \u2014 ${dirs} (${files})`;
1023
- }
1024
-
1025
- // src/display-monorepo.ts
1026
- var import_types2 = require("@viberails/types");
1027
- var import_chalk5 = __toESM(require("chalk"), 1);
1028
- function formatPackageSummary(pkg) {
1029
- const parts = [];
1030
- if (pkg.stack.framework) {
1031
- parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1032
- }
1033
- if (pkg.stack.styling) {
1034
- parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1035
- }
1036
- const files = `${pkg.statistics.totalFiles} files`;
1037
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1038
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1039
- }
1040
- function displayMonorepoResults(scanResult) {
1041
- const { stack, packages } = scanResult;
1042
- console.log(`
1043
- ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1044
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
1045
- if (stack.packageManager) {
1046
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1047
- }
1048
- if (stack.linter) {
1049
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
1050
- }
1051
- if (stack.formatter) {
1052
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1053
- }
1054
- if (stack.testRunner) {
1055
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1056
- }
1057
- console.log("");
1058
- for (const pkg of packages) {
1059
- console.log(formatPackageSummary(pkg));
1060
- }
1061
- const packagesWithDirs = packages.filter(
1062
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1063
- );
1064
- if (packagesWithDirs.length > 0) {
1065
- console.log(`
1066
- ${import_chalk5.default.bold("Structure:")}`);
1067
- for (const pkg of packagesWithDirs) {
1068
- const groups = groupByRole(pkg.structure.directories);
1069
- if (groups.length === 0) continue;
1070
- console.log(` ${pkg.relativePath}:`);
1071
- for (const group of groups) {
1072
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
1073
- }
1074
- }
1075
- }
1076
- displayConventions(scanResult);
1077
- displaySummarySection(scanResult);
1078
- console.log("");
1079
- }
1080
-
1081
- // src/display.ts
1082
- var CONVENTION_LABELS = {
1083
- fileNaming: "File naming",
1084
- componentNaming: "Component naming",
1085
- hookNaming: "Hook naming",
1086
- importAlias: "Import alias"
1087
- };
1088
- function formatItem(item, nameMap) {
1089
- const name = nameMap?.[item.name] ?? item.name;
1090
- return item.version ? `${name} ${item.version}` : name;
1091
- }
1092
- function confidenceLabel(convention) {
1093
- const pct = Math.round(convention.consistency);
1094
- if (convention.confidence === "high") {
1095
- return `${pct}% \u2014 high confidence, will enforce`;
1096
- }
1097
- return `${pct}% \u2014 medium confidence, suggested only`;
1098
- }
1099
- function displayConventions(scanResult) {
1100
- const conventionEntries = Object.entries(scanResult.conventions);
1101
- if (conventionEntries.length === 0) return;
1102
- console.log(`
1103
- ${import_chalk6.default.bold("Conventions:")}`);
1104
- for (const [key, convention] of conventionEntries) {
1105
- if (convention.confidence === "low") continue;
1106
- const label = CONVENTION_LABELS[key] ?? key;
1107
- if (scanResult.packages.length > 1) {
1108
- const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1109
- const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1110
- if (allSame || pkgValues.length <= 1) {
1111
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1112
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1113
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1114
- } else {
1115
- console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
1116
- for (const pv of pkgValues) {
1117
- const pct = Math.round(pv.convention.consistency);
1118
- console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1119
- }
1120
- }
1121
- } else {
1122
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1123
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1124
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1125
- }
1126
- }
1127
- }
1128
- function displaySummarySection(scanResult) {
1129
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1130
- console.log(`
1131
- ${import_chalk6.default.bold("Summary:")}`);
1132
- console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1133
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
1134
- if (ext) {
1135
- console.log(` ${ext}`);
1136
- }
1137
- }
1138
- function displayScanResults(scanResult) {
1139
- if (scanResult.packages.length > 1) {
1140
- displayMonorepoResults(scanResult);
1141
- return;
1142
- }
1143
- const { stack } = scanResult;
1144
- console.log(`
1145
- ${import_chalk6.default.bold("Detected:")}`);
1146
- if (stack.framework) {
1147
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1148
- }
1149
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1150
- if (stack.styling) {
1151
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1152
- }
1153
- if (stack.backend) {
1154
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1155
- }
1156
- if (stack.linter) {
1157
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1158
- }
1159
- if (stack.formatter) {
1160
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1161
- }
1162
- if (stack.testRunner) {
1163
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1164
- }
1165
- if (stack.packageManager) {
1166
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1167
- }
1168
- if (stack.libraries.length > 0) {
1169
- for (const lib of stack.libraries) {
1170
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1171
- }
1172
- }
1173
- const groups = groupByRole(scanResult.structure.directories);
1174
- if (groups.length > 0) {
1175
- console.log(`
1176
- ${import_chalk6.default.bold("Structure:")}`);
1177
- for (const group of groups) {
1178
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1179
- }
1180
- }
1181
- displayConventions(scanResult);
1182
- displaySummarySection(scanResult);
1183
- console.log("");
1184
- }
984
+ var import_chalk9 = __toESM(require("chalk"), 1);
1185
985
 
1186
986
  // src/utils/write-generated-files.ts
1187
987
  var fs10 = __toESM(require("fs"), 1);
@@ -1212,18 +1012,60 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1212
1012
  // src/commands/init-hooks.ts
1213
1013
  var fs11 = __toESM(require("fs"), 1);
1214
1014
  var path12 = __toESM(require("path"), 1);
1215
- var import_chalk7 = __toESM(require("chalk"), 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
+ }
1216
1058
  function setupPreCommitHook(projectRoot) {
1217
1059
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1218
1060
  if (fs11.existsSync(lefthookPath)) {
1219
1061
  addLefthookPreCommit(lefthookPath);
1220
- console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1062
+ console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1221
1063
  return;
1222
1064
  }
1223
1065
  const huskyDir = path12.join(projectRoot, ".husky");
1224
1066
  if (fs11.existsSync(huskyDir)) {
1225
1067
  writeHuskyPreCommit(huskyDir);
1226
- console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1068
+ console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1227
1069
  return;
1228
1070
  }
1229
1071
  const gitDir = path12.join(projectRoot, ".git");
@@ -1233,7 +1075,7 @@ function setupPreCommitHook(projectRoot) {
1233
1075
  fs11.mkdirSync(hooksDir, { recursive: true });
1234
1076
  }
1235
1077
  writeGitHookPreCommit(hooksDir);
1236
- console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
1078
+ console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
1237
1079
  }
1238
1080
  }
1239
1081
  function writeGitHookPreCommit(hooksDir) {
@@ -1282,6 +1124,187 @@ npx viberails check --staged
1282
1124
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1283
1125
  }
1284
1126
 
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
+
1285
1308
  // src/commands/init.ts
1286
1309
  var CONFIG_FILE4 = "viberails.config.json";
1287
1310
  function filterHighConfidence(conventions) {
@@ -1296,6 +1319,13 @@ function filterHighConfidence(conventions) {
1296
1319
  }
1297
1320
  return filtered;
1298
1321
  }
1322
+ function applyWizardResult(config, wizard) {
1323
+ config.enforcement = wizard.enforcement;
1324
+ if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
1325
+ config.rules.enforceNaming = wizard.checks.naming;
1326
+ config.rules.requireTests = wizard.checks.tests;
1327
+ config.rules.enforceBoundaries = wizard.checks.boundaries;
1328
+ }
1299
1329
  async function initCommand(options, cwd) {
1300
1330
  const startDir = cwd ?? process.cwd();
1301
1331
  const projectRoot = findProjectRoot(startDir);
@@ -1307,64 +1337,69 @@ async function initCommand(options, cwd) {
1307
1337
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1308
1338
  if (fs12.existsSync(configPath)) {
1309
1339
  console.log(
1310
- import_chalk8.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk8.default.cyan("viberails sync") + " to update the generated files."
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."
1311
1341
  );
1312
1342
  return;
1313
1343
  }
1314
- console.log(import_chalk8.default.dim("Scanning project..."));
1344
+ p2.intro("viberails");
1345
+ const s = p2.spinner();
1346
+ s.start("Scanning project...");
1315
1347
  const scanResult = await (0, import_scanner.scan)(projectRoot);
1316
- displayScanResults(scanResult);
1348
+ s.stop("Scan complete");
1317
1349
  if (scanResult.statistics.totalFiles === 0) {
1318
- console.log(
1319
- import_chalk8.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk8.default.cyan("viberails sync") + " after adding source files.\n"
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.`
1320
1353
  );
1321
1354
  }
1322
- if (!options.yes) {
1323
- const accepted = await confirm("Does this look right?");
1324
- if (!accepted) {
1325
- console.log("Aborted.");
1326
- return;
1327
- }
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;
1328
1362
  }
1329
1363
  const config = (0, import_config4.generateConfig)(scanResult);
1330
1364
  if (options.yes) {
1331
1365
  config.conventions = filterHighConfidence(config.conventions);
1332
1366
  }
1333
- if (config.workspace && config.workspace.packages.length > 0) {
1334
- let shouldInfer = options.yes;
1335
- if (!options.yes) {
1336
- shouldInfer = await confirm("Infer boundary rules from import patterns?");
1337
- }
1338
- if (shouldInfer) {
1339
- console.log(import_chalk8.default.dim("Building import graph..."));
1340
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1341
- const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1342
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1343
- const inferred = inferBoundaries(graph);
1344
- if (inferred.length > 0) {
1345
- config.boundaries = inferred;
1346
- config.rules.enforceBoundaries = true;
1347
- console.log(` ${import_chalk8.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1348
- }
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;
1349
1381
  }
1350
1382
  }
1351
1383
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1352
1384
  `);
1353
1385
  writeGeneratedFiles(projectRoot, config, scanResult);
1354
1386
  updateGitignore(projectRoot);
1355
- setupPreCommitHook(projectRoot);
1356
- console.log(`
1357
- ${import_chalk8.default.bold("Created:")}`);
1358
- console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
1359
- console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
1360
- console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
1361
- console.log(`
1362
- ${import_chalk8.default.bold("Next steps:")}`);
1363
- console.log(` 1. Review ${import_chalk8.default.cyan("viberails.config.json")} and adjust rules`);
1364
- console.log(
1365
- ` 2. Commit ${import_chalk8.default.cyan("viberails.config.json")} and ${import_chalk8.default.cyan(".viberails/context.md")}`
1387
+ if (wizard.integration.includes("pre-commit")) {
1388
+ setupPreCommitHook(projectRoot);
1389
+ }
1390
+ if (wizard.integration.includes("claude-hook")) {
1391
+ setupClaudeCodeHook(projectRoot);
1392
+ }
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`
1366
1402
  );
1367
- console.log(` 3. Run ${import_chalk8.default.cyan("viberails check")} to verify your project passes`);
1368
1403
  }
1369
1404
  function updateGitignore(projectRoot) {
1370
1405
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1384,7 +1419,7 @@ var fs13 = __toESM(require("fs"), 1);
1384
1419
  var path14 = __toESM(require("path"), 1);
1385
1420
  var import_config5 = require("@viberails/config");
1386
1421
  var import_scanner2 = require("@viberails/scanner");
1387
- var import_chalk9 = __toESM(require("chalk"), 1);
1422
+ var import_chalk10 = __toESM(require("chalk"), 1);
1388
1423
  var CONFIG_FILE5 = "viberails.config.json";
1389
1424
  async function syncCommand(cwd) {
1390
1425
  const startDir = cwd ?? process.cwd();
@@ -1396,21 +1431,21 @@ async function syncCommand(cwd) {
1396
1431
  }
1397
1432
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1398
1433
  const existing = await (0, import_config5.loadConfig)(configPath);
1399
- console.log(import_chalk9.default.dim("Scanning project..."));
1434
+ console.log(import_chalk10.default.dim("Scanning project..."));
1400
1435
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
1401
1436
  const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1402
1437
  fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1403
1438
  `);
1404
1439
  writeGeneratedFiles(projectRoot, merged, scanResult);
1405
1440
  console.log(`
1406
- ${import_chalk9.default.bold("Synced:")}`);
1407
- console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1408
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1409
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
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`);
1410
1445
  }
1411
1446
 
1412
1447
  // src/index.ts
1413
- var VERSION = "0.2.3";
1448
+ var VERSION = "0.3.0";
1414
1449
  var program = new import_commander.Command();
1415
1450
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1416
1451
  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) => {
@@ -1418,7 +1453,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
1418
1453
  await initCommand(options);
1419
1454
  } catch (err) {
1420
1455
  const message = err instanceof Error ? err.message : String(err);
1421
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1456
+ console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1422
1457
  process.exit(1);
1423
1458
  }
1424
1459
  });
@@ -1427,7 +1462,7 @@ program.command("sync").description("Re-scan and update generated files").action
1427
1462
  await syncCommand();
1428
1463
  } catch (err) {
1429
1464
  const message = err instanceof Error ? err.message : String(err);
1430
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1465
+ console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1431
1466
  process.exit(1);
1432
1467
  }
1433
1468
  });
@@ -1441,7 +1476,7 @@ program.command("check").description("Check files against enforced rules").optio
1441
1476
  process.exit(exitCode);
1442
1477
  } catch (err) {
1443
1478
  const message = err instanceof Error ? err.message : String(err);
1444
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1479
+ console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1445
1480
  process.exit(1);
1446
1481
  }
1447
1482
  }
@@ -1452,7 +1487,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1452
1487
  process.exit(exitCode);
1453
1488
  } catch (err) {
1454
1489
  const message = err instanceof Error ? err.message : String(err);
1455
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1490
+ console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1456
1491
  process.exit(1);
1457
1492
  }
1458
1493
  });
@@ -1461,7 +1496,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1461
1496
  await boundariesCommand(options);
1462
1497
  } catch (err) {
1463
1498
  const message = err instanceof Error ? err.message : String(err);
1464
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1499
+ console.error(`${import_chalk11.default.red("Error:")} ${message}`);
1465
1500
  process.exit(1);
1466
1501
  }
1467
1502
  });