viberails 0.3.1 → 0.3.3

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,14 +1,14 @@
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
8
8
  import * as fs3 from "fs";
9
9
  import * as path3 from "path";
10
10
  import { loadConfig } from "@viberails/config";
11
- import chalk2 from "chalk";
11
+ import chalk from "chalk";
12
12
 
13
13
  // src/utils/find-project-root.ts
14
14
  import * as fs from "fs";
@@ -28,43 +28,110 @@ function findProjectRoot(startDir) {
28
28
  }
29
29
 
30
30
  // src/utils/prompt.ts
31
- import * as readline from "readline";
32
- import chalk from "chalk";
33
- async function confirm(message) {
34
- const rl = readline.createInterface({
35
- input: process.stdin,
36
- output: process.stdout
37
- });
38
- return new Promise((resolve4) => {
39
- rl.question(`${message} (Y/n) `, (answer) => {
40
- rl.close();
41
- const trimmed = answer.trim().toLowerCase();
42
- resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
43
- });
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
+ ]
44
55
  });
56
+ assertNotCancelled(result);
57
+ return result;
45
58
  }
46
- async function selectIntegrations(hookManager) {
47
- const result = { preCommitHook: true, claudeCodeHook: true };
48
- const hookLabel = hookManager ? `Pre-commit hook (detected: ${hookManager})` : "Pre-commit hook (git hook)";
49
- console.log("Set up integrations:");
50
- const rl = readline.createInterface({
51
- input: process.stdin,
52
- output: process.stdout
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
+ }
53
68
  });
54
- const askYn = (label, defaultYes) => new Promise((resolve4) => {
55
- const hint = defaultYes ? "Y/n" : "y/N";
56
- rl.question(` ${label}? (${hint}) `, (answer) => {
57
- const trimmed = answer.trim().toLowerCase();
58
- if (trimmed === "") resolve4(defaultYes);
59
- else resolve4(trimmed === "y" || trimmed === "yes");
60
- });
69
+ assertNotCancelled(maxFileLinesResult);
70
+ const requireTestsResult = await clack.confirm({
71
+ message: "Require matching test files for source files?",
72
+ initialValue: defaults.requireTests
61
73
  });
62
- console.log(` ${chalk.dim("Runs viberails check automatically when you commit")}`);
63
- result.preCommitHook = await askYn(hookLabel, true);
64
- console.log(` ${chalk.dim("Checks files against your rules when Claude edits them")}`);
65
- result.claudeCodeHook = await askYn("Claude Code hook", true);
66
- rl.close();
67
- return result;
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
+ value: "claudeMd",
122
+ label: "CLAUDE.md reference",
123
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
124
+ }
125
+ ],
126
+ initialValues: ["preCommit", "claude", "claudeMd"],
127
+ required: false
128
+ });
129
+ assertNotCancelled(result);
130
+ return {
131
+ preCommitHook: result.includes("preCommit"),
132
+ claudeCodeHook: result.includes("claude"),
133
+ claudeMdRef: result.includes("claudeMd")
134
+ };
68
135
  }
69
136
 
70
137
  // src/utils/resolve-workspace-packages.ts
@@ -121,67 +188,65 @@ async function boundariesCommand(options, cwd) {
121
188
  displayRules(config);
122
189
  }
