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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import chalk10 from "chalk";
4
+ import chalk11 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/boundaries.ts
@@ -66,7 +66,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
66
66
  ];
67
67
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
68
68
  }
69
- const packageNames = new Set(packages.map((p) => p.name));
69
+ const packageNames = new Set(packages.map((p3) => p3.name));
70
70
  for (const pkg of packages) {
71
71
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
72
72
  }
@@ -96,23 +96,37 @@ async function boundariesCommand(options, cwd) {
96
96
  }
97
97
  displayRules(config);
98
98
  }
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
+ }
99
104
  function displayRules(config) {
100
- if (!config.boundaries || config.boundaries.length === 0) {
105
+ const total = countBoundaries(config.boundaries);
106
+ if (total === 0) {
101
107
  console.log(chalk.yellow("No boundary rules configured."));
102
108
  console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
103
109
  return;
104
110
  }
105
- const allowRules = config.boundaries.filter((r) => r.allow);
106
- const denyRules = config.boundaries.filter((r) => !r.allow);
107
111
  console.log(`
108
- ${chalk.bold(`Boundary rules (${config.boundaries.length} rules):`)}
112
+ ${chalk.bold(`Boundary rules (${total} rules):`)}
109
113
  `);
110
- for (const r of allowRules) {
111
- console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
112
- }
113
- for (const r of denyRules) {
114
- const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
115
- console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
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
+ }
116
130
  }
117
131
  console.log(
118
132
  `
@@ -129,31 +143,29 @@ async function inferAndDisplay(projectRoot, config, configPath) {
129
143
  });
130
144
  console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
131
145
  const inferred = inferBoundaries(graph);
132
- if (inferred.length === 0) {
146
+ const entries = Object.entries(inferred);
147
+ if (entries.length === 0) {
133
148
  console.log(chalk.yellow("No boundary rules could be inferred."));
134
149
  return;
135
150
  }
136
- const allow = inferred.filter((r) => r.allow);
137
- const deny = inferred.filter((r) => !r.allow);
151
+ const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
138
152
  console.log(`
139
153
  ${chalk.bold("Inferred boundary rules:")}
140
154
  `);
141
- for (const r of allow) {
142
- console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
143
- }
144
- for (const r of deny) {
145
- const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
146
- console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
155
+ for (const [from, denied] of entries) {
156
+ for (const to of denied) {
157
+ console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
158
+ }
147
159
  }
148
160
  console.log(`
149
- ${allow.length} allowed, ${deny.length} denied`);
161
+ ${totalRules} deny rules`);
150
162
  const shouldSave = await confirm("\nSave to viberails.config.json?");
151
163
  if (shouldSave) {
152
164
  config.boundaries = inferred;
153
165
  config.rules.enforceBoundaries = true;
154
166
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
155
167
  `);
156
- console.log(`${chalk.green("\u2713")} Saved ${inferred.length} rules`);
168
+ console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
157
169
  }
158
170
  }
159
171
  async function showGraph(projectRoot, config) {
@@ -527,7 +539,8 @@ async function checkCommand(options, cwd) {
527
539
  const testViolations = checkMissingTests(projectRoot, config, severity);
528
540
  violations.push(...testViolations);
529
541
  }
530
- if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
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) {
531
544
  const startTime = Date.now();
532
545
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
533
546
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -932,223 +945,10 @@ async function fixCommand(options, cwd) {
932
945
  // src/commands/init.ts
933
946
  import * as fs12 from "fs";
934
947
  import * as path13 from "path";
948
+ import * as p2 from "@clack/prompts";
935
949
  import { generateConfig } from "@viberails/config";
936
950
  import { scan } from "@viberails/scanner";
937
- import chalk8 from "chalk";
938
-
939
- // src/display.ts
940
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
941
- import chalk6 from "chalk";
942
-
943
- // src/display-helpers.ts
944
- import { ROLE_DESCRIPTIONS } from "@viberails/types";
945
- function groupByRole(directories) {
946
- const map = /* @__PURE__ */ new Map();
947
- for (const dir of directories) {
948
- if (dir.role === "unknown") continue;
949
- const existing = map.get(dir.role);
950
- if (existing) {
951
- existing.dirs.push(dir);
952
- } else {
953
- map.set(dir.role, { dirs: [dir] });
954
- }
955
- }
956
- const groups = [];
957
- for (const [role, { dirs }] of map) {
958
- const label = ROLE_DESCRIPTIONS[role] ?? role;
959
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
960
- groups.push({
961
- role,
962
- label,
963
- dirCount: dirs.length,
964
- totalFiles,
965
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
966
- });
967
- }
968
- return groups;
969
- }
970
- function formatSummary(stats, packageCount) {
971
- const parts = [];
972
- if (packageCount && packageCount > 1) {
973
- parts.push(`${packageCount} packages`);
974
- }
975
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
976
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
977
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
978
- return parts.join(" \xB7 ");
979
- }
980
- function formatExtensions(filesByExtension, maxEntries = 4) {
981
- return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
982
- }
983
- function formatRoleGroup(group) {
984
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
985
- if (group.singlePath) {
986
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
987
- }
988
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
989
- return `${group.label} \u2014 ${dirs} (${files})`;
990
- }
991
-
992
- // src/display-monorepo.ts
993
- import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
994
- import chalk5 from "chalk";
995
- function formatPackageSummary(pkg) {
996
- const parts = [];
997
- if (pkg.stack.framework) {
998
- parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
999
- }
1000
- if (pkg.stack.styling) {
1001
- parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1002
- }
1003
- const files = `${pkg.statistics.totalFiles} files`;
1004
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1005
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1006
- }
1007
- function displayMonorepoResults(scanResult) {
1008
- const { stack, packages } = scanResult;
1009
- console.log(`
1010
- ${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1011
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
1012
- if (stack.packageManager) {
1013
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
1014
- }
1015
- if (stack.linter) {
1016
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
1017
- }
1018
- if (stack.formatter) {
1019
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
1020
- }
1021
- if (stack.testRunner) {
1022
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
1023
- }
1024
- console.log("");
1025
- for (const pkg of packages) {
1026
- console.log(formatPackageSummary(pkg));
1027
- }
1028
- const packagesWithDirs = packages.filter(
1029
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1030
- );
1031
- if (packagesWithDirs.length > 0) {
1032
- console.log(`
1033
- ${chalk5.bold("Structure:")}`);
1034
- for (const pkg of packagesWithDirs) {
1035
- const groups = groupByRole(pkg.structure.directories);
1036
- if (groups.length === 0) continue;
1037
- console.log(` ${pkg.relativePath}:`);
1038
- for (const group of groups) {
1039
- console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
1040
- }
1041
- }
1042
- }
1043
- displayConventions(scanResult);
1044
- displaySummarySection(scanResult);
1045
- console.log("");
1046
- }
1047
-
1048
- // src/display.ts
1049
- var CONVENTION_LABELS = {
1050
- fileNaming: "File naming",
1051
- componentNaming: "Component naming",
1052
- hookNaming: "Hook naming",
1053
- importAlias: "Import alias"
1054
- };
1055
- function formatItem(item, nameMap) {
1056
- const name = nameMap?.[item.name] ?? item.name;
1057
- return item.version ? `${name} ${item.version}` : name;
1058
- }
1059
- function confidenceLabel(convention) {
1060
- const pct = Math.round(convention.consistency);
1061
- if (convention.confidence === "high") {
1062
- return `${pct}% \u2014 high confidence, will enforce`;
1063
- }
1064
- return `${pct}% \u2014 medium confidence, suggested only`;
1065
- }
1066
- function displayConventions(scanResult) {
1067
- const conventionEntries = Object.entries(scanResult.conventions);
1068
- if (conventionEntries.length === 0) return;
1069
- console.log(`
1070
- ${chalk6.bold("Conventions:")}`);
1071
- for (const [key, convention] of conventionEntries) {
1072
- if (convention.confidence === "low") continue;
1073
- const label = CONVENTION_LABELS[key] ?? key;
1074
- if (scanResult.packages.length > 1) {
1075
- const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1076
- const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1077
- if (allSame || pkgValues.length <= 1) {
1078
- const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1079
- const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1080
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1081
- } else {
1082
- console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
1083
- for (const pv of pkgValues) {
1084
- const pct = Math.round(pv.convention.consistency);
1085
- console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1086
- }
1087
- }
1088
- } else {
1089
- const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1090
- const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1091
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1092
- }
1093
- }
1094
- }
1095
- function displaySummarySection(scanResult) {
1096
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1097
- console.log(`
1098
- ${chalk6.bold("Summary:")}`);
1099
- console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1100
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
1101
- if (ext) {
1102
- console.log(` ${ext}`);
1103
- }
1104
- }
1105
- function displayScanResults(scanResult) {
1106
- if (scanResult.packages.length > 1) {
1107
- displayMonorepoResults(scanResult);
1108
- return;
1109
- }
1110
- const { stack } = scanResult;
1111
- console.log(`
1112
- ${chalk6.bold("Detected:")}`);
1113
- if (stack.framework) {
1114
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1115
- }
1116
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
1117
- if (stack.styling) {
1118
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1119
- }
1120
- if (stack.backend) {
1121
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1122
- }
1123
- if (stack.linter) {
1124
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
1125
- }
1126
- if (stack.formatter) {
1127
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
1128
- }
1129
- if (stack.testRunner) {
1130
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
1131
- }
1132
- if (stack.packageManager) {
1133
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
1134
- }
1135
- if (stack.libraries.length > 0) {
1136
- for (const lib of stack.libraries) {
1137
- console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1138
- }
1139
- }
1140
- const groups = groupByRole(scanResult.structure.directories);
1141
- if (groups.length > 0) {
1142
- console.log(`
1143
- ${chalk6.bold("Structure:")}`);
1144
- for (const group of groups) {
1145
- console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
1146
- }
1147
- }
1148
- displayConventions(scanResult);
1149
- displaySummarySection(scanResult);
1150
- console.log("");
1151
- }
951
+ import chalk9 from "chalk";
1152
952
 
1153
953
  // src/utils/write-generated-files.ts
1154
954
  import * as fs10 from "fs";
@@ -1179,18 +979,60 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1179
979
  // src/commands/init-hooks.ts
1180
980
  import * as fs11 from "fs";
1181
981
  import * as path12 from "path";
1182
- import chalk7 from "chalk";
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
+ }
1183
1025
  function setupPreCommitHook(projectRoot) {
1184
1026
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1185
1027
  if (fs11.existsSync(lefthookPath)) {
1186
1028
  addLefthookPreCommit(lefthookPath);
1187
- console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1029
+ console.log(` ${chalk5.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1188
1030
  return;
1189
1031
  }
1190
1032
  const huskyDir = path12.join(projectRoot, ".husky");
1191
1033
  if (fs11.existsSync(huskyDir)) {
1192
1034
  writeHuskyPreCommit(huskyDir);
1193
- console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1035
+ console.log(` ${chalk5.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1194
1036
  return;
1195
1037
  }
1196
1038
  const gitDir = path12.join(projectRoot, ".git");
@@ -1200,7 +1042,7 @@ function setupPreCommitHook(projectRoot) {
1200
1042
  fs11.mkdirSync(hooksDir, { recursive: true });
1201
1043
  }
1202
1044
  writeGitHookPreCommit(hooksDir);
1203
- console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
1045
+ console.log(` ${chalk5.green("\u2713")} .git/hooks/pre-commit`);
1204
1046
  }
1205
1047
  }
1206
1048
  function writeGitHookPreCommit(hooksDir) {
@@ -1249,6 +1091,187 @@ npx viberails check --staged
1249
1091
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1250
1092
  }
1251
1093
 
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
+
1252
1275
  // src/commands/init.ts
1253
1276
  var CONFIG_FILE4 = "viberails.config.json";
1254
1277
  function filterHighConfidence(conventions) {
@@ -1263,6 +1286,13 @@ function filterHighConfidence(conventions) {
1263
1286
  }
1264
1287
  return filtered;
1265
1288
  }
1289
+ function applyWizardResult(config, wizard) {
1290
+ config.enforcement = wizard.enforcement;
1291
+ if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
1292
+ config.rules.enforceNaming = wizard.checks.naming;
1293
+ config.rules.requireTests = wizard.checks.tests;
1294
+ config.rules.enforceBoundaries = wizard.checks.boundaries;
1295
+ }
1266
1296
  async function initCommand(options, cwd) {
1267
1297
  const startDir = cwd ?? process.cwd();
1268
1298
  const projectRoot = findProjectRoot(startDir);
@@ -1274,64 +1304,69 @@ async function initCommand(options, cwd) {
1274
1304
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1275
1305
  if (fs12.existsSync(configPath)) {
1276
1306
  console.log(
1277
- chalk8.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk8.cyan("viberails sync") + " to update the generated files."
1307
+ chalk9.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk9.cyan("viberails sync") + " to update the generated files."
1278
1308
  );
1279
1309
  return;
1280
1310
  }
1281
- console.log(chalk8.dim("Scanning project..."));
1311
+ p2.intro("viberails");
1312
+ const s = p2.spinner();
1313
+ s.start("Scanning project...");
1282
1314
  const scanResult = await scan(projectRoot);
1283
- displayScanResults(scanResult);
1315
+ s.stop("Scan complete");
1284
1316
  if (scanResult.statistics.totalFiles === 0) {
1285
- console.log(
1286
- chalk8.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + chalk8.cyan("viberails sync") + " after adding source files.\n"
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.`
1287
1320
  );
1288
1321
  }
1289
- if (!options.yes) {
1290
- const accepted = await confirm("Does this look right?");
1291
- if (!accepted) {
1292
- console.log("Aborted.");
1293
- return;
1294
- }
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;
1295
1329
  }
1296
1330
  const config = generateConfig(scanResult);
1297
1331
  if (options.yes) {
1298
1332
  config.conventions = filterHighConfidence(config.conventions);
1299
1333
  }
1300
- if (config.workspace && config.workspace.packages.length > 0) {
1301
- let shouldInfer = options.yes;
1302
- if (!options.yes) {
1303
- shouldInfer = await confirm("Infer boundary rules from import patterns?");
1304
- }
1305
- if (shouldInfer) {
1306
- console.log(chalk8.dim("Building import graph..."));
1307
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1308
- const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1309
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1310
- const inferred = inferBoundaries(graph);
1311
- if (inferred.length > 0) {
1312
- config.boundaries = inferred;
1313
- config.rules.enforceBoundaries = true;
1314
- console.log(` ${chalk8.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1315
- }
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;
1316
1348
  }
1317
1349
  }
1318
1350
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1319
1351
  `);
1320
1352
  writeGeneratedFiles(projectRoot, config, scanResult);
1321
1353
  updateGitignore(projectRoot);
1322
- setupPreCommitHook(projectRoot);
1323
- console.log(`
1324
- ${chalk8.bold("Created:")}`);
1325
- console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
1326
- console.log(` ${chalk8.green("\u2713")} .viberails/context.md`);
1327
- console.log(` ${chalk8.green("\u2713")} .viberails/scan-result.json`);
1328
- console.log(`
1329
- ${chalk8.bold("Next steps:")}`);
1330
- console.log(` 1. Review ${chalk8.cyan("viberails.config.json")} and adjust rules`);
1331
- console.log(
1332
- ` 2. Commit ${chalk8.cyan("viberails.config.json")} and ${chalk8.cyan(".viberails/context.md")}`
1354
+ if (wizard.integration.includes("pre-commit")) {
1355
+ setupPreCommitHook(projectRoot);
1356
+ }
1357
+ if (wizard.integration.includes("claude-hook")) {
1358
+ setupClaudeCodeHook(projectRoot);
1359
+ }
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`
1333
1369
  );
1334
- console.log(` 3. Run ${chalk8.cyan("viberails check")} to verify your project passes`);
1335
1370
  }
1336
1371
  function updateGitignore(projectRoot) {
1337
1372
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1351,7 +1386,7 @@ import * as fs13 from "fs";
1351
1386
  import * as path14 from "path";
1352
1387
  import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
1353
1388
  import { scan as scan2 } from "@viberails/scanner";
1354
- import chalk9 from "chalk";
1389
+ import chalk10 from "chalk";
1355
1390
  var CONFIG_FILE5 = "viberails.config.json";
1356
1391
  async function syncCommand(cwd) {
1357
1392
  const startDir = cwd ?? process.cwd();
@@ -1363,21 +1398,21 @@ async function syncCommand(cwd) {
1363
1398
  }
1364
1399
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1365
1400
  const existing = await loadConfig4(configPath);
1366
- console.log(chalk9.dim("Scanning project..."));
1401
+ console.log(chalk10.dim("Scanning project..."));
1367
1402
  const scanResult = await scan2(projectRoot);
1368
1403
  const merged = mergeConfig(existing, scanResult);
1369
1404
  fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1370
1405
  `);
1371
1406
  writeGeneratedFiles(projectRoot, merged, scanResult);
1372
1407
  console.log(`
1373
- ${chalk9.bold("Synced:")}`);
1374
- console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1375
- console.log(` ${chalk9.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1376
- console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
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`);
1377
1412
  }
1378
1413
 
1379
1414
  // src/index.ts
1380
- var VERSION = "0.2.3";
1415
+ var VERSION = "0.3.0";
1381
1416
  var program = new Command();
1382
1417
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1383
1418
  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) => {
@@ -1385,7 +1420,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
1385
1420
  await initCommand(options);
1386
1421
  } catch (err) {
1387
1422
  const message = err instanceof Error ? err.message : String(err);
1388
- console.error(`${chalk10.red("Error:")} ${message}`);
1423
+ console.error(`${chalk11.red("Error:")} ${message}`);
1389
1424
  process.exit(1);
1390
1425
  }
1391
1426
  });
@@ -1394,7 +1429,7 @@ program.command("sync").description("Re-scan and update generated files").action
1394
1429
  await syncCommand();
1395
1430
  } catch (err) {
1396
1431
  const message = err instanceof Error ? err.message : String(err);
1397
- console.error(`${chalk10.red("Error:")} ${message}`);
1432
+ console.error(`${chalk11.red("Error:")} ${message}`);
1398
1433
  process.exit(1);
1399
1434
  }
1400
1435
  });
@@ -1408,7 +1443,7 @@ program.command("check").description("Check files against enforced rules").optio
1408
1443
  process.exit(exitCode);
1409
1444
  } catch (err) {
1410
1445
  const message = err instanceof Error ? err.message : String(err);
1411
- console.error(`${chalk10.red("Error:")} ${message}`);
1446
+ console.error(`${chalk11.red("Error:")} ${message}`);
1412
1447
  process.exit(1);
1413
1448
  }
1414
1449
  }
@@ -1419,7 +1454,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1419
1454
  process.exit(exitCode);
1420
1455
  } catch (err) {
1421
1456
  const message = err instanceof Error ? err.message : String(err);
1422
- console.error(`${chalk10.red("Error:")} ${message}`);
1457
+ console.error(`${chalk11.red("Error:")} ${message}`);
1423
1458
  process.exit(1);
1424
1459
  }
1425
1460
  });
@@ -1428,7 +1463,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1428
1463
  await boundariesCommand(options);
1429
1464
  } catch (err) {
1430
1465
  const message = err instanceof Error ? err.message : String(err);
1431
- console.error(`${chalk10.red("Error:")} ${message}`);
1466
+ console.error(`${chalk11.red("Error:")} ${message}`);
1432
1467
  process.exit(1);
1433
1468
  }
1434
1469
  });