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.cjs CHANGED
@@ -34,14 +34,14 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk11 = __toESM(require("chalk"), 1);
37
+ var import_chalk10 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
41
41
  var fs3 = __toESM(require("fs"), 1);
42
42
  var path3 = __toESM(require("path"), 1);
43
43
  var import_config = require("@viberails/config");
44
- var import_chalk2 = __toESM(require("chalk"), 1);
44
+ var import_chalk = __toESM(require("chalk"), 1);
45
45
 
46
46
  // src/utils/find-project-root.ts
47
47
  var fs = __toESM(require("fs"), 1);
@@ -61,43 +61,110 @@ function findProjectRoot(startDir) {
61
61
  }
62
62
 
63
63
  // src/utils/prompt.ts
64
- var readline = __toESM(require("readline"), 1);
65
- var import_chalk = __toESM(require("chalk"), 1);
66
- async function confirm(message) {
67
- const rl = readline.createInterface({
68
- input: process.stdin,
69
- output: process.stdout
70
- });
71
- return new Promise((resolve4) => {
72
- rl.question(`${message} (Y/n) `, (answer) => {
73
- rl.close();
74
- const trimmed = answer.trim().toLowerCase();
75
- resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
76
- });
64
+ var clack = __toESM(require("@clack/prompts"), 1);
65
+ function assertNotCancelled(value) {
66
+ if (clack.isCancel(value)) {
67
+ clack.cancel("Setup cancelled.");
68
+ process.exit(0);
69
+ }
70
+ }
71
+ async function confirm2(message) {
72
+ const result = await clack.confirm({ message, initialValue: true });
73
+ assertNotCancelled(result);
74
+ return result;
75
+ }
76
+ async function confirmDangerous(message) {
77
+ const result = await clack.confirm({ message, initialValue: false });
78
+ assertNotCancelled(result);
79
+ return result;
80
+ }
81
+ async function promptInitDecision() {
82
+ const result = await clack.select({
83
+ message: "Accept these settings?",
84
+ options: [
85
+ { value: "accept", label: "Yes, looks good", hint: "recommended" },
86
+ { value: "customize", label: "Let me customize" }
87
+ ]
77
88
  });
89
+ assertNotCancelled(result);
90
+ return result;
78
91
  }
79
- async function selectIntegrations(hookManager) {
80
- const result = { preCommitHook: true, claudeCodeHook: true };
81
- const hookLabel = hookManager ? `Pre-commit hook (detected: ${hookManager})` : "Pre-commit hook (git hook)";
82
- console.log("Set up integrations:");
83
- const rl = readline.createInterface({
84
- input: process.stdin,
85
- output: process.stdout
92
+ async function promptRuleCustomization(defaults) {
93
+ const maxFileLinesResult = await clack.text({
94
+ message: "Maximum lines per source file?",
95
+ placeholder: String(defaults.maxFileLines),
96
+ initialValue: String(defaults.maxFileLines),
97
+ validate: (v) => {
98
+ const n = Number.parseInt(v, 10);
99
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
100
+ }
86
101
  });
87
- const askYn = (label, defaultYes) => new Promise((resolve4) => {
88
- const hint = defaultYes ? "Y/n" : "y/N";
89
- rl.question(` ${label}? (${hint}) `, (answer) => {
90
- const trimmed = answer.trim().toLowerCase();
91
- if (trimmed === "") resolve4(defaultYes);
92
- else resolve4(trimmed === "y" || trimmed === "yes");
93
- });
102
+ assertNotCancelled(maxFileLinesResult);
103
+ const requireTestsResult = await clack.confirm({
104
+ message: "Require matching test files for source files?",
105
+ initialValue: defaults.requireTests
94
106
  });
95
- console.log(` ${import_chalk.default.dim("Runs viberails check automatically when you commit")}`);
96
- result.preCommitHook = await askYn(hookLabel, true);
97
- console.log(` ${import_chalk.default.dim("Checks files against your rules when Claude edits them")}`);
98
- result.claudeCodeHook = await askYn("Claude Code hook", true);
99
- rl.close();
100
- return result;
107
+ assertNotCancelled(requireTestsResult);
108
+ const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
109
+ const enforceNamingResult = await clack.confirm({
110
+ message: namingLabel,
111
+ initialValue: defaults.enforceNaming
112
+ });
113
+ assertNotCancelled(enforceNamingResult);
114
+ const enforcementResult = await clack.select({
115
+ message: "Enforcement mode",
116
+ options: [
117
+ {
118
+ value: "warn",
119
+ label: "warn",
120
+ hint: "show violations but don't block commits (recommended)"
121
+ },
122
+ {
123
+ value: "enforce",
124
+ label: "enforce",
125
+ hint: "block commits with violations"
126
+ }
127
+ ],
128
+ initialValue: defaults.enforcement
129
+ });
130
+ assertNotCancelled(enforcementResult);
131
+ return {
132
+ maxFileLines: Number.parseInt(maxFileLinesResult, 10),
133
+ requireTests: requireTestsResult,
134
+ enforceNaming: enforceNamingResult,
135
+ enforcement: enforcementResult
136
+ };
137
+ }
138
+ async function promptIntegrations(hookManager) {
139
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
140
+ const result = await clack.multiselect({
141
+ message: "Set up integrations?",
142
+ options: [
143
+ {
144
+ value: "preCommit",
145
+ label: hookLabel,
146
+ hint: "runs checks when you commit"
147
+ },
148
+ {
149
+ value: "claude",
150
+ label: "Claude Code hook",
151
+ hint: "checks files when Claude edits them"
152
+ },
153
+ {
154
+ value: "claudeMd",
155
+ label: "CLAUDE.md reference",
156
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
157
+ }
158
+ ],
159
+ initialValues: ["preCommit", "claude", "claudeMd"],
160
+ required: false
161
+ });
162
+ assertNotCancelled(result);
163
+ return {
164
+ preCommitHook: result.includes("preCommit"),
165
+ claudeCodeHook: result.includes("claude"),
166
+ claudeMdRef: result.includes("claudeMd")
167
+ };
101
168
  }
102
169
 
103
170
  // src/utils/resolve-workspace-packages.ts
@@ -154,67 +221,65 @@ async function boundariesCommand(options, cwd) {
154
221
  displayRules(config);
155
222
  }
156
223
  function displayRules(config) {
157
- if (!config.boundaries || config.boundaries.length === 0) {
158
- console.log(import_chalk2.default.yellow("No boundary rules configured."));
159
- console.log(`Run ${import_chalk2.default.cyan("viberails boundaries --infer")} to generate rules.`);
224
+ if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
225
+ console.log(import_chalk.default.yellow("No boundary rules configured."));
226
+ console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
160
227
  return;
161
228
  }
162
- const allowRules = config.boundaries.filter((r) => r.allow);
163
- const denyRules = config.boundaries.filter((r) => !r.allow);
229
+ const { deny } = config.boundaries;
230
+ const sources = Object.keys(deny).filter((k) => deny[k].length > 0);
231
+ const totalRules = sources.reduce((sum, k) => sum + deny[k].length, 0);
164
232
  console.log(`
165
- ${import_chalk2.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
233
+ ${import_chalk.default.bold(`Boundary rules (${totalRules} deny rules):`)}
166
234
  `);
167
- for (const r of allowRules) {
168
- console.log(` ${import_chalk2.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
169
- }
170
- for (const r of denyRules) {
171
- const reason = r.reason ? import_chalk2.default.dim(` (${r.reason})`) : "";
172
- console.log(` ${import_chalk2.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
235
+ for (const source of sources) {
236
+ for (const target of deny[source]) {
237
+ console.log(` ${import_chalk.default.red("\u2717")} ${source} \u2192 ${target}`);
238
+ }
173
239
  }
174
240
  console.log(
175
241
  `
176
- Enforcement: ${config.rules.enforceBoundaries ? import_chalk2.default.green("on") : import_chalk2.default.yellow("off")}`
242
+ Enforcement: ${config.rules.enforceBoundaries ? import_chalk.default.green("on") : import_chalk.default.yellow("off")}`
177
243
  );
178
244
  }
179
245
  async function inferAndDisplay(projectRoot, config, configPath) {
180
- console.log(import_chalk2.default.dim("Analyzing imports..."));
246
+ console.log(import_chalk.default.dim("Analyzing imports..."));
181
247
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
182
248
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
183
249
  const graph = await buildImportGraph(projectRoot, {
184
250
  packages,
185
251
  ignore: config.ignore
186
252
  });
187
- console.log(import_chalk2.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
253
+ console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
188
254
  const inferred = inferBoundaries(graph);
189
- if (inferred.length === 0) {
190
- console.log(import_chalk2.default.yellow("No boundary rules could be inferred."));
255
+ const sources = Object.keys(inferred.deny).filter((k) => inferred.deny[k].length > 0);
256
+ const totalRules = sources.reduce((sum, k) => sum + inferred.deny[k].length, 0);
257
+ if (totalRules === 0) {
258
+ console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
191
259
  return;
192
260
  }
193
- const allow = inferred.filter((r) => r.allow);
194
- const deny = inferred.filter((r) => !r.allow);
195
261
  console.log(`
196
- ${import_chalk2.default.bold("Inferred boundary rules:")}
262
+ ${import_chalk.default.bold("Inferred boundary rules:")}
197
263
  `);
198
- for (const r of allow) {
199
- console.log(` ${import_chalk2.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
200
- }
201
- for (const r of deny) {
202
- const reason = r.reason ? import_chalk2.default.dim(` (${r.reason})`) : "";
203
- console.log(` ${import_chalk2.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
264
+ for (const source of sources) {
265
+ for (const target of inferred.deny[source]) {
266
+ console.log(` ${import_chalk.default.red("\u2717")} ${source} \u2192 ${target}`);
267
+ }
204
268
  }
205
269
  console.log(`
206
- ${allow.length} allowed, ${deny.length} denied`);
207
- const shouldSave = await confirm("\nSave to viberails.config.json?");
270
+ ${totalRules} denied`);
271
+ console.log("");
272
+ const shouldSave = await confirm2("Save to viberails.config.json?");
208
273
  if (shouldSave) {
209
274
  config.boundaries = inferred;
210
275
  config.rules.enforceBoundaries = true;
211
276
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
212
277
  `);
213
- console.log(`${import_chalk2.default.green("\u2713")} Saved ${inferred.length} rules`);
278
+ console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
214
279
  }
215
280
  }
216
281
  async function showGraph(projectRoot, config) {
217
- console.log(import_chalk2.default.dim("Building import graph..."));
282
+ console.log(import_chalk.default.dim("Building import graph..."));
218
283
  const { buildImportGraph } = await import("@viberails/graph");
219
284
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
220
285
  const graph = await buildImportGraph(projectRoot, {
@@ -222,20 +287,20 @@ async function showGraph(projectRoot, config) {
222
287
  ignore: config.ignore
223
288
  });
224
289
  console.log(`
225
- ${import_chalk2.default.bold("Import dependency graph:")}
290
+ ${import_chalk.default.bold("Import dependency graph:")}
226
291
  `);
227
292
  console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
228
293
  `);
229
294
  if (graph.packages.length > 0) {
230
295
  for (const pkg of graph.packages) {
231
296
  const deps = pkg.internalDeps.length > 0 ? `
232
- ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : import_chalk2.default.dim(" (no internal deps)");
297
+ ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : import_chalk.default.dim(" (no internal deps)");
233
298
  console.log(` ${pkg.name}${deps}`);
234
299
  }
235
300
  }
236
301
  if (graph.cycles.length > 0) {
237
302
  console.log(`
238
- ${import_chalk2.default.yellow("Cycles detected:")}`);
303
+ ${import_chalk.default.yellow("Cycles detected:")}`);
239
304
  for (const cycle of graph.cycles) {
240
305
  const paths = cycle.map((f) => path3.relative(projectRoot, f));
241
306
  console.log(` ${paths.join(" \u2192 ")}`);
@@ -247,7 +312,7 @@ ${import_chalk2.default.yellow("Cycles detected:")}`);
247
312
  var fs6 = __toESM(require("fs"), 1);
248
313
  var path6 = __toESM(require("path"), 1);
249
314
  var import_config2 = require("@viberails/config");
250
- var import_chalk3 = __toESM(require("chalk"), 1);
315
+ var import_chalk2 = __toESM(require("chalk"), 1);
251
316
 
252
317
  // src/commands/check-config.ts
253
318
  function resolveConfigForFile(relPath, config) {
@@ -485,12 +550,12 @@ function printGroupedViolations(violations, limit) {
485
550
  const toShow = group.slice(0, remaining);
486
551
  const hidden = group.length - toShow.length;
487
552
  for (const v of toShow) {
488
- const icon = v.severity === "error" ? import_chalk3.default.red("\u2717") : import_chalk3.default.yellow("!");
489
- console.log(`${icon} ${import_chalk3.default.dim(v.rule)} ${v.file}: ${v.message}`);
553
+ const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
554
+ console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
490
555
  }
491
556
  totalShown += toShow.length;
492
557
  if (hidden > 0) {
493
- console.log(import_chalk3.default.dim(` ... and ${hidden} more ${rule} violations`));
558
+ console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
494
559
  }
495
560
  }
496
561
  }
@@ -508,13 +573,13 @@ async function checkCommand(options, cwd) {
508
573
  const startDir = cwd ?? process.cwd();
509
574
  const projectRoot = findProjectRoot(startDir);
510
575
  if (!projectRoot) {
511
- console.error(`${import_chalk3.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
576
+ console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
512
577
  return 1;
513
578
  }
514
579
  const configPath = path6.join(projectRoot, CONFIG_FILE2);
515
580
  if (!fs6.existsSync(configPath)) {
516
581
  console.error(
517
- `${import_chalk3.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
582
+ `${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
518
583
  );
519
584
  return 1;
520
585
  }
@@ -528,7 +593,7 @@ async function checkCommand(options, cwd) {
528
593
  filesToCheck = getAllSourceFiles(projectRoot, config);
529
594
  }
530
595
  if (filesToCheck.length === 0) {
531
- console.log(`${import_chalk3.default.green("\u2713")} No files to check.`);
596
+ console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
532
597
  return 0;
533
598
  }
534
599
  const violations = [];
@@ -569,7 +634,7 @@ async function checkCommand(options, cwd) {
569
634
  const testViolations = checkMissingTests(projectRoot, config, severity);
570
635
  violations.push(...testViolations);
571
636
  }
572
- if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
637
+ if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
573
638
  const startTime = Date.now();
574
639
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
575
640
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -585,12 +650,12 @@ async function checkCommand(options, cwd) {
585
650
  violations.push({
586
651
  file: relFile,
587
652
  rule: "boundary-violation",
588
- message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}${bv.rule.reason ? ` (${bv.rule.reason})` : ""}`,
653
+ message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}`,
589
654
  severity
590
655
  });
591
656
  }
592
657
  const elapsed = Date.now() - startTime;
593
- console.log(import_chalk3.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
658
+ console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
594
659
  }
595
660
  if (options.format === "json") {
596
661
  console.log(
@@ -603,7 +668,7 @@ async function checkCommand(options, cwd) {
603
668
  return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
604
669
  }
605
670
  if (violations.length === 0) {
606
- console.log(`${import_chalk3.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
671
+ console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
607
672
  return 0;
608
673
  }
609
674
  if (!options.quiet) {
@@ -611,7 +676,7 @@ async function checkCommand(options, cwd) {
611
676
  }
612
677
  printSummary(violations);
613
678
  if (config.enforcement === "enforce") {
614
- console.log(import_chalk3.default.red("Fix violations before committing."));
679
+ console.log(import_chalk2.default.red("Fix violations before committing."));
615
680
  return 1;
616
681
  }
617
682
  return 0;
@@ -621,23 +686,22 @@ async function checkCommand(options, cwd) {
621
686
  var fs9 = __toESM(require("fs"), 1);
622
687
  var path10 = __toESM(require("path"), 1);
623
688
  var import_config3 = require("@viberails/config");
624
- var import_chalk5 = __toESM(require("chalk"), 1);
689
+ var import_chalk4 = __toESM(require("chalk"), 1);
625
690
 
626
691
  // src/commands/fix-helpers.ts
627
692
  var import_node_child_process2 = require("child_process");
628
- var import_node_readline = require("readline");
629
- var import_chalk4 = __toESM(require("chalk"), 1);
693
+ var import_chalk3 = __toESM(require("chalk"), 1);
630
694
  function printPlan(renames, stubs) {
631
695
  if (renames.length > 0) {
632
- console.log(import_chalk4.default.bold("\nFile renames:"));
696
+ console.log(import_chalk3.default.bold("\nFile renames:"));
633
697
  for (const r of renames) {
634
- console.log(` ${import_chalk4.default.red(r.oldPath)} \u2192 ${import_chalk4.default.green(r.newPath)}`);
698
+ console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
635
699
  }
636
700
  }
637
701
  if (stubs.length > 0) {
638
- console.log(import_chalk4.default.bold("\nTest stubs to create:"));
702
+ console.log(import_chalk3.default.bold("\nTest stubs to create:"));
639
703
  for (const s of stubs) {
640
- console.log(` ${import_chalk4.default.green("+")} ${s.path}`);
704
+ console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
641
705
  }
642
706
  }
643
707
  }
@@ -659,15 +723,6 @@ function getConventionValue(convention) {
659
723
  }
660
724
  return void 0;
661
725
  }
662
- function promptConfirm(question) {
663
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
664
- return new Promise((resolve4) => {
665
- rl.question(`${question} (y/N) `, (answer) => {
666
- rl.close();
667
- resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
668
- });
669
- });
670
- }
671
726
 
672
727
  // src/commands/fix-imports.ts
673
728
  var path7 = __toESM(require("path"), 1);
@@ -888,13 +943,13 @@ async function fixCommand(options, cwd) {
888
943
  const startDir = cwd ?? process.cwd();
889
944
  const projectRoot = findProjectRoot(startDir);
890
945
  if (!projectRoot) {
891
- console.error(`${import_chalk5.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
946
+ console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
892
947
  return 1;
893
948
  }
894
949
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
895
950
  if (!fs9.existsSync(configPath)) {
896
951
  console.error(
897
- `${import_chalk5.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
952
+ `${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
898
953
  );
899
954
  return 1;
900
955
  }
@@ -903,7 +958,7 @@ async function fixCommand(options, cwd) {
903
958
  const isDirty = checkGitDirty(projectRoot);
904
959
  if (isDirty) {
905
960
  console.log(
906
- import_chalk5.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
961
+ import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
907
962
  );
908
963
  }
909
964
  }
@@ -933,16 +988,16 @@ async function fixCommand(options, cwd) {
933
988
  }
934
989
  }
935
990
  if (dedupedRenames.length === 0 && testStubs.length === 0) {
936
- console.log(`${import_chalk5.default.green("\u2713")} No fixable violations found.`);
991
+ console.log(`${import_chalk4.default.green("\u2713")} No fixable violations found.`);
937
992
  return 0;
938
993
  }
939
994
  printPlan(dedupedRenames, testStubs);
940
995
  if (options.dryRun) {
941
- console.log(import_chalk5.default.dim("\nDry run \u2014 no changes applied."));
996
+ console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
942
997
  return 0;
943
998
  }
944
999
  if (!options.yes) {
945
- const confirmed = await promptConfirm("Apply these fixes?");
1000
+ const confirmed = await confirmDangerous("Apply these fixes?");
946
1001
  if (!confirmed) {
947
1002
  console.log("Aborted.");
948
1003
  return 0;
@@ -969,15 +1024,15 @@ async function fixCommand(options, cwd) {
969
1024
  }
970
1025
  console.log("");
971
1026
  if (renameCount > 0) {
972
- console.log(`${import_chalk5.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
1027
+ console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
973
1028
  }
974
1029
  if (importUpdateCount > 0) {
975
1030
  console.log(
976
- `${import_chalk5.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
1031
+ `${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
977
1032
  );
978
1033
  }
979
1034
  if (stubCount > 0) {
980
- console.log(`${import_chalk5.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
1035
+ console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
981
1036
  }
982
1037
  return 0;
983
1038
  }
@@ -985,13 +1040,13 @@ async function fixCommand(options, cwd) {
985
1040
  // src/commands/init.ts
986
1041
  var fs12 = __toESM(require("fs"), 1);
987
1042
  var path13 = __toESM(require("path"), 1);
1043
+ var clack2 = __toESM(require("@clack/prompts"), 1);
988
1044
  var import_config4 = require("@viberails/config");
989
1045
  var import_scanner = require("@viberails/scanner");
990
- var import_chalk9 = __toESM(require("chalk"), 1);
1046
+ var import_chalk8 = __toESM(require("chalk"), 1);
991
1047
 
992
- // src/display.ts
993
- var import_types3 = require("@viberails/types");
994
- var import_chalk7 = __toESM(require("chalk"), 1);
1048
+ // src/display-text.ts
1049
+ var import_types4 = require("@viberails/types");
995
1050
 
996
1051
  // src/display-helpers.ts
997
1052
  var import_types = require("@viberails/types");
@@ -1042,9 +1097,13 @@ function formatRoleGroup(group) {
1042
1097
  return `${group.label} \u2014 ${dirs} (${files})`;
1043
1098
  }
1044
1099
 
1100
+ // src/display.ts
1101
+ var import_types3 = require("@viberails/types");
1102
+ var import_chalk6 = __toESM(require("chalk"), 1);
1103
+
1045
1104
  // src/display-monorepo.ts
1046
1105
  var import_types2 = require("@viberails/types");
1047
- var import_chalk6 = __toESM(require("chalk"), 1);
1106
+ var import_chalk5 = __toESM(require("chalk"), 1);
1048
1107
  function formatPackageSummary(pkg) {
1049
1108
  const parts = [];
1050
1109
  if (pkg.stack.framework) {
@@ -1060,19 +1119,19 @@ function formatPackageSummary(pkg) {
1060
1119
  function displayMonorepoResults(scanResult) {
1061
1120
  const { stack, packages } = scanResult;
1062
1121
  console.log(`
1063
- ${import_chalk6.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1064
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1122
+ ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1123
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
1065
1124
  if (stack.packageManager) {
1066
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1125
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1067
1126
  }
1068
1127
  if (stack.linter) {
1069
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1128
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
1070
1129
  }
1071
1130
  if (stack.formatter) {
1072
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1131
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1073
1132
  }
1074
1133
  if (stack.testRunner) {
1075
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1134
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1076
1135
  }
1077
1136
  console.log("");
1078
1137
  for (const pkg of packages) {
@@ -1083,13 +1142,13 @@ ${import_chalk6.default.bold(`Detected: (monorepo, ${packages.length} packages)`
1083
1142
  );
1084
1143
  if (packagesWithDirs.length > 0) {
1085
1144
  console.log(`
1086
- ${import_chalk6.default.bold("Structure:")}`);
1145
+ ${import_chalk5.default.bold("Structure:")}`);
1087
1146
  for (const pkg of packagesWithDirs) {
1088
1147
  const groups = groupByRole(pkg.structure.directories);
1089
1148
  if (groups.length === 0) continue;
1090
1149
  console.log(` ${pkg.relativePath}:`);
1091
1150
  for (const group of groups) {
1092
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1151
+ console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
1093
1152
  }
1094
1153
  }
1095
1154
  }
@@ -1097,14 +1156,60 @@ ${import_chalk6.default.bold("Structure:")}`);
1097
1156
  displaySummarySection(scanResult);
1098
1157
  console.log("");
1099
1158
  }
1159
+ function formatPackageSummaryPlain(pkg) {
1160
+ const parts = [];
1161
+ if (pkg.stack.framework) {
1162
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1163
+ }
1164
+ if (pkg.stack.styling) {
1165
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1166
+ }
1167
+ const files = `${pkg.statistics.totalFiles} files`;
1168
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1169
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1170
+ }
1171
+ function formatMonorepoResultsText(scanResult, config) {
1172
+ const lines = [];
1173
+ const { stack, packages } = scanResult;
1174
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1175
+ const sharedParts = [formatItem(stack.language)];
1176
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1177
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1178
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1179
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1180
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1181
+ lines.push("");
1182
+ for (const pkg of packages) {
1183
+ lines.push(formatPackageSummaryPlain(pkg));
1184
+ }
1185
+ const packagesWithDirs = packages.filter(
1186
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1187
+ );
1188
+ if (packagesWithDirs.length > 0) {
1189
+ lines.push("");
1190
+ lines.push("Structure:");
1191
+ for (const pkg of packagesWithDirs) {
1192
+ const groups = groupByRole(pkg.structure.directories);
1193
+ if (groups.length === 0) continue;
1194
+ lines.push(` ${pkg.relativePath}:`);
1195
+ for (const group of groups) {
1196
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1197
+ }
1198
+ }
1199
+ }
1200
+ lines.push(...formatConventionsText(scanResult));
1201
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1202
+ lines.push("");
1203
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1204
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1205
+ if (ext) {
1206
+ lines.push(ext);
1207
+ }
1208
+ lines.push(...formatRulesText(config));
1209
+ return lines.join("\n");
1210
+ }
1100
1211
 
1101
1212
  // src/display.ts
1102
- var CONVENTION_LABELS = {
1103
- fileNaming: "File naming",
1104
- componentNaming: "Component naming",
1105
- hookNaming: "Hook naming",
1106
- importAlias: "Import alias"
1107
- };
1108
1213
  function formatItem(item, nameMap) {
1109
1214
  const name = nameMap?.[item.name] ?? item.name;
1110
1215
  return item.version ? `${name} ${item.version}` : name;
@@ -1120,27 +1225,27 @@ function displayConventions(scanResult) {
1120
1225
  const conventionEntries = Object.entries(scanResult.conventions);
1121
1226
  if (conventionEntries.length === 0) return;
1122
1227
  console.log(`
1123
- ${import_chalk7.default.bold("Conventions:")}`);
1228
+ ${import_chalk6.default.bold("Conventions:")}`);
1124
1229
  for (const [key, convention] of conventionEntries) {
1125
1230
  if (convention.confidence === "low") continue;
1126
- const label = CONVENTION_LABELS[key] ?? key;
1231
+ const label = import_types3.CONVENTION_LABELS[key] ?? key;
1127
1232
  if (scanResult.packages.length > 1) {
1128
1233
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1129
1234
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1130
1235
  if (allSame || pkgValues.length <= 1) {
1131
- const ind = convention.confidence === "high" ? import_chalk7.default.green("\u2713") : import_chalk7.default.yellow("~");
1132
- const detail = import_chalk7.default.dim(`(${confidenceLabel(convention)})`);
1236
+ const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1237
+ const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1133
1238
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1134
1239
  } else {
1135
- console.log(` ${import_chalk7.default.yellow("~")} ${label}: varies by package`);
1240
+ console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
1136
1241
  for (const pv of pkgValues) {
1137
1242
  const pct = Math.round(pv.convention.consistency);
1138
1243
  console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1139
1244
  }
1140
1245
  }
1141
1246
  } else {
1142
- const ind = convention.confidence === "high" ? import_chalk7.default.green("\u2713") : import_chalk7.default.yellow("~");
1143
- const detail = import_chalk7.default.dim(`(${confidenceLabel(convention)})`);
1247
+ const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1248
+ const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1144
1249
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1145
1250
  }
1146
1251
  }
@@ -1148,7 +1253,7 @@ ${import_chalk7.default.bold("Conventions:")}`);
1148
1253
  function displaySummarySection(scanResult) {
1149
1254
  const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1150
1255
  console.log(`
1151
- ${import_chalk7.default.bold("Summary:")}`);
1256
+ ${import_chalk6.default.bold("Summary:")}`);
1152
1257
  console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1153
1258
  const ext = formatExtensions(scanResult.statistics.filesByExtension);
1154
1259
  if (ext) {
@@ -1162,43 +1267,43 @@ function displayScanResults(scanResult) {
1162
1267
  }
1163
1268
  const { stack } = scanResult;
1164
1269
  console.log(`
1165
- ${import_chalk7.default.bold("Detected:")}`);
1270
+ ${import_chalk6.default.bold("Detected:")}`);
1166
1271
  if (stack.framework) {
1167
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1272
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1168
1273
  }
1169
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.language)}`);
1274
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1170
1275
  if (stack.styling) {
1171
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1276
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1172
1277
  }
1173
1278
  if (stack.backend) {
1174
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1279
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1175
1280
  }
1176
1281
  if (stack.orm) {
1177
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1282
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1178
1283
  }
1179
1284
  if (stack.linter) {
1180
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.linter)}`);
1285
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1181
1286
  }
1182
1287
  if (stack.formatter) {
1183
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1288
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1184
1289
  }
1185
1290
  if (stack.testRunner) {
1186
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1291
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1187
1292
  }
1188
1293
  if (stack.packageManager) {
1189
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1294
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1190
1295
  }
1191
1296
  if (stack.libraries.length > 0) {
1192
1297
  for (const lib of stack.libraries) {
1193
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1298
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1194
1299
  }
1195
1300
  }
1196
1301
  const groups = groupByRole(scanResult.structure.directories);
1197
1302
  if (groups.length > 0) {
1198
1303
  console.log(`
1199
- ${import_chalk7.default.bold("Structure:")}`);
1304
+ ${import_chalk6.default.bold("Structure:")}`);
1200
1305
  for (const group of groups) {
1201
- console.log(` ${import_chalk7.default.green("\u2713")} ${formatRoleGroup(group)}`);
1306
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1202
1307
  }
1203
1308
  }
1204
1309
  displayConventions(scanResult);
@@ -1209,38 +1314,151 @@ function getConventionStr(cv) {
1209
1314
  return typeof cv === "string" ? cv : cv.value;
1210
1315
  }
1211
1316
  function displayRulesPreview(config) {
1212
- console.log(`${import_chalk7.default.bold("Rules:")}`);
1213
- console.log(` ${import_chalk7.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1317
+ console.log(`${import_chalk6.default.bold("Rules:")}`);
1318
+ console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1214
1319
  if (config.rules.requireTests && config.structure.testPattern) {
1215
1320
  console.log(
1216
- ` ${import_chalk7.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1321
+ ` ${import_chalk6.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1217
1322
  );
1218
1323
  } else if (config.rules.requireTests) {
1219
- console.log(` ${import_chalk7.default.dim("\u2022")} Require test files: yes`);
1324
+ console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: yes`);
1220
1325
  } else {
1221
- console.log(` ${import_chalk7.default.dim("\u2022")} Require test files: no`);
1326
+ console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: no`);
1222
1327
  }
1223
1328
  if (config.rules.enforceNaming && config.conventions.fileNaming) {
1224
1329
  console.log(
1225
- ` ${import_chalk7.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1330
+ ` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1226
1331
  );
1227
1332
  } else {
1228
- console.log(` ${import_chalk7.default.dim("\u2022")} Enforce file naming: no`);
1333
+ console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
1229
1334
  }
1230
1335
  console.log(
1231
- ` ${import_chalk7.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1336
+ ` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1232
1337
  );
1233
1338
  console.log("");
1234
1339
  if (config.enforcement === "enforce") {
1235
- console.log(`${import_chalk7.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
1340
+ console.log(`${import_chalk6.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
1236
1341
  } else {
1237
1342
  console.log(
1238
- `${import_chalk7.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1343
+ `${import_chalk6.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1239
1344
  );
1240
1345
  }
1241
1346
  console.log("");
1242
1347
  }
1243
1348
 
1349
+ // src/display-text.ts
1350
+ function getConventionStr2(cv) {
1351
+ return typeof cv === "string" ? cv : cv.value;
1352
+ }
1353
+ function plainConfidenceLabel(convention) {
1354
+ const pct = Math.round(convention.consistency);
1355
+ if (convention.confidence === "high") {
1356
+ return `${pct}%`;
1357
+ }
1358
+ return `${pct}%, suggested only`;
1359
+ }
1360
+ function formatConventionsText(scanResult) {
1361
+ const lines = [];
1362
+ const conventionEntries = Object.entries(scanResult.conventions);
1363
+ if (conventionEntries.length === 0) return lines;
1364
+ lines.push("");
1365
+ lines.push("Conventions:");
1366
+ for (const [key, convention] of conventionEntries) {
1367
+ if (convention.confidence === "low") continue;
1368
+ const label = import_types4.CONVENTION_LABELS[key] ?? key;
1369
+ if (scanResult.packages.length > 1) {
1370
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1371
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1372
+ if (allSame || pkgValues.length <= 1) {
1373
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1374
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1375
+ } else {
1376
+ lines.push(` ~ ${label}: varies by package`);
1377
+ for (const pv of pkgValues) {
1378
+ const pct = Math.round(pv.convention.consistency);
1379
+ lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1380
+ }
1381
+ }
1382
+ } else {
1383
+ const ind = convention.confidence === "high" ? "\u2713" : "~";
1384
+ lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
1385
+ }
1386
+ }
1387
+ return lines;
1388
+ }
1389
+ function formatRulesText(config) {
1390
+ const lines = [];
1391
+ lines.push("");
1392
+ lines.push("Rules:");
1393
+ lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
1394
+ if (config.rules.requireTests && config.structure.testPattern) {
1395
+ lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
1396
+ } else if (config.rules.requireTests) {
1397
+ lines.push(" \u2022 Require test files: yes");
1398
+ } else {
1399
+ lines.push(" \u2022 Require test files: no");
1400
+ }
1401
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1402
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
1403
+ } else {
1404
+ lines.push(" \u2022 Enforce file naming: no");
1405
+ }
1406
+ lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
1407
+ return lines;
1408
+ }
1409
+ function formatScanResultsText(scanResult, config) {
1410
+ if (scanResult.packages.length > 1) {
1411
+ return formatMonorepoResultsText(scanResult, config);
1412
+ }
1413
+ const lines = [];
1414
+ const { stack } = scanResult;
1415
+ lines.push("Detected:");
1416
+ if (stack.framework) {
1417
+ lines.push(` \u2713 ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
1418
+ }
1419
+ lines.push(` \u2713 ${formatItem(stack.language)}`);
1420
+ if (stack.styling) {
1421
+ lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
1422
+ }
1423
+ if (stack.backend) {
1424
+ lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
1425
+ }
1426
+ if (stack.orm) {
1427
+ lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
1428
+ }
1429
+ const secondaryParts = [];
1430
+ if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
1431
+ if (stack.linter) secondaryParts.push(formatItem(stack.linter));
1432
+ if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
1433
+ if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
1434
+ if (secondaryParts.length > 0) {
1435
+ lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
1436
+ }
1437
+ if (stack.libraries.length > 0) {
1438
+ for (const lib of stack.libraries) {
1439
+ lines.push(` \u2713 ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
1440
+ }
1441
+ }
1442
+ const groups = groupByRole(scanResult.structure.directories);
1443
+ if (groups.length > 0) {
1444
+ lines.push("");
1445
+ lines.push("Structure:");
1446
+ for (const group of groups) {
1447
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1448
+ }
1449
+ }
1450
+ lines.push(...formatConventionsText(scanResult));
1451
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1452
+ lines.push("");
1453
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1454
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1455
+ if (ext) {
1456
+ lines.push(ext);
1457
+ }
1458
+ lines.push(...formatRulesText(config));
1459
+ return lines.join("\n");
1460
+ }
1461
+
1244
1462
  // src/utils/write-generated-files.ts
1245
1463
  var fs10 = __toESM(require("fs"), 1);
1246
1464
  var path11 = __toESM(require("path"), 1);
@@ -1270,18 +1488,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1270
1488
  // src/commands/init-hooks.ts
1271
1489
  var fs11 = __toESM(require("fs"), 1);
1272
1490
  var path12 = __toESM(require("path"), 1);
1273
- var import_chalk8 = __toESM(require("chalk"), 1);
1491
+ var import_chalk7 = __toESM(require("chalk"), 1);
1274
1492
  function setupPreCommitHook(projectRoot) {
1275
1493
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1276
1494
  if (fs11.existsSync(lefthookPath)) {
1277
1495
  addLefthookPreCommit(lefthookPath);
1278
- console.log(` ${import_chalk8.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1496
+ console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1279
1497
  return;
1280
1498
  }
1281
1499
  const huskyDir = path12.join(projectRoot, ".husky");
1282
1500
  if (fs11.existsSync(huskyDir)) {
1283
1501
  writeHuskyPreCommit(huskyDir);
1284
- console.log(` ${import_chalk8.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1502
+ console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1285
1503
  return;
1286
1504
  }
1287
1505
  const gitDir = path12.join(projectRoot, ".git");
@@ -1291,7 +1509,7 @@ function setupPreCommitHook(projectRoot) {
1291
1509
  fs11.mkdirSync(hooksDir, { recursive: true });
1292
1510
  }
1293
1511
  writeGitHookPreCommit(hooksDir);
1294
- console.log(` ${import_chalk8.default.green("\u2713")} .git/hooks/pre-commit`);
1512
+ console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
1295
1513
  }
1296
1514
  }
1297
1515
  function writeGitHookPreCommit(hooksDir) {
@@ -1361,7 +1579,7 @@ function setupClaudeCodeHook(projectRoot) {
1361
1579
  settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1362
1580
  } catch {
1363
1581
  console.warn(
1364
- ` ${import_chalk8.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1582
+ ` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1365
1583
  );
1366
1584
  settings = {};
1367
1585
  }
@@ -1370,7 +1588,17 @@ function setupClaudeCodeHook(projectRoot) {
1370
1588
  const existing = hooks.PostToolUse ?? [];
1371
1589
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1372
1590
  const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1373
- const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1591
+ const checkAndReport = [
1592
+ `FILE=$(${extractFile})`,
1593
+ 'if [ -z "$FILE" ]; then exit 0; fi',
1594
+ 'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
1595
+ `if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
1596
+ ' echo "$OUTPUT" >&2',
1597
+ " exit 2",
1598
+ "fi",
1599
+ "exit 0"
1600
+ ].join("\n");
1601
+ const hookCommand = checkAndReport;
1374
1602
  hooks.PostToolUse = [
1375
1603
  ...existing,
1376
1604
  {
@@ -1386,7 +1614,19 @@ function setupClaudeCodeHook(projectRoot) {
1386
1614
  settings.hooks = hooks;
1387
1615
  fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1388
1616
  `);
1389
- console.log(` ${import_chalk8.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1617
+ console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1618
+ }
1619
+ function setupClaudeMdReference(projectRoot) {
1620
+ const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
1621
+ let content = "";
1622
+ if (fs11.existsSync(claudeMdPath)) {
1623
+ content = fs11.readFileSync(claudeMdPath, "utf-8");
1624
+ }
1625
+ if (content.includes("@.viberails/context.md")) return;
1626
+ const ref = "\n@.viberails/context.md\n";
1627
+ const prefix = content.length === 0 ? "" : content.trimEnd();
1628
+ fs11.writeFileSync(claudeMdPath, prefix + ref);
1629
+ console.log(` ${import_chalk7.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
1390
1630
  }
1391
1631
  function writeHuskyPreCommit(huskyDir) {
1392
1632
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1416,6 +1656,14 @@ function filterHighConfidence(conventions) {
1416
1656
  }
1417
1657
  return filtered;
1418
1658
  }
1659
+ function getConventionStr3(cv) {
1660
+ if (!cv) return void 0;
1661
+ return typeof cv === "string" ? cv : cv.value;
1662
+ }
1663
+ function hasConventionOverrides(config) {
1664
+ if (!config.packages || config.packages.length === 0) return false;
1665
+ return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1666
+ }
1419
1667
  async function initCommand(options, cwd) {
1420
1668
  const startDir = cwd ?? process.cwd();
1421
1669
  const projectRoot = findProjectRoot(startDir);
@@ -1425,84 +1673,146 @@ async function initCommand(options, cwd) {
1425
1673
  );
1426
1674
  }
1427
1675
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1428
- if (fs12.existsSync(configPath)) {
1676
+ if (fs12.existsSync(configPath) && !options.force) {
1429
1677
  console.log(
1430
- import_chalk9.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk9.default.cyan("viberails sync") + " to update the generated files."
1678
+ `${import_chalk8.default.yellow("!")} viberails is already initialized.
1679
+ Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
1431
1680
  );
1432
1681
  return;
1433
1682
  }
1434
- console.log(import_chalk9.default.dim("Scanning project..."));
1435
- const scanResult = await (0, import_scanner.scan)(projectRoot);
1436
- const config = (0, import_config4.generateConfig)(scanResult);
1437
1683
  if (options.yes) {
1438
- config.conventions = filterHighConfidence(config.conventions);
1684
+ console.log(import_chalk8.default.dim("Scanning project..."));
1685
+ const scanResult2 = await (0, import_scanner.scan)(projectRoot);
1686
+ const config2 = (0, import_config4.generateConfig)(scanResult2);
1687
+ config2.conventions = filterHighConfidence(config2.conventions);
1688
+ displayScanResults(scanResult2);
1689
+ displayRulesPreview(config2);
1690
+ if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
1691
+ console.log(import_chalk8.default.dim("Building import graph..."));
1692
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1693
+ const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
1694
+ const graph = await buildImportGraph(projectRoot, {
1695
+ packages,
1696
+ ignore: config2.ignore
1697
+ });
1698
+ const inferred = inferBoundaries(graph);
1699
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1700
+ if (denyCount > 0) {
1701
+ config2.boundaries = inferred;
1702
+ config2.rules.enforceBoundaries = true;
1703
+ console.log(` Inferred ${denyCount} boundary rules`);
1704
+ }
1705
+ }
1706
+ fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1707
+ `);
1708
+ writeGeneratedFiles(projectRoot, config2, scanResult2);
1709
+ updateGitignore(projectRoot);
1710
+ setupClaudeMdReference(projectRoot);
1711
+ console.log(`
1712
+ Created:`);
1713
+ console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
1714
+ console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
1715
+ console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
1716
+ return;
1439
1717
  }
1440
- displayScanResults(scanResult);
1718
+ clack2.intro("viberails");
1719
+ const s = clack2.spinner();
1720
+ s.start("Scanning project...");
1721
+ const scanResult = await (0, import_scanner.scan)(projectRoot);
1722
+ const config = (0, import_config4.generateConfig)(scanResult);
1723
+ s.stop("Scan complete");
1441
1724
  if (scanResult.statistics.totalFiles === 0) {
1442
- console.log(
1443
- import_chalk9.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk9.default.cyan("viberails sync") + " after adding source files.\n"
1725
+ clack2.log.warn(
1726
+ "No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
1444
1727
  );
1445
1728
  }
1446
- displayRulesPreview(config);
1447
- if (!options.yes) {
1448
- const accepted = await confirm("Proceed with these settings?");
1449
- if (!accepted) {
1450
- console.log("Aborted.");
1451
- return;
1729
+ const resultsText = formatScanResultsText(scanResult, config);
1730
+ clack2.note(resultsText, "Scan results");
1731
+ const decision = await promptInitDecision();
1732
+ if (decision === "customize") {
1733
+ clack2.note(
1734
+ "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1735
+ "Rules"
1736
+ );
1737
+ const overrides = await promptRuleCustomization({
1738
+ maxFileLines: config.rules.maxFileLines,
1739
+ requireTests: config.rules.requireTests,
1740
+ enforceNaming: config.rules.enforceNaming,
1741
+ enforcement: config.enforcement,
1742
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1743
+ });
1744
+ config.rules.maxFileLines = overrides.maxFileLines;
1745
+ config.rules.requireTests = overrides.requireTests;
1746
+ config.rules.enforceNaming = overrides.enforceNaming;
1747
+ config.enforcement = overrides.enforcement;
1748
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1749
+ clack2.note(
1750
+ 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1751
+ "Per-package overrides"
1752
+ );
1452
1753
  }
1453
1754
  }
1454
- if (config.workspace && config.workspace.packages.length > 0) {
1455
- let shouldInfer = options.yes;
1456
- if (!options.yes) {
1457
- console.log(import_chalk9.default.dim(" Scans imports between packages to suggest dependency rules"));
1458
- shouldInfer = await confirm("Infer boundary rules from import patterns?");
1459
- }
1755
+ if (config.workspace?.packages && config.workspace.packages.length > 0) {
1756
+ clack2.note(
1757
+ "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
1758
+ "Boundaries"
1759
+ );
1760
+ const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
1460
1761
  if (shouldInfer) {
1461
- console.log(import_chalk9.default.dim("Building import graph..."));
1762
+ const bs = clack2.spinner();
1763
+ bs.start("Building import graph...");
1462
1764
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1463
1765
  const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1464
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1766
+ const graph = await buildImportGraph(projectRoot, {
1767
+ packages,
1768
+ ignore: config.ignore
1769
+ });
1465
1770
  const inferred = inferBoundaries(graph);
1466
- if (inferred.length > 0) {
1771
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1772
+ if (denyCount > 0) {
1467
1773
  config.boundaries = inferred;
1468
1774
  config.rules.enforceBoundaries = true;
1469
- console.log(` ${import_chalk9.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1775
+ bs.stop(`Inferred ${denyCount} boundary rules`);
1776
+ } else {
1777
+ bs.stop("No boundary rules inferred");
1470
1778
  }
1471
1779
  }
1472
1780
  }
1473
1781
  const hookManager = detectHookManager(projectRoot);
1474
- let integrations = { preCommitHook: true, claudeCodeHook: true };
1475
- if (!options.yes) {
1476
- console.log("");
1477
- integrations = await selectIntegrations(hookManager);
1782
+ const integrations = await promptIntegrations(hookManager);
1783
+ if (hasConventionOverrides(config)) {
1784
+ clack2.note(
1785
+ "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1786
+ "Per-package conventions"
1787
+ );
1478
1788
  }
1479
1789
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1480
1790
  `);
1481
1791
  writeGeneratedFiles(projectRoot, config, scanResult);
1482
1792
  updateGitignore(projectRoot);
1483
- console.log(`
1484
- ${import_chalk9.default.bold("Created:")}`);
1485
- console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE4}`);
1486
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
1487
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
1793
+ const createdFiles = [
1794
+ CONFIG_FILE4,
1795
+ ".viberails/context.md",
1796
+ ".viberails/scan-result.json"
1797
+ ];
1488
1798
  if (integrations.preCommitHook) {
1489
1799
  setupPreCommitHook(projectRoot);
1800
+ const hookMgr = detectHookManager(projectRoot);
1801
+ if (hookMgr) {
1802
+ createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1803
+ }
1490
1804
  }
1491
1805
  if (integrations.claudeCodeHook) {
1492
1806
  setupClaudeCodeHook(projectRoot);
1807
+ createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1493
1808
  }
1494
- const filesToCommit = [
1495
- `${import_chalk9.default.cyan("viberails.config.json")}`,
1496
- import_chalk9.default.cyan(".viberails/context.md")
1497
- ];
1498
- if (integrations.claudeCodeHook) {
1499
- filesToCommit.push(import_chalk9.default.cyan(".claude/settings.json"));
1809
+ if (integrations.claudeMdRef) {
1810
+ setupClaudeMdReference(projectRoot);
1811
+ createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
1500
1812
  }
1501
- console.log(`
1502
- ${import_chalk9.default.bold("Next steps:")}`);
1503
- console.log(` 1. Review ${import_chalk9.default.cyan("viberails.config.json")} and adjust rules`);
1504
- console.log(` 2. Commit ${filesToCommit.join(", ")}`);
1505
- console.log(` 3. Run ${import_chalk9.default.cyan("viberails check")} to verify your project passes`);
1813
+ clack2.log.success(`Created:
1814
+ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1815
+ clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
1506
1816
  }
1507
1817
  function updateGitignore(projectRoot) {
1508
1818
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1523,8 +1833,146 @@ var fs13 = __toESM(require("fs"), 1);
1523
1833
  var path14 = __toESM(require("path"), 1);
1524
1834
  var import_config5 = require("@viberails/config");
1525
1835
  var import_scanner2 = require("@viberails/scanner");
1526
- var import_chalk10 = __toESM(require("chalk"), 1);
1836
+ var import_chalk9 = __toESM(require("chalk"), 1);
1837
+
1838
+ // src/utils/diff-configs.ts
1839
+ var import_types5 = require("@viberails/types");
1840
+ function parseStackString(s) {
1841
+ const atIdx = s.indexOf("@");
1842
+ if (atIdx > 0) {
1843
+ return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1844
+ }
1845
+ return { name: s };
1846
+ }
1847
+ function displayStackName(s) {
1848
+ const { name, version } = parseStackString(s);
1849
+ const allMaps = {
1850
+ ...import_types5.FRAMEWORK_NAMES,
1851
+ ...import_types5.STYLING_NAMES,
1852
+ ...import_types5.ORM_NAMES
1853
+ };
1854
+ const display = allMaps[name] ?? name;
1855
+ return version ? `${display} ${version}` : display;
1856
+ }
1857
+ function conventionStr(cv) {
1858
+ return typeof cv === "string" ? cv : cv.value;
1859
+ }
1860
+ function isDetected(cv) {
1861
+ return typeof cv !== "string" && cv._detected === true;
1862
+ }
1863
+ var STACK_FIELDS = [
1864
+ "framework",
1865
+ "styling",
1866
+ "backend",
1867
+ "orm",
1868
+ "linter",
1869
+ "formatter",
1870
+ "testRunner"
1871
+ ];
1872
+ var CONVENTION_KEYS = [
1873
+ "fileNaming",
1874
+ "componentNaming",
1875
+ "hookNaming",
1876
+ "importAlias"
1877
+ ];
1878
+ var STRUCTURE_FIELDS = [
1879
+ { key: "srcDir", label: "source directory" },
1880
+ { key: "pages", label: "pages directory" },
1881
+ { key: "components", label: "components directory" },
1882
+ { key: "hooks", label: "hooks directory" },
1883
+ { key: "utils", label: "utilities directory" },
1884
+ { key: "types", label: "types directory" },
1885
+ { key: "tests", label: "tests directory" },
1886
+ { key: "testPattern", label: "test pattern" }
1887
+ ];
1888
+ function diffConfigs(existing, merged) {
1889
+ const changes = [];
1890
+ for (const field of STACK_FIELDS) {
1891
+ const oldVal = existing.stack[field];
1892
+ const newVal = merged.stack[field];
1893
+ if (!oldVal && newVal) {
1894
+ changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
1895
+ } else if (oldVal && newVal && oldVal !== newVal) {
1896
+ changes.push({
1897
+ type: "changed",
1898
+ description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1899
+ });
1900
+ }
1901
+ }
1902
+ for (const key of CONVENTION_KEYS) {
1903
+ const oldVal = existing.conventions[key];
1904
+ const newVal = merged.conventions[key];
1905
+ const label = import_types5.CONVENTION_LABELS[key] ?? key;
1906
+ if (!oldVal && newVal) {
1907
+ changes.push({
1908
+ type: "added",
1909
+ description: `New convention: ${label} (${conventionStr(newVal)})`
1910
+ });
1911
+ } else if (oldVal && newVal && isDetected(newVal)) {
1912
+ changes.push({
1913
+ type: "changed",
1914
+ description: `Convention updated: ${label} (${conventionStr(newVal)})`
1915
+ });
1916
+ }
1917
+ }
1918
+ for (const { key, label } of STRUCTURE_FIELDS) {
1919
+ const oldVal = existing.structure[key];
1920
+ const newVal = merged.structure[key];
1921
+ if (!oldVal && newVal) {
1922
+ changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
1923
+ }
1924
+ }
1925
+ const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
1926
+ for (const pkg of merged.packages ?? []) {
1927
+ if (!existingPaths.has(pkg.path)) {
1928
+ changes.push({ type: "added", description: `New package: ${pkg.path}` });
1929
+ }
1930
+ }
1931
+ const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
1932
+ const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
1933
+ for (const pkg of mergedWsPkgs) {
1934
+ if (!existingWsPkgs.has(pkg)) {
1935
+ changes.push({ type: "added", description: `Workspace: added ${pkg}` });
1936
+ }
1937
+ }
1938
+ for (const pkg of existingWsPkgs) {
1939
+ if (!mergedWsPkgs.has(pkg)) {
1940
+ changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
1941
+ }
1942
+ }
1943
+ return changes;
1944
+ }
1945
+ function formatStatsDelta(oldStats, newStats) {
1946
+ const fileDelta = newStats.totalFiles - oldStats.totalFiles;
1947
+ const lineDelta = newStats.totalLines - oldStats.totalLines;
1948
+ if (fileDelta === 0 && lineDelta === 0) return void 0;
1949
+ const parts = [];
1950
+ if (fileDelta !== 0) {
1951
+ const sign = fileDelta > 0 ? "+" : "";
1952
+ parts.push(`${sign}${fileDelta.toLocaleString()} files`);
1953
+ }
1954
+ if (lineDelta !== 0) {
1955
+ const sign = lineDelta > 0 ? "+" : "";
1956
+ parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
1957
+ }
1958
+ return `${parts.join(", ")} since last sync`;
1959
+ }
1960
+
1961
+ // src/commands/sync.ts
1527
1962
  var CONFIG_FILE5 = "viberails.config.json";
1963
+ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
1964
+ function loadPreviousStats(projectRoot) {
1965
+ const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
1966
+ try {
1967
+ const raw = fs13.readFileSync(scanResultPath, "utf-8");
1968
+ const parsed = JSON.parse(raw);
1969
+ if (parsed?.statistics?.totalFiles !== void 0) {
1970
+ return parsed.statistics;
1971
+ }
1972
+ } catch {
1973
+ }
1974
+ return void 0;
1975
+ }
1528
1976
  async function syncCommand(cwd) {
1529
1977
  const startDir = cwd ?? process.cwd();
1530
1978
  const projectRoot = findProjectRoot(startDir);
@@ -1535,41 +1983,50 @@ async function syncCommand(cwd) {
1535
1983
  }
1536
1984
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1537
1985
  const existing = await (0, import_config5.loadConfig)(configPath);
1538
- console.log(import_chalk10.default.dim("Scanning project..."));
1986
+ const previousStats = loadPreviousStats(projectRoot);
1987
+ console.log(import_chalk9.default.dim("Scanning project..."));
1539
1988
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
1540
1989
  const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1541
1990
  const existingJson = JSON.stringify(existing, null, 2);
1542
1991
  const mergedJson = JSON.stringify(merged, null, 2);
1543
1992
  const configChanged = existingJson !== mergedJson;
1544
- if (configChanged) {
1545
- console.log(
1546
- ` ${import_chalk10.default.yellow("!")} Config updated \u2014 review ${import_chalk10.default.cyan(CONFIG_FILE5)} for changes`
1547
- );
1993
+ const changes = configChanged ? diffConfigs(existing, merged) : [];
1994
+ const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
1995
+ if (changes.length > 0 || statsDelta) {
1996
+ console.log(`
1997
+ ${import_chalk9.default.bold("Changes:")}`);
1998
+ for (const change of changes) {
1999
+ const icon = change.type === "removed" ? import_chalk9.default.red("-") : import_chalk9.default.green("+");
2000
+ console.log(` ${icon} ${change.description}`);
2001
+ }
2002
+ if (statsDelta) {
2003
+ console.log(` ${import_chalk9.default.dim(statsDelta)}`);
2004
+ }
1548
2005
  }
1549
2006
  fs13.writeFileSync(configPath, `${mergedJson}
1550
2007
  `);
1551
2008
  writeGeneratedFiles(projectRoot, merged, scanResult);
1552
2009
  console.log(`
1553
- ${import_chalk10.default.bold("Synced:")}`);
2010
+ ${import_chalk9.default.bold("Synced:")}`);
1554
2011
  if (configChanged) {
1555
- console.log(` ${import_chalk10.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
2012
+ console.log(` ${import_chalk9.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1556
2013
  } else {
1557
- console.log(` ${import_chalk10.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
2014
+ console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1558
2015
  }
1559
- console.log(` ${import_chalk10.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1560
- console.log(` ${import_chalk10.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
2016
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
2017
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1561
2018
  }
1562
2019
 
1563
2020
  // src/index.ts
1564
- var VERSION = "0.3.1";
2021
+ var VERSION = "0.3.3";
1565
2022
  var program = new import_commander.Command();
1566
2023
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1567
- 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) => {
2024
+ 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) => {
1568
2025
  try {
1569
2026
  await initCommand(options);
1570
2027
  } catch (err) {
1571
2028
  const message = err instanceof Error ? err.message : String(err);
1572
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
2029
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1573
2030
  process.exit(1);
1574
2031
  }
1575
2032
  });
@@ -1578,7 +2035,7 @@ program.command("sync").description("Re-scan and update generated files").action
1578
2035
  await syncCommand();
1579
2036
  } catch (err) {
1580
2037
  const message = err instanceof Error ? err.message : String(err);
1581
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
2038
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1582
2039
  process.exit(1);
1583
2040
  }
1584
2041
  });
@@ -1593,7 +2050,7 @@ program.command("check").description("Check files against enforced rules").optio
1593
2050
  process.exit(exitCode);
1594
2051
  } catch (err) {
1595
2052
  const message = err instanceof Error ? err.message : String(err);
1596
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
2053
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1597
2054
  process.exit(1);
1598
2055
  }
1599
2056
  }
@@ -1604,7 +2061,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
1604
2061
  process.exit(exitCode);
1605
2062
  } catch (err) {
1606
2063
  const message = err instanceof Error ? err.message : String(err);
1607
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
2064
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1608
2065
  process.exit(1);
1609
2066
  }
1610
2067
  });
@@ -1613,7 +2070,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
1613
2070
  await boundariesCommand(options);
1614
2071
  } catch (err) {
1615
2072
  const message = err instanceof Error ? err.message : String(err);
1616
- console.error(`${import_chalk11.default.red("Error:")} ${message}`);
2073
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1617
2074
  process.exit(1);
1618
2075
  }
1619
2076
  });