123
190
  function displayRules(config) {
124
- if (!config.boundaries || config.boundaries.length === 0) {
125
- console.log(chalk2.yellow("No boundary rules configured."));
126
- console.log(`Run ${chalk2.cyan("viberails boundaries --infer")} to generate rules.`);
191
+ if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
192
+ console.log(chalk.yellow("No boundary rules configured."));
193
+ console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
127
194
  return;
128
195
  }
129
- const allowRules = config.boundaries.filter((r) => r.allow);
130
- const denyRules = config.boundaries.filter((r) => !r.allow);
196
+ const { deny } = config.boundaries;
197
+ const sources = Object.keys(deny).filter((k) => deny[k].length > 0);
198
+ const totalRules = sources.reduce((sum, k) => sum + deny[k].length, 0);
131
199
  console.log(`
132
- ${chalk2.bold(`Boundary rules (${config.boundaries.length} rules):`)}
200
+ ${chalk.bold(`Boundary rules (${totalRules} deny rules):`)}
133
201
  `);
134
- for (const r of allowRules) {
135
- console.log(` ${chalk2.green("\u2713")} ${r.from} \u2192 ${r.to}`);
136
- }
137
- for (const r of denyRules) {
138
- const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
139
- console.log(` ${chalk2.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
202
+ for (const source of sources) {
203
+ for (const target of deny[source]) {
204
+ console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
205
+ }
140
206
  }
141
207
  console.log(
142
208
  `
143
- Enforcement: ${config.rules.enforceBoundaries ? chalk2.green("on") : chalk2.yellow("off")}`
209
+ Enforcement: ${config.rules.enforceBoundaries ? chalk.green("on") : chalk.yellow("off")}`
144
210
  );
145
211
  }
146
212
  async function inferAndDisplay(projectRoot, config, configPath) {
147
- console.log(chalk2.dim("Analyzing imports..."));
213
+ console.log(chalk.dim("Analyzing imports..."));
148
214
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
149
215
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
150
216
  const graph = await buildImportGraph(projectRoot, {
151
217
  packages,
152
218
  ignore: config.ignore
153
219
  });
154
- console.log(chalk2.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
220
+ console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
155
221
  const inferred = inferBoundaries(graph);
156
- if (inferred.length === 0) {
157
- console.log(chalk2.yellow("No boundary rules could be inferred."));
222
+ const sources = Object.keys(inferred.deny).filter((k) => inferred.deny[k].length > 0);
223
+ const totalRules = sources.reduce((sum, k) => sum + inferred.deny[k].length, 0);
224
+ if (totalRules === 0) {
225
+ console.log(chalk.yellow("No boundary rules could be inferred."));
158
226
  return;
159
227
  }
160
- const allow = inferred.filter((r) => r.allow);
161
- const deny = inferred.filter((r) => !r.allow);
162
228
  console.log(`
163
- ${chalk2.bold("Inferred boundary rules:")}
229
+ ${chalk.bold("Inferred boundary rules:")}
164
230
  `);
165
- for (const r of allow) {
166
- console.log(` ${chalk2.green("\u2713")} ${r.from} \u2192 ${r.to}`);
167
- }
168
- for (const r of deny) {
169
- const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
170
- console.log(` ${chalk2.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
231
+ for (const source of sources) {
232
+ for (const target of inferred.deny[source]) {
233
+ console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
234
+ }
171
235
  }
172
236
  console.log(`
173
- ${allow.length} allowed, ${deny.length} denied`);
174
- const shouldSave = await confirm("\nSave to viberails.config.json?");
237
+ ${totalRules} denied`);
238
+ console.log("");
239
+ const shouldSave = await confirm2("Save to viberails.config.json?");
175
240
  if (shouldSave) {
176
241
  config.boundaries = inferred;
177
242
  config.rules.enforceBoundaries = true;
178
243
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
179
244
  `);
180
- console.log(`${chalk2.green("\u2713")} Saved ${inferred.length} rules`);
245
+ console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
181
246
  }
182
247
  }
183
248
  async function showGraph(projectRoot, config) {
184
- console.log(chalk2.dim("Building import graph..."));
249
+ console.log(chalk.dim("Building import graph..."));
185
250
  const { buildImportGraph } = await import("@viberails/graph");
186
251
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
187
252
  const graph = await buildImportGraph(projectRoot, {
@@ -189,20 +254,20 @@ async function showGraph(projectRoot, config) {
189
254
  ignore: config.ignore
190
255
  });
191
256
  console.log(`
192
- ${chalk2.bold("Import dependency graph:")}
257
+ ${chalk.bold("Import dependency graph:")}
193
258
  `);
194
259
  console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
195
260
  `);
196
261
  if (graph.packages.length > 0) {
197
262
  for (const pkg of graph.packages) {
198
263
  const deps = pkg.internalDeps.length > 0 ? `
199
- ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : chalk2.dim(" (no internal deps)");
264
+ ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : chalk.dim(" (no internal deps)");
200
265
  console.log(` ${pkg.name}${deps}`);
201
266
  }
202
267
  }
203
268
  if (graph.cycles.length > 0) {
204
269
  console.log(`
205
- ${chalk2.yellow("Cycles detected:")}`);
270
+ ${chalk.yellow("Cycles detected:")}`);
206
271
  for (const cycle of graph.cycles) {
207
272
  const paths = cycle.map((f) => path3.relative(projectRoot, f));
208
273
  console.log(` ${paths.join(" \u2192 ")}`);
@@ -214,7 +279,7 @@ ${chalk2.yellow("Cycles detected:")}`);
214
279
  import * as fs6 from "fs";
215
280
  import * as path6 from "path";
216
281
  import { loadConfig as loadConfig2 } from "@viberails/config";
217
- import chalk3 from "chalk";
282
+ import chalk2 from "chalk";
218
283
 
219
284
  // src/commands/check-config.ts
220
285
  function resolveConfigForFile(relPath, config) {
@@ -452,12 +517,12 @@ function printGroupedViolations(violations, limit) {
452
517
  const toShow = group.slice(0, remaining);
453
518
  const hidden = group.length - toShow.length;
454
519
  for (const v of toShow) {
455
- const icon = v.severity === "error" ? chalk3.red("\u2717") : chalk3.yellow("!");
456
- console.log(`${icon} ${chalk3.dim(v.rule)} ${v.file}: ${v.message}`);
520
+ const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
521
+ console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
457
522
  }
458
523
  totalShown += toShow.length;
459
524
  if (hidden > 0) {
460
- console.log(chalk3.dim(` ... and ${hidden} more ${rule} violations`));
525
+ console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
461
526
  }
462
527
  }
463
528
  }
@@ -475,13 +540,13 @@ async function checkCommand(options, cwd) {
475
540
  const startDir = cwd ?? process.cwd();
476
541
  const projectRoot = findProjectRoot(startDir);
477
542
  if (!projectRoot) {
478
- console.error(`${chalk3.red("Error:")} No package.json found. Are you in a JS/TS project?`);
543
+ console.error(`${chalk2.red("Error:")} No package.json found. Are you in a JS/TS project?`);
479
544
  return 1;
480
545
  }
481
546
  const configPath = path6.join(projectRoot, CONFIG_FILE2);
482
547
  if (!fs6.existsSync(configPath)) {
483
548
  console.error(
484
- `${chalk3.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
549
+ `${chalk2.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
485
550
  );
486
551
  return 1;
487
552
  }
@@ -495,7 +560,7 @@ async function checkCommand(options, cwd) {
495
560
  filesToCheck = getAllSourceFiles(projectRoot, config);
496
561
  }
497
562
  if (filesToCheck.length === 0) {
498
- console.log(`${chalk3.green("\u2713")} No files to check.`);
563
+ console.log(`${chalk2.green("\u2713")} No files to check.`);
499
564
  return 0;
500
565
  }
501
566
  const violations = [];
@@ -536,7 +601,7 @@ async function checkCommand(options, cwd) {
536
601
  const testViolations = checkMissingTests(projectRoot, config, severity);
537
602
  violations.push(...testViolations);
538
603
  }
539
- if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
604
+ if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
540
605
  const startTime = Date.now();
541
606
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
542
607
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -552,12 +617,12 @@ async function checkCommand(options, cwd) {
552
617
  violations.push({
553
618
  file: relFile,
554
619
  rule: "boundary-violation",
555
- message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}${bv.rule.reason ? ` (${bv.rule.reason})` : ""}`,
620
+ message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}`,
556
621
  severity
557
622
  });
558
623
  }
559
624
  const elapsed = Date.now() - startTime;
560
- console.log(chalk3.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
625
+ console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
561
626
  }
562
627
  if (options.format === "json") {
563
628
  console.log(
@@ -570,7 +635,7 @@ async function checkCommand(options, cwd) {
570
635
  return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
571
636
  }
572
637
  if (violations.length === 0) {
573
- console.log(`${chalk3.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
638
+ console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
574
639
  return 0;
575
640
  }
576
641
  if (!options.quiet) {
@@ -578,7 +643,7 @@ async function checkCommand(options, cwd) {
578
643
  }
579
644
  printSummary(violations);
580
645
  if (config.enforcement === "enforce") {
581
- console.log(chalk3.red("Fix violations before committing."));
646
+ console.log(chalk2.red("Fix violations before committing."));
582
647
  return 1;
583
648
  }
584
649
  return 0;
@@ -588,23 +653,22 @@ async function checkCommand(options, cwd) {
588
653
  import * as fs9 from "fs";
589
654
  import * as path10 from "path";
590
655
  import { loadConfig as loadConfig3 } from "@viberails/config";
591
- import chalk5 from "chalk";
656
+ import chalk4 from "chalk";
592
657
 
593
658
  // src/commands/fix-helpers.ts
594
659
  import { execSync as execSync2 } from "child_process";
595
- import { createInterface as createInterface2 } from "readline";
596
- import chalk4 from "chalk";
660
+ import chalk3 from "chalk";
597
661
  function printPlan(renames, stubs) {
598
662
  if (renames.length > 0) {
599
- console.log(chalk4.bold("\nFile renames:"));
663
+ console.log(chalk3.bold("\nFile renames:"));
600
664
  for (const r of renames) {
601
- console.log(` ${chalk4.red(r.oldPath)} \u2192 ${chalk4.green(r.newPath)}`);
665
+ console.log(` ${chalk3.red(r.oldPath)} \u2192 ${chalk3.green(r.newPath)}`);
602
666
  }
603
667
  }
604
668
  if (stubs.length > 0) {
605
- console.log(chalk4.bold("\nTest stubs to create:"));
669
+ console.log(chalk3.bold("\nTest stubs to create:"));
606
670
  for (const s of stubs) {
607
- console.log(` ${chalk4.green("+")} ${s.path}`);
671
+ console.log(` ${chalk3.green("+")} ${s.path}`);
608
672
  }
609
673
  }
610
674
  }
@@ -626,15 +690,6 @@ function getConventionValue(convention) {
626
690
  }
627
691
  return void 0;
628
692
  }
629
- function promptConfirm(question) {
630
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
631
- return new Promise((resolve4) => {
632
- rl.question(`${question} (y/N) `, (answer) => {
633
- rl.close();
634
- resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
635
- });
636
- });
637
- }
638
693
 
639
694
  // src/commands/fix-imports.ts
640
695
  import * as path7 from "path";
@@ -855,13 +910,13 @@ async function fixCommand(options, cwd) {
855
910
  const startDir = cwd ?? process.cwd();
856
911
  const projectRoot = findProjectRoot(startDir);
857
912
  if (!projectRoot) {
858
- console.error(`${chalk5.red("Error:")} No package.json found. Are you in a JS/TS project?`);
913
+ console.error(`${chalk4.red("Error:")} No package.json found. Are you in a JS/TS project?`);
859
914
  return 1;
860
915
  }
861
916
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
862
917
  if (!fs9.existsSync(configPath)) {
863
918
  console.error(
864
- `${chalk5.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
919
+ `${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
865
920
  );
866
921
  return 1;
867
922
  }
@@ -870,7 +925,7 @@ async function fixCommand(options, cwd) {
870
925
  const isDirty = checkGitDirty(projectRoot);
871
926
  if (isDirty) {
872
927
  console.log(
873
- chalk5.yellow("Warning: You have uncommitted changes. Consider committing first.")
928
+ chalk4.yellow("Warning: You have uncommitted changes. Consider committing first.")
874
929
  );
875
930
  }
876
931
  }
@@ -900,16 +955,16 @@ async function fixCommand(options, cwd) {
900
955
  }
901
956
  }
902
957
  if (dedupedRenames.length === 0 && testStubs.length === 0) {
903
- console.log(`${chalk5.green("\u2713")} No fixable violations found.`);
958
+ console.log(`${chalk4.green("\u2713")} No fixable violations found.`);
904
959
  return 0;
905
960
  }
906
961
  printPlan(dedupedRenames, testStubs);
907
962
  if (options.dryRun) {
908
- console.log(chalk5.dim("\nDry run \u2014 no changes applied."));
963
+ console.log(chalk4.dim("\nDry run \u2014 no changes applied."));
909
964
  return 0;
910
965
  }
911
966
  if (!options.yes) {
912
- const confirmed = await promptConfirm("Apply these fixes?");
967
+ const confirmed = await confirmDangerous("Apply these fixes?");
913
968
  if (!confirmed) {
914
969
  console.log("Aborted.");
915
970
  return 0;
@@ -936,15 +991,15 @@ async function fixCommand(options, cwd) {
936
991
  }
937
992
  console.log("");
938
993
  if (renameCount > 0) {
939
- console.log(`${chalk5.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
994
+ console.log(`${chalk4.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
940
995
  }
941
996
  if (importUpdateCount > 0) {
942
997
  console.log(
943
- `${chalk5.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
998
+ `${chalk4.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
944
999
  );
945
1000
  }
946
1001
  if (stubCount > 0) {
947
- console.log(`${chalk5.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
1002
+ console.log(`${chalk4.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
948
1003
  }
949
1004
  return 0;
950
1005
  }
@@ -952,13 +1007,19 @@ async function fixCommand(options, cwd) {
952
1007
  // src/commands/init.ts
953
1008
  import * as fs12 from "fs";
954
1009
  import * as path13 from "path";
1010
+ import * as clack2 from "@clack/prompts";
955
1011
  import { generateConfig } from "@viberails/config";
956
1012
  import { scan } from "@viberails/scanner";
957
- import chalk9 from "chalk";
1013
+ import chalk8 from "chalk";
958
1014
 
959
- // src/display.ts
960
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, ORM_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
961
- import chalk7 from "chalk";
1015
+ // src/display-text.ts
1016
+ import {
1017
+ CONVENTION_LABELS as CONVENTION_LABELS2,
1018
+ FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
1019
+ LIBRARY_NAMES as LIBRARY_NAMES2,
1020
+ ORM_NAMES as ORM_NAMES2,
1021
+ STYLING_NAMES as STYLING_NAMES3
1022
+ } from "@viberails/types";
962
1023
 
963
1024
  // src/display-helpers.ts
964
1025
  import { ROLE_DESCRIPTIONS } from "@viberails/types";
@@ -1009,9 +1070,19 @@ function formatRoleGroup(group) {
1009
1070
  return `${group.label} \u2014 ${dirs} (${files})`;
1010
1071
  }
1011
1072
 
1073
+ // src/display.ts
1074
+ import {
1075
+ CONVENTION_LABELS,
1076
+ FRAMEWORK_NAMES as FRAMEWORK_NAMES2,
1077
+ LIBRARY_NAMES,
1078
+ ORM_NAMES,
1079
+ STYLING_NAMES as STYLING_NAMES2
1080
+ } from "@viberails/types";
1081
+ import chalk6 from "chalk";
1082
+
1012
1083
  // src/display-monorepo.ts
1013
1084
  import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1014
- import chalk6 from "chalk";
1085
+ import chalk5 from "chalk";
1015
1086
  function formatPackageSummary(pkg) {
1016
1087
  const parts = [];
1017
1088
  if (pkg.stack.framework) {
@@ -1027,19 +1098,19 @@ function formatPackageSummary(pkg) {
1027
1098
  function displayMonorepoResults(scanResult) {
1028
1099
  const { stack, packages } = scanResult;
1029
1100
  console.log(`
1030
- ${chalk6.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1031
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
1101
+ ${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1102
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
1032
1103
  if (stack.packageManager) {
1033
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
1104
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
1034
1105
  }
1035
1106
  if (stack.linter) {
1036
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
1107
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
1037
1108
  }
1038
1109
  if (stack.formatter) {
1039
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
1110
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
1040
1111
  }
1041
1112
  if (stack.testRunner) {
1042
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
1113
+ console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
1043
1114
  }
1044
1115
  console.log("");
1045
1116
  for (const pkg of packages) {
@@ -1050,13 +1121,13 @@ ${chalk6.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1050
1121
  );
1051
1122
  if (packagesWithDirs.length > 0) {
1052
1123
  console.log(`
1053
- ${chalk6.bold("Structure:")}`);
1124
+ ${chalk5.bold("Structure:")}`);
1054
1125
  for (const pkg of packagesWithDirs) {
1055
1126
  const groups = groupByRole(pkg.structure.directories);
1056
1127
  if (groups.length === 0) continue;
1057
1128
  console.log(` ${pkg.relativePath}:`);
1058
1129
  for (const group of groups) {
1059
- console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
1130
+ console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
1060
1131
  }
1061
1132
  }
1062
1133
  }
@@ -1064,14 +1135,60 @@ ${chalk6.bold("Structure:")}`);
1064
1135
  displaySummarySection(scanResult);
1065
1136
  console.log("");
1066
1137
  }
1138
+ function formatPackageSummaryPlain(pkg) {
1139
+ const parts = [];
1140
+ if (pkg.stack.framework) {
1141
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1142
+ }
1143
+ if (pkg.stack.styling) {
1144
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1145
+ }
1146
+ const files = `${pkg.statistics.totalFiles} files`;
1147
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1148
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1149
+ }
1150
+ function formatMonorepoResultsText(scanResult, config) {
1151
+ const lines = [];
1152
+ const { stack, packages } = scanResult;
1153
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1154
+ const sharedParts = [formatItem(stack.language)];
1155
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1156
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1157
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1158
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1159
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1160
+ lines.push("");
1161
+ for (const pkg of packages) {
1162
+ lines.push(formatPackageSummaryPlain(pkg));
1163
+ }
1164
+ const packagesWithDirs = packages.filter(
1165
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1166
+ );
1167
+ if (packagesWithDirs.length > 0) {
1168
+ lines.push("");
1169
+ lines.push("Structure:");
1170
+ for (const pkg of packagesWithDirs) {
1171
+ const groups = groupByRole(pkg.structure.directories);
1172
+ if (groups.length === 0) continue;
1173
+ lines.push(` ${pkg.relativePath}:`);
1174
+ for (const group of groups) {
1175
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1176
+ }
1177
+ }
1178
+ }
1179
+ lines.push(...formatConventionsText(scanResult));
1180
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1181
+ lines.push("");
1182
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1183
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1184
+ if (ext) {
1185
+ lines.push(ext);
1186
+ }
1187
+ lines.push(...formatRulesText(config));
1188
+ return lines.join("\n");
1189
+ }
1067
1190
 
1068
1191
  // src/display.ts
1069
- var CONVENTION_LABELS = {
1070
- fileNaming: "File naming",
1071
- componentNaming: "Component naming",
1072
- hookNaming: "Hook naming",
1073
- importAlias: "Import alias"
1074
- };
1075
1192
  function formatItem(item, nameMap) {
1076
1193
  const name = nameMap?.[item.name] ?? item.name;
1077
1194
  return item.version ? `${name} ${item.version}` : name;
@@ -1087,7 +1204,7 @@ function displayConventions(scanResult) {
1087
1204
  const conventionEntries = Object.entries(scanResult.conventions);
1088
1205
  if (conventionEntries.length === 0) return;
1089
1206
  console.log(`
1090
- ${chalk7.bold("Conventions:")}`);
1207
+ ${chalk6.bold("Conventions:")}`);
1091
1208
  for (const [key, convention] of conventionEntries) {
1092
1209
  if (convention.confidence === "low") continue;
1093
1210
  const label = CONVENTION_LABELS[key] ?? key;
@@ -1095,19 +1212,19 @@ ${chalk7.bold("Conventions:")}`);
1095
1212
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1096
1213
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1097
1214
  if (allSame || pkgValues.length <= 1) {
1098
- const ind = convention.confidence === "high" ? chalk7.green("\u2713") : chalk7.yellow("~");
1099
- const detail = chalk7.dim(`(${confidenceLabel(convention)})`);
1215
+ const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1216
+ const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1100
1217
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1101
1218
  } else {
1102
- console.log(` ${chalk7.yellow("~")} ${label}: varies by package`);
1219
+ console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
1103
1220
  for (const pv of pkgValues) {
1104
1221
  const pct = Math.round(pv.convention.consistency);
1105
1222
  console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1106
1223
  }
1107
1224
  }
1108
1225
  } else {
1109
- const ind = convention.confidence === "high" ? chalk7.green("\u2713") : chalk7.yellow("~");
1110
- const detail = chalk7.dim(`(${confidenceLabel(convention)})`);
1226
+ const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1227
+ const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1111
1228
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1112
1229
  }
1113
1230
  }
@@ -1115,7 +1232,7 @@ ${chalk7.bold("Conventions:")}`);
1115
1232
  function displaySummarySection(scanResult) {
1116
1233
  const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1117
1234
  console.log(`
1118
- ${chalk7.bold("Summary:")}`);
1235
+ ${chalk6.bold("Summary:")}`);
1119
1236
  console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1120
1237
  const ext = formatExtensions(scanResult.statistics.filesByExtension);
1121
1238
  if (ext) {
@@ -1129,43 +1246,43 @@ function displayScanResults(scanResult) {
1129
1246
  }
1130
1247
  const { stack } = scanResult;
1131
1248
  console.log(`
1132
- ${chalk7.bold("Detected:")}`);
1249
+ ${chalk6.bold("Detected:")}`);
1133
1250
  if (stack.framework) {
1134
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1251
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1135
1252
  }
1136
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.language)}`);
1253
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
1137
1254
  if (stack.styling) {
1138
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1255
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1139
1256
  }
1140
1257
  if (stack.backend) {
1141
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1258
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1142
1259
  }
1143
1260
  if (stack.orm) {
1144
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
1261
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
1145
1262
  }
1146
1263
  if (stack.linter) {
1147
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.linter)}`);
1264
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
1148
1265
  }
1149
1266
  if (stack.formatter) {
1150
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.formatter)}`);
1267
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
1151
1268
  }
1152
1269
  if (stack.testRunner) {
1153
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.testRunner)}`);
1270
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
1154
1271
  }
1155
1272
  if (stack.packageManager) {
1156
- console.log(` ${chalk7.green("\u2713")} ${formatItem(stack.packageManager)}`);
1273
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
1157
1274
  }
1158
1275
  if (stack.libraries.length > 0) {
1159
1276
  for (const lib of stack.libraries) {
1160
- console.log(` ${chalk7.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1277
+ console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1161
1278
  }
1162
1279
  }
1163
1280
  const groups = groupByRole(scanResult.structure.directories);
1164
1281
  if (groups.length > 0) {
1165
1282
  console.log(`
1166
- ${chalk7.bold("Structure:")}`);
1283
+ ${chalk6.bold("Structure:")}`);
1167
1284
  for (const group of groups) {
1168
- console.log(` ${chalk7.green("\u2713")} ${formatRoleGroup(group)}`);
1285
+ console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
1169
1286
  }
1170
1287
  }
1171
1288
  displayConventions(scanResult);
@@ -1176,38 +1293,151 @@ function getConventionStr(cv) {
1176
1293
  return typeof cv === "string" ? cv : cv.value;
1177
1294
  }
1178
1295
  function displayRulesPreview(config) {
1179
- console.log(`${chalk7.bold("Rules:")}`);
1180
- console.log(` ${chalk7.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1296
+ console.log(`${chalk6.bold("Rules:")}`);
1297
+ console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1181
1298
  if (config.rules.requireTests && config.structure.testPattern) {
1182
1299
  console.log(
1183
- ` ${chalk7.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1300
+ ` ${chalk6.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1184
1301
  );
1185
1302
  } else if (config.rules.requireTests) {
1186
- console.log(` ${chalk7.dim("\u2022")} Require test files: yes`);
1303
+ console.log(` ${chalk6.dim("\u2022")} Require test files: yes`);
1187
1304
  } else {
1188
- console.log(` ${chalk7.dim("\u2022")} Require test files: no`);
1305
+ console.log(` ${chalk6.dim("\u2022")} Require test files: no`);
1189
1306
  }
1190
1307
  if (config.rules.enforceNaming && config.conventions.fileNaming) {
1191
1308
  console.log(
1192
- ` ${chalk7.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1309
+ ` ${chalk6.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1193
1310
  );
1194
1311
  } else {
1195
- console.log(` ${chalk7.dim("\u2022")} Enforce file naming: no`);
1312
+ console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
1196
1313
  }
1197
1314
  console.log(
1198
- ` ${chalk7.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1315
+ ` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1199
1316
  );
1200
1317
  console.log("");
1201
1318
  if (config.enforcement === "enforce") {
1202
- console.log(`${chalk7.bold("Enforcement mode:")} enforce (violations will block commits)`);
1319
+ console.log(`${chalk6.bold("Enforcement mode:")} enforce (violations will block commits)`);
1203
1320
  } else {
1204
1321
  console.log(
1205
- `${chalk7.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1322
+ `${chalk6.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1206
1323
  );
1207
1324
  }
1208
1325
  console.log("");
1209
1326
  }
1210
1327
 
1328
+ // src/display-text.ts
1329
+ function getConventionStr2(cv) {
1330
+ return typeof cv === "string" ? cv : cv.value;
1331
+ }
1332
+ function plainConfidenceLabel(convention) {
1333
+ const pct = Math.round(convention.consistency);
1334
+ if (convention.confidence === "high") {
1335
+ return `${pct}%`;
1336
+ }
1337
+ return `${pct}%, suggested only`;
1338
+ }
1339
+ function formatConventionsText(scanResult) {
1340
+ const lines = [];
1341
+ const conventionEntries = Object.entries(scanResult.conventions);
1342
+ if (conventionEntries.length === 0) return lines;
1343
+ lines.push("");
1344
+ lines.push("Conventions:");
1345
+ for (const [key, convention] of conventionEntries) {
1346
+ if (convention.confidence === "low") continue;
1347
+ const label = CONVENTION_LABELS2[key] ?? key;
1348
+ if (scanResult.packages.length > 1) {
1349
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1350
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1351
+ if (allSame || pkgValues.length <= 1) {
1352
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1353
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1354
+ } else {
1355
+ lines.push(` ~ ${label}: varies by package`);
1356
+ for (const pv of pkgValues) {
1357
+ const pct = Math.round(pv.convention.consistency);
1358
+ lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1359
+ }
1360
+ }
1361
+ } else {
1362
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1363
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1364
+ }
1365
+ }
1366
+ return lines;
1367
+ }
1368
+ function formatRulesText(config) {
1369
+ const lines = [];
1370
+ lines.push("");
1371
+ lines.push("Rules:");
1372
+ lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
1373
+ if (config.rules.requireTests && config.structure.testPattern) {
1374
+ lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
1375
+ } else if (config.rules.requireTests) {
1376
+ lines.push(" \u2022 Require test files: yes");
1377
+ } else {
1378
+ lines.push(" \u2022 Require test files: no");
1379
+ }
1380
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1381
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
1382
+ } else {
1383
+ lines.push(" \u2022 Enforce file naming: no");
1384
+ }
1385
+ lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
1386
+ return lines;
1387
+ }
1388
+ function formatScanResultsText(scanResult, config) {
1389
+ if (scanResult.packages.length > 1) {
1390
+ return formatMonorepoResultsText(scanResult, config);
1391
+ }
1392
+ const lines = [];
1393
+ const { stack } = scanResult;
1394
+ lines.push("Detected:");
1395
+ if (stack.framework) {
1396
+ lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
1397
+ }
1398
+ lines.push(` \u2713 ${formatItem(stack.language)}`);
1399
+ if (stack.styling) {
1400
+ lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
1401
+ }
1402
+ if (stack.backend) {
1403
+ lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
1404
+ }
1405
+ if (stack.orm) {
1406
+ lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
1407
+ }
1408
+ const secondaryParts = [];
1409
+ if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
1410
+ if (stack.linter) secondaryParts.push(formatItem(stack.linter));
1411
+ if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
1412
+ if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
1413
+ if (secondaryParts.length > 0) {
1414
+ lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
1415
+ }
1416
+ if (stack.libraries.length > 0) {
1417
+ for (const lib of stack.libraries) {
1418
+ lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
1419
+ }
1420
+ }
1421
+ const groups = groupByRole(scanResult.structure.directories);
1422
+ if (groups.length > 0) {
1423
+ lines.push("");
1424
+ lines.push("Structure:");
1425
+ for (const group of groups) {
1426
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1427
+ }
1428
+ }
1429
+ lines.push(...formatConventionsText(scanResult));
1430
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1431
+ lines.push("");
1432
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1433
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1434
+ if (ext) {
1435
+ lines.push(ext);
1436
+ }
1437
+ lines.push(...formatRulesText(config));
1438
+ return lines.join("\n");
1439
+ }
1440
+
1211
1441
  // src/utils/write-generated-files.ts
1212
1442
  import * as fs10 from "fs";
1213
1443
  import * as path11 from "path";
@@ -1237,18 +1467,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1237
1467
  // src/commands/init-hooks.ts
1238
1468
  import * as fs11 from "fs";
1239
1469
  import * as path12 from "path";
1240
- import chalk8 from "chalk";
1470
+ import chalk7 from "chalk";
1241
1471
  function setupPreCommitHook(projectRoot) {
1242
1472
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1243
1473
  if (fs11.existsSync(lefthookPath)) {
1244
1474
  addLefthookPreCommit(lefthookPath);
1245
- console.log(` ${chalk8.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1475
+ console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1246
1476
  return;
1247
1477
  }
1248
1478
  const huskyDir = path12.join(projectRoot, ".husky");
1249
1479
  if (fs11.existsSync(huskyDir)) {
1250
1480
  writeHuskyPreCommit(huskyDir);
1251
- console.log(` ${chalk8.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1481
+ console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1252
1482
  return;
1253
1483
  }
1254
1484
  const gitDir = path12.join(projectRoot, ".git");
@@ -1258,7 +1488,7 @@ function setupPreCommitHook(projectRoot) {
1258
1488
  fs11.mkdirSync(hooksDir, { recursive: true });
1259
1489
  }
1260
1490
  writeGitHookPreCommit(hooksDir);
1261
- console.log(` ${chalk8.green("\u2713")} .git/hooks/pre-commit`);
1491
+ console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
1262
1492
  }
1263
1493
  }
1264
1494
  function writeGitHookPreCommit(hooksDir) {
@@ -1328,7 +1558,7 @@ function setupClaudeCodeHook(projectRoot) {
1328
1558
  settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1329
1559
  } catch {
1330
1560
  console.warn(
1331
- ` ${chalk8.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1561
+ ` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1332
1562
  );
1333
1563
  settings = {};
1334
1564
  }
@@ -1337,7 +1567,17 @@ function setupClaudeCodeHook(projectRoot) {
1337
1567
  const existing = hooks.PostToolUse ?? [];
1338
1568
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1339
1569
  const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1340
- const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1570
+ const checkAndReport = [
1571
+ `FILE=$(${extractFile})`,
1572
+ 'if [ -z "$FILE" ]; then exit 0; fi',
1573
+ 'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
1574
+ `if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
1575
+ ' echo "$OUTPUT" >&2',
1576
+ " exit 2",
1577
+ "fi",
1578
+ "exit 0"
1579
+ ].join("\n");
1580
+ const hookCommand = checkAndReport;
1341
1581
  hooks.PostToolUse = [
1342
1582
  ...existing,
1343
1583
  {
@@ -1353,7 +1593,19 @@ function setupClaudeCodeHook(projectRoot) {
1353
1593
  settings.hooks = hooks;
1354
1594
  fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1355
1595
  `);
1356
- console.log(` ${chalk8.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1596
+ console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1597
+ }
1598
+ function setupClaudeMdReference(projectRoot) {
1599
+ const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
1600
+ let content = "";
1601
+ if (fs11.existsSync(claudeMdPath)) {
1602
+ content = fs11.readFileSync(claudeMdPath, "utf-8");
1603
+ }
1604
+ if (content.includes("@.viberails/context.md")) return;
1605
+ const ref = "\n@.viberails/context.md\n";
1606
+ const prefix = content.length === 0 ? "" : content.trimEnd();
1607
+ fs11.writeFileSync(claudeMdPath, prefix + ref);
1608
+ console.log(` ${chalk7.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
1357
1609
  }
1358
1610
  function writeHuskyPreCommit(huskyDir) {
1359
1611
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1383,6 +1635,14 @@ function filterHighConfidence(conventions) {
1383
1635
  }
1384
1636
  return filtered;
1385
1637
  }
1638
+ function getConventionStr3(cv) {
1639
+ if (!cv) return void 0;
1640
+ return typeof cv === "string" ? cv : cv.value;
1641
+ }
1642
+ function hasConventionOverrides(config) {
1643
+ if (!config.packages || config.packages.length === 0) return false;
1644
+ return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1645
+ }
1386
1646
  async function initCommand(options, cwd) {
1387
1647
  const startDir = cwd ?? process.cwd();
1388
1648
  const projectRoot = findProjectRoot(startDir);
@@ -1392,84 +1652,146 @@ async function initCommand(options, cwd) {
1392
1652
  );
1393
1653
  }
1394
1654
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1395
- if (fs12.existsSync(configPath)) {
1655
+ if (fs12.existsSync(configPath) && !options.force) {
1396
1656
  console.log(
1397
- chalk9.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk9.cyan("viberails sync") + " to update the generated files."
1657
+ `${chalk8.yellow("!")} viberails is already initialized.
1658
+ Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
1398
1659
  );
1399
1660
  return;
1400
1661
  }
1401
- console.log(chalk9.dim("Scanning project..."));
1402
- const scanResult = await scan(projectRoot);
1403
- const config = generateConfig(scanResult);
1404
1662
  if (options.yes) {
1405
- config.conventions = filterHighConfidence(config.conventions);
1663
+ console.log(chalk8.dim("Scanning project..."));
1664
+ const scanResult2 = await scan(projectRoot);
1665
+ const config2 = generateConfig(scanResult2);
1666
+ config2.conventions = filterHighConfidence(config2.conventions);
1667
+ displayScanResults(scanResult2);
1668
+ displayRulesPreview(config2);
1669
+ if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
1670
+ console.log(chalk8.dim("Building import graph..."));
1671
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1672
+ const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
1673
+ const graph = await buildImportGraph(projectRoot, {
1674
+ packages,
1675
+ ignore: config2.ignore
1676
+ });
1677
+ const inferred = inferBoundaries(graph);
1678
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1679
+ if (denyCount > 0) {
1680
+ config2.boundaries = inferred;
1681
+ config2.rules.enforceBoundaries = true;
1682
+ console.log(` Inferred ${denyCount} boundary rules`);
1683
+ }
1684
+ }
1685
+ fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1686
+ `);
1687
+ writeGeneratedFiles(projectRoot, config2, scanResult2);
1688
+ updateGitignore(projectRoot);
1689
+ setupClaudeMdReference(projectRoot);
1690
+ console.log(`
1691
+ Created:`);
1692
+ console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
1693
+ console.log(` ${chalk8.green("\u2713")} .viberails/context.md`);
1694
+ console.log(` ${chalk8.green("\u2713")} .viberails/scan-result.json`);
1695
+ return;
1406
1696
  }
1407
- displayScanResults(scanResult);
1697
+ clack2.intro("viberails");
1698
+ const s = clack2.spinner();
1699
+ s.start("Scanning project...");
1700
+ const scanResult = await scan(projectRoot);
1701
+ const config = generateConfig(scanResult);
1702
+ s.stop("Scan complete");
1408
1703
  if (scanResult.statistics.totalFiles === 0) {
1409
- console.log(
1410
- chalk9.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + chalk9.cyan("viberails sync") + " after adding source files.\n"
1704
+ clack2.log.warn(
1705
+ "No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
1411
1706
  );
1412
1707
  }
1413
- displayRulesPreview(config);
1414
- if (!options.yes) {
1415
- const accepted = await confirm("Proceed with these settings?");
1416
- if (!accepted) {
1417
- console.log("Aborted.");
1418
- return;
1708
+ const resultsText = formatScanResultsText(scanResult, config);
1709
+ clack2.note(resultsText, "Scan results");
1710
+ const decision = await promptInitDecision();
1711
+ if (decision === "customize") {
1712
+ clack2.note(
1713
+ "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1714
+ "Rules"
1715
+ );
1716
+ const overrides = await promptRuleCustomization({
1717
+ maxFileLines: config.rules.maxFileLines,
1718
+ requireTests: config.rules.requireTests,
1719
+ enforceNaming: config.rules.enforceNaming,
1720
+ enforcement: config.enforcement,
1721
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1722
+ });
1723
+ config.rules.maxFileLines = overrides.maxFileLines;
1724
+ config.rules.requireTests = overrides.requireTests;
1725
+ config.rules.enforceNaming = overrides.enforceNaming;
1726
+ config.enforcement = overrides.enforcement;
1727
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1728
+ clack2.note(
1729
+ 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1730
+ "Per-package overrides"
1731
+ );
1419
1732
  }
1420
1733
  }
1421
- if (config.workspace && config.workspace.packages.length > 0) {
1422
- let shouldInfer = options.yes;
1423
- if (!options.yes) {
1424
- console.log(chalk9.dim(" Scans imports between packages to suggest dependency rules"));
1425
- shouldInfer = await confirm("Infer boundary rules from import patterns?");
1426
- }
1734
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1735
+ clack2.note(
1736
+ "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
1737
+ "Boundaries"
1738
+ );
1739
+ const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
1427
1740
  if (shouldInfer) {
1428
- console.log(chalk9.dim("Building import graph..."));
1741
+ const bs = clack2.spinner();
1742
+ bs.start("Building import graph...");
1429
1743
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1430
1744
  const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1431
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1745
+ const graph = await buildImportGraph(projectRoot, {
1746
+ packages,
1747
+ ignore: config.ignore
1748
+ });
1432
1749
  const inferred = inferBoundaries(graph);
1433
- if (inferred.length > 0) {
1750
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1751
+ if (denyCount > 0) {
1434
1752
  config.boundaries = inferred;
1435
1753
  config.rules.enforceBoundaries = true;
1436
- console.log(` ${chalk9.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1754
+ bs.stop(`Inferred ${denyCount} boundary rules`);
1755
+ } else {
1756
+ bs.stop("No boundary rules inferred");
1437
1757
  }
1438
1758
  }
1439
1759
  }
1440
1760
  const hookManager = detectHookManager(projectRoot);
1441
- let integrations = { preCommitHook: true, claudeCodeHook: true };
1442
- if (!options.yes) {
1443
- console.log("");
1444
- integrations = await selectIntegrations(hookManager);
1761
+ const integrations = await promptIntegrations(hookManager);
1762
+ if (hasConventionOverrides(config)) {
1763
+ clack2.note(
1764
+ "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1765
+ "Per-package conventions"
1766
+ );
1445
1767
  }
1446
1768
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1447
1769
  `);
1448
1770
  writeGeneratedFiles(projectRoot, config, scanResult);
1449
1771
  updateGitignore(projectRoot);
1450
- console.log(`
1451
- ${chalk9.bold("Created:")}`);
1452
- console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE4}`);
1453
- console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
1454
- console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
1772
+ const createdFiles = [
1773
+ CONFIG_FILE4,
1774
+ ".viberails/context.md",
1775
+ ".viberails/scan-result.json"
1776
+ ];
1455
1777
  if (integrations.preCommitHook) {
1456
1778
  setupPreCommitHook(projectRoot);
1779
+ const hookMgr = detectHookManager(projectRoot);
1780
+ if (hookMgr) {
1781
+ createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1782
+ }
1457
1783
  }
1458
1784
  if (integrations.claudeCodeHook) {
1459
1785
  setupClaudeCodeHook(projectRoot);
1786
+ createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1460
1787
  }
1461
- const filesToCommit = [
1462
- `${chalk9.cyan("viberails.config.json")}`,
1463
- chalk9.cyan(".viberails/context.md")
1464
- ];
1465
- if (integrations.claudeCodeHook) {
1466
- filesToCommit.push(chalk9.cyan(".claude/settings.json"));
1788
+ if (integrations.claudeMdRef) {
1789
+ setupClaudeMdReference(projectRoot);
1790
+ createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
1467
1791
  }
1468
- console.log(`
1469
- ${chalk9.bold("Next steps:")}`);
1470
- console.log(` 1. Review ${chalk9.cyan("viberails.config.json")} and adjust rules`);
1471
- console.log(` 2. Commit ${filesToCommit.join(", ")}`);
1472
- console.log(` 3. Run ${chalk9.cyan("viberails check")} to verify your project passes`);
1792
+ clack2.log.success(`Created:
1793
+ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1794
+ clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
1473
1795
  }
1474
1796
  function updateGitignore(projectRoot) {
1475
1797
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1490,8 +1812,146 @@ import * as fs13 from "fs";
1490
1812
  import * as path14 from "path";
1491
1813
  import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
1492
1814
  import { scan as scan2 } from "@viberails/scanner";
1493
- import chalk10 from "chalk";
1815
+ import chalk9 from "chalk";
1816
+
1817
+ // src/utils/diff-configs.ts
1818
+ import { CONVENTION_LABELS as CONVENTION_LABELS3, FRAMEWORK_NAMES as FRAMEWORK_NAMES4, ORM_NAMES as ORM_NAMES3, STYLING_NAMES as STYLING_NAMES4 } from "@viberails/types";
1819
+ function parseStackString(s) {
1820
+ const atIdx = s.indexOf("@");
1821
+ if (atIdx > 0) {
1822
+ return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1823
+ }
1824
+ return { name: s };
1825
+ }
1826
+ function displayStackName(s) {
1827
+ const { name, version } = parseStackString(s);
1828
+ const allMaps = {
1829
+ ...FRAMEWORK_NAMES4,
1830
+ ...STYLING_NAMES4,
1831
+ ...ORM_NAMES3
1832
+ };
1833
+ const display = allMaps[name] ?? name;
1834
+ return version ? `${display} ${version}` : display;
1835
+ }
1836
+ function conventionStr(cv) {
1837
+ return typeof cv === "string" ? cv : cv.value;
1838
+ }
1839
+ function isDetected(cv) {
1840
+ return typeof cv !== "string" && cv._detected === true;
1841
+ }
1842
+ var STACK_FIELDS = [
1843
+ "framework",
1844
+ "styling",
1845
+ "backend",
1846
+ "orm",
1847
+ "linter",
1848
+ "formatter",
1849
+ "testRunner"
1850
+ ];
1851
+ var CONVENTION_KEYS = [
1852
+ "fileNaming",
1853
+ "componentNaming",
1854
+ "hookNaming",
1855
+ "importAlias"
1856
+ ];
1857
+ var STRUCTURE_FIELDS = [
1858
+ { key: "srcDir", label: "source directory" },
1859
+ { key: "pages", label: "pages directory" },
1860
+ { key: "components", label: "components directory" },
1861
+ { key: "hooks", label: "hooks directory" },
1862
+ { key: "utils", label: "utilities directory" },
1863
+ { key: "types", label: "types directory" },
1864
+ { key: "tests", label: "tests directory" },
1865
+ { key: "testPattern", label: "test pattern" }
1866
+ ];
1867
+ function diffConfigs(existing, merged) {
1868
+ const changes = [];
1869
+ for (const field of STACK_FIELDS) {
1870
+ const oldVal = existing.stack[field];
1871
+ const newVal = merged.stack[field];
1872
+ if (!oldVal && newVal) {
1873
+ changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
1874
+ } else if (oldVal && newVal && oldVal !== newVal) {
1875
+ changes.push({
1876
+ type: "changed",
1877
+ description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1878
+ });
1879
+ }
1880
+ }
1881
+ for (const key of CONVENTION_KEYS) {
1882
+ const oldVal = existing.conventions[key];
1883
+ const newVal = merged.conventions[key];
1884
+ const label = CONVENTION_LABELS3[key] ?? key;
1885
+ if (!oldVal && newVal) {
1886
+ changes.push({
1887
+ type: "added",
1888
+ description: `New convention: ${label} (${conventionStr(newVal)})`
1889
+ });
1890
+ } else if (oldVal && newVal && isDetected(newVal)) {
1891
+ changes.push({
1892
+ type: "changed",
1893
+ description: `Convention updated: ${label} (${conventionStr(newVal)})`
1894
+ });
1895
+ }
1896
+ }
1897
+ for (const { key, label } of STRUCTURE_FIELDS) {
1898
+ const oldVal = existing.structure[key];
1899
+ const newVal = merged.structure[key];
1900
+ if (!oldVal && newVal) {
1901
+ changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
1902
+ }
1903
+ }
1904
+ const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
1905
+ for (const pkg of merged.packages ?? []) {
1906
+ if (!existingPaths.has(pkg.path)) {
1907
+ changes.push({ type: "added", description: `New package: ${pkg.path}` });
1908
+ }
1909
+ }
1910
+ const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
1911
+ const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
1912
+ for (const pkg of mergedWsPkgs) {
1913
+ if (!existingWsPkgs.has(pkg)) {
1914
+ changes.push({ type: "added", description: `Workspace: added ${pkg}` });
1915
+ }
1916
+ }
1917
+ for (const pkg of existingWsPkgs) {
1918
+ if (!mergedWsPkgs.has(pkg)) {
1919
+ changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
1920
+ }
1921
+ }
1922
+ return changes;
1923
+ }
1924
+ function formatStatsDelta(oldStats, newStats) {
1925
+ const fileDelta = newStats.totalFiles - oldStats.totalFiles;
1926
+ const lineDelta = newStats.totalLines - oldStats.totalLines;
1927
+ if (fileDelta === 0 && lineDelta === 0) return void 0;
1928
+ const parts = [];
1929
+ if (fileDelta !== 0) {
1930
+ const sign = fileDelta > 0 ? "+" : "";
1931
+ parts.push(`${sign}${fileDelta.toLocaleString()} files`);
1932
+ }
1933
+ if (lineDelta !== 0) {
1934
+ const sign = lineDelta > 0 ? "+" : "";
1935
+ parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
1936
+ }
1937
+ return `${parts.join(", ")} since last sync`;
1938
+ }
1939
+
1940
+ // src/commands/sync.ts
1494
1941
  var CONFIG_FILE5 = "viberails.config.json";
1942
+ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
1943
+ function loadPreviousStats(projectRoot) {
1944
+ const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
1945
+ try {
1946
+ const raw = fs13.readFileSync(scanResultPath, "utf-8");
1947
+ const parsed = JSON.parse(raw);
1948
+ if (parsed?.statistics?.totalFiles !== void 0) {
1949
+ return parsed.statistics;
1950
+ }
1951
+ } catch {
1952
+ }
1953
+ return void 0;
1954
+ }
1495
1955
  async function syncCommand(cwd) {
1496
1956
  const startDir = cwd ?? process.cwd();
1497
1957
  const projectRoot = findProjectRoot(startDir);
@@ -1502,41 +1962,50 @@ async function syncCommand(cwd) {
1502
1962
  }
1503
1963
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1504
1964
  const existing = await loadConfig4(configPath);
1505
- console.log(chalk10.dim("Scanning project..."));
1965
+ const previousStats = loadPreviousStats(projectRoot);
1966
+ console.log(chalk9.dim("Scanning project..."));
1506
1967
  const scanResult = await scan2(projectRoot);
1507
1968
  const merged = mergeConfig(existing, scanResult);
1508
1969
  const existingJson = JSON.stringify(existing, null, 2);
1509
1970
  const mergedJson = JSON.stringify(merged, null, 2);
1510
1971
  const configChanged = existingJson !== mergedJson;
1511
- if (configChanged) {
1512
- console.log(
1513
- ` ${chalk10.yellow("!")} Config updated \u2014 review ${chalk10.cyan(CONFIG_FILE5)} for changes`
1514
- );
1972
+ const changes = configChanged ? diffConfigs(existing, merged) : [];
1973
+ const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
1974
+ if (changes.length > 0 || statsDelta) {
1975
+ console.log(`
1976
+ ${chalk9.bold("Changes:")}`);
1977
+ for (const change of changes) {
1978
+ const icon = change.type === "removed" ? chalk9.red("-") : chalk9.green("+");
1979
+ console.log(` ${icon} ${change.description}`);
1980
+ }
1981
+ if (statsDelta) {
1982
+ console.log(` ${chalk9.dim(statsDelta)}`);
1983
+ }
1515
1984
  }
1516
1985
  fs13.writeFileSync(configPath, `${mergedJson}
1517
1986
  `);
1518
1987
  writeGeneratedFiles(projectRoot, merged, scanResult);
1519
1988
  console.log(`
1520
- ${chalk10.bold("Synced:")}`);
1989
+ ${chalk9.bold("Synced:")}`);
1521
1990
  if (configChanged) {
1522
- console.log(` ${chalk10.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1991
+ console.log(` ${chalk9.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1523
1992
  } else {
1524
- console.log(` ${chalk10.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1993
+ console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1525
1994
  }
1526
- console.log(` ${chalk10.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1527
- console.log(` ${chalk10.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1995
+ console.log(` ${chalk9.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1996
+ console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1528
1997
  }
1529
1998
 
1530
1999
  // src/index.ts
1531
- var VERSION = "0.3.1";
2000
+ var VERSION = "0.3.3";
1532
2001
  var program = new Command();
1533
2002
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1534
- program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
2003
+ 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)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
1535
2004
  try {
1536
2005
  await initCommand(options);
1537
2006
  } catch (err) {
1538
2007
  const message = err instanceof Error ? err.message : String(err);
1539
- console.error(`${chalk11.red("Error:")} ${message}`);
2008
+ console.error(`${chalk10.red("Error:")} ${message}`);
1540
2009
  process.exit(1);
1541
2010
  }
1542
2011
  });
@@ -1545,7 +2014,7 @@ program.command("sync").description("Re-scan and update generated files").action
1545
2014
  await syncCommand();
1546
2015
  } catch (err) {
1547
2016
  const message = err instanceof Error ? err.message : String(err);
1548
- console.error(`${chalk11.red("Error:")} ${message}`);
2017
+ console.error(`${chalk10.red("Error:")} ${message}`);
1549
2018
  process.exit(1);
1550
2019
  }
1551
2020
  });
@@ -1560,7 +2029,7 @@ program.command("check").description("Check files against enforced rules").optio
1560
2029
  process.exit(exitCode);
1561
2030
  } catch (err) {
1562
2031
  const message = err instanceof Error ? err.message : String(err);
1563
- console.error(`${chalk11.red("Error:")} ${message}`);
2032
+ console.error(`${chalk10.red("Error:")} ${message}`);
1564
2033
  process.exit(1);
1565
2034
  }
1566
2035
  }
@@ -1571,7 +2040,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1571
2040
  process.exit(exitCode);
1572
2041
  } catch (err) {
1573
2042
  const message = err instanceof Error ? err.message : String(err);
1574
- console.error(`${chalk11.red("Error:")} ${message}`);
2043
+ console.error(`${chalk10.red("Error:")} ${message}`);
1575
2044
  process.exit(1);
1576
2045
  }
1577
2046
  });
@@ -1580,7 +2049,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1580
2049
  await boundariesCommand(options);
1581
2050
  } catch (err) {
1582
2051
  const message = err instanceof Error ? err.message : String(err);
1583
- console.error(`${chalk11.red("Error:")} ${message}`);
2052
+ console.error(`${chalk10.red("Error:")} ${message}`);
1584
2053
  process.exit(1);
1585
2054
  }
1586
2055
  });