viberails 0.3.2 → 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
@@ -116,15 +116,21 @@ async function promptIntegrations(hookManager) {
116
116
  value: "claude",
117
117
  label: "Claude Code hook",
118
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"
119
124
  }
120
125
  ],
121
- initialValues: ["preCommit", "claude"],
126
+ initialValues: ["preCommit", "claude", "claudeMd"],
122
127
  required: false
123
128
  });
124
129
  assertNotCancelled(result);
125
130
  return {
126
131
  preCommitHook: result.includes("preCommit"),
127
- claudeCodeHook: result.includes("claude")
132
+ claudeCodeHook: result.includes("claude"),
133
+ claudeMdRef: result.includes("claudeMd")
128
134
  };
129
135
  }
130
136
 
@@ -182,22 +188,21 @@ async function boundariesCommand(options, cwd) {
182
188
  displayRules(config);
183
189
  }
184
190
  function displayRules(config) {
185
- if (!config.boundaries || config.boundaries.length === 0) {
191
+ if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
186
192
  console.log(chalk.yellow("No boundary rules configured."));
187
193
  console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
188
194
  return;
189
195
  }
190
- const allowRules = config.boundaries.filter((r) => r.allow);
191
- 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);
192
199
  console.log(`
193
- ${chalk.bold(`Boundary rules (${config.boundaries.length} rules):`)}
200
+ ${chalk.bold(`Boundary rules (${totalRules} deny rules):`)}
194
201
  `);
195
- for (const r of allowRules) {
196
- console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
197
- }
198
- for (const r of denyRules) {
199
- const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
200
- console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
202
+ for (const source of sources) {
203
+ for (const target of deny[source]) {
204
+ console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
205
+ }
201
206
  }
202
207
  console.log(
203
208
  `
@@ -214,24 +219,22 @@ async function inferAndDisplay(projectRoot, config, configPath) {
214
219
  });
215
220
  console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
216
221
  const inferred = inferBoundaries(graph);
217
- if (inferred.length === 0) {
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) {
218
225
  console.log(chalk.yellow("No boundary rules could be inferred."));
219
226
  return;
220
227
  }
221
- const allow = inferred.filter((r) => r.allow);
222
- const deny = inferred.filter((r) => !r.allow);
223
228
  console.log(`
224
229
  ${chalk.bold("Inferred boundary rules:")}
225
230
  `);
226
- for (const r of allow) {
227
- console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
228
- }
229
- for (const r of deny) {
230
- const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
231
- console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
231
+ for (const source of sources) {
232
+ for (const target of inferred.deny[source]) {
233
+ console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
234
+ }
232
235
  }
233
236
  console.log(`
234
- ${allow.length} allowed, ${deny.length} denied`);
237
+ ${totalRules} denied`);
235
238
  console.log("");
236
239
  const shouldSave = await confirm2("Save to viberails.config.json?");
237
240
  if (shouldSave) {
@@ -239,7 +242,7 @@ ${chalk.bold("Inferred boundary rules:")}
239
242
  config.rules.enforceBoundaries = true;
240
243
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
241
244
  `);
242
- console.log(`${chalk.green("\u2713")} Saved ${inferred.length} rules`);
245
+ console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
243
246
  }
244
247
  }
245
248
  async function showGraph(projectRoot, config) {
@@ -598,7 +601,7 @@ async function checkCommand(options, cwd) {
598
601
  const testViolations = checkMissingTests(projectRoot, config, severity);
599
602
  violations.push(...testViolations);
600
603
  }
601
- 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) {
602
605
  const startTime = Date.now();
603
606
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
604
607
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -614,7 +617,7 @@ async function checkCommand(options, cwd) {
614
617
  violations.push({
615
618
  file: relFile,
616
619
  rule: "boundary-violation",
617
- 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}`,
618
621
  severity
619
622
  });
620
623
  }
@@ -1009,9 +1012,14 @@ import { generateConfig } from "@viberails/config";
1009
1012
  import { scan } from "@viberails/scanner";
1010
1013
  import chalk8 from "chalk";
1011
1014
 
1012
- // src/display.ts
1013
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, ORM_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
1014
- import chalk6 from "chalk";
1015
+ // 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";
1015
1023
 
1016
1024
  // src/display-helpers.ts
1017
1025
  import { ROLE_DESCRIPTIONS } from "@viberails/types";
@@ -1062,6 +1070,16 @@ function formatRoleGroup(group) {
1062
1070
  return `${group.label} \u2014 ${dirs} (${files})`;
1063
1071
  }
1064
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
+
1065
1083
  // src/display-monorepo.ts
1066
1084
  import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1067
1085
  import chalk5 from "chalk";
@@ -1171,12 +1189,6 @@ function formatMonorepoResultsText(scanResult, config) {
1171
1189
  }
1172
1190
 
1173
1191
  // src/display.ts
1174
- var CONVENTION_LABELS = {
1175
- fileNaming: "File naming",
1176
- componentNaming: "Component naming",
1177
- hookNaming: "Hook naming",
1178
- importAlias: "Import alias"
1179
- };
1180
1192
  function formatItem(item, nameMap) {
1181
1193
  const name = nameMap?.[item.name] ?? item.name;
1182
1194
  return item.version ? `${name} ${item.version}` : name;
@@ -1312,6 +1324,11 @@ function displayRulesPreview(config) {
1312
1324
  }
1313
1325
  console.log("");
1314
1326
  }
1327
+
1328
+ // src/display-text.ts
1329
+ function getConventionStr2(cv) {
1330
+ return typeof cv === "string" ? cv : cv.value;
1331
+ }
1315
1332
  function plainConfidenceLabel(convention) {
1316
1333
  const pct = Math.round(convention.consistency);
1317
1334
  if (convention.confidence === "high") {
@@ -1327,7 +1344,7 @@ function formatConventionsText(scanResult) {
1327
1344
  lines.push("Conventions:");
1328
1345
  for (const [key, convention] of conventionEntries) {
1329
1346
  if (convention.confidence === "low") continue;
1330
- const label = CONVENTION_LABELS[key] ?? key;
1347
+ const label = CONVENTION_LABELS2[key] ?? key;
1331
1348
  if (scanResult.packages.length > 1) {
1332
1349
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1333
1350
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
@@ -1361,7 +1378,7 @@ function formatRulesText(config) {
1361
1378
  lines.push(" \u2022 Require test files: no");
1362
1379
  }
1363
1380
  if (config.rules.enforceNaming && config.conventions.fileNaming) {
1364
- lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
1381
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
1365
1382
  } else {
1366
1383
  lines.push(" \u2022 Enforce file naming: no");
1367
1384
  }
@@ -1376,17 +1393,17 @@ function formatScanResultsText(scanResult, config) {
1376
1393
  const { stack } = scanResult;
1377
1394
  lines.push("Detected:");
1378
1395
  if (stack.framework) {
1379
- lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1396
+ lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
1380
1397
  }
1381
1398
  lines.push(` \u2713 ${formatItem(stack.language)}`);
1382
1399
  if (stack.styling) {
1383
- lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES2)}`);
1400
+ lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
1384
1401
  }
1385
1402
  if (stack.backend) {
1386
- lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1403
+ lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
1387
1404
  }
1388
1405
  if (stack.orm) {
1389
- lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES)}`);
1406
+ lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
1390
1407
  }
1391
1408
  const secondaryParts = [];
1392
1409
  if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
@@ -1398,7 +1415,7 @@ function formatScanResultsText(scanResult, config) {
1398
1415
  }
1399
1416
  if (stack.libraries.length > 0) {
1400
1417
  for (const lib of stack.libraries) {
1401
- lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES)}`);
1418
+ lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
1402
1419
  }
1403
1420
  }
1404
1421
  const groups = groupByRole(scanResult.structure.directories);
@@ -1550,7 +1567,17 @@ function setupClaudeCodeHook(projectRoot) {
1550
1567
  const existing = hooks.PostToolUse ?? [];
1551
1568
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1552
1569
  const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1553
- const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
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;
1554
1581
  hooks.PostToolUse = [
1555
1582
  ...existing,
1556
1583
  {
@@ -1568,6 +1595,18 @@ function setupClaudeCodeHook(projectRoot) {
1568
1595
  `);
1569
1596
  console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1570
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`);
1609
+ }
1571
1610
  function writeHuskyPreCommit(huskyDir) {
1572
1611
  const hookPath = path12.join(huskyDir, "pre-commit");
1573
1612
  if (fs11.existsSync(hookPath)) {
@@ -1596,7 +1635,7 @@ function filterHighConfidence(conventions) {
1596
1635
  }
1597
1636
  return filtered;
1598
1637
  }
1599
- function getConventionStr2(cv) {
1638
+ function getConventionStr3(cv) {
1600
1639
  if (!cv) return void 0;
1601
1640
  return typeof cv === "string" ? cv : cv.value;
1602
1641
  }
@@ -1613,10 +1652,10 @@ async function initCommand(options, cwd) {
1613
1652
  );
1614
1653
  }
1615
1654
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1616
- if (fs12.existsSync(configPath)) {
1655
+ if (fs12.existsSync(configPath) && !options.force) {
1617
1656
  console.log(
1618
1657
  `${chalk8.yellow("!")} viberails is already initialized.
1619
- Run ${chalk8.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
1658
+ Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
1620
1659
  );
1621
1660
  return;
1622
1661
  }
@@ -1636,16 +1675,18 @@ async function initCommand(options, cwd) {
1636
1675
  ignore: config2.ignore
1637
1676
  });
1638
1677
  const inferred = inferBoundaries(graph);
1639
- if (inferred.length > 0) {
1678
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1679
+ if (denyCount > 0) {
1640
1680
  config2.boundaries = inferred;
1641
1681
  config2.rules.enforceBoundaries = true;
1642
- console.log(` Inferred ${inferred.length} boundary rules`);
1682
+ console.log(` Inferred ${denyCount} boundary rules`);
1643
1683
  }
1644
1684
  }
1645
1685
  fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1646
1686
  `);
1647
1687
  writeGeneratedFiles(projectRoot, config2, scanResult2);
1648
1688
  updateGitignore(projectRoot);
1689
+ setupClaudeMdReference(projectRoot);
1649
1690
  console.log(`
1650
1691
  Created:`);
1651
1692
  console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
@@ -1677,7 +1718,7 @@ Created:`);
1677
1718
  requireTests: config.rules.requireTests,
1678
1719
  enforceNaming: config.rules.enforceNaming,
1679
1720
  enforcement: config.enforcement,
1680
- fileNamingValue: getConventionStr2(config.conventions.fileNaming)
1721
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1681
1722
  });
1682
1723
  config.rules.maxFileLines = overrides.maxFileLines;
1683
1724
  config.rules.requireTests = overrides.requireTests;
@@ -1706,10 +1747,11 @@ Created:`);
1706
1747
  ignore: config.ignore
1707
1748
  });
1708
1749
  const inferred = inferBoundaries(graph);
1709
- if (inferred.length > 0) {
1750
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1751
+ if (denyCount > 0) {
1710
1752
  config.boundaries = inferred;
1711
1753
  config.rules.enforceBoundaries = true;
1712
- bs.stop(`Inferred ${inferred.length} boundary rules`);
1754
+ bs.stop(`Inferred ${denyCount} boundary rules`);
1713
1755
  } else {
1714
1756
  bs.stop("No boundary rules inferred");
1715
1757
  }
@@ -1743,6 +1785,10 @@ Created:`);
1743
1785
  setupClaudeCodeHook(projectRoot);
1744
1786
  createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1745
1787
  }
1788
+ if (integrations.claudeMdRef) {
1789
+ setupClaudeMdReference(projectRoot);
1790
+ createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
1791
+ }
1746
1792
  clack2.log.success(`Created:
1747
1793
  ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1748
1794
  clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
@@ -1767,7 +1813,145 @@ import * as path14 from "path";
1767
1813
  import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
1768
1814
  import { scan as scan2 } from "@viberails/scanner";
1769
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
1770
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
+ }
1771
1955
  async function syncCommand(cwd) {
1772
1956
  const startDir = cwd ?? process.cwd();
1773
1957
  const projectRoot = findProjectRoot(startDir);
@@ -1778,16 +1962,25 @@ async function syncCommand(cwd) {
1778
1962
  }
1779
1963
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1780
1964
  const existing = await loadConfig4(configPath);
1965
+ const previousStats = loadPreviousStats(projectRoot);
1781
1966
  console.log(chalk9.dim("Scanning project..."));
1782
1967
  const scanResult = await scan2(projectRoot);
1783
1968
  const merged = mergeConfig(existing, scanResult);
1784
1969
  const existingJson = JSON.stringify(existing, null, 2);
1785
1970
  const mergedJson = JSON.stringify(merged, null, 2);
1786
1971
  const configChanged = existingJson !== mergedJson;
1787
- if (configChanged) {
1788
- console.log(
1789
- ` ${chalk9.yellow("!")} Config updated \u2014 review ${chalk9.cyan(CONFIG_FILE5)} for changes`
1790
- );
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
+ }
1791
1984
  }
1792
1985
  fs13.writeFileSync(configPath, `${mergedJson}
1793
1986
  `);
@@ -1804,10 +1997,10 @@ ${chalk9.bold("Synced:")}`);
1804
1997
  }
1805
1998
 
1806
1999
  // src/index.ts
1807
- var VERSION = "0.3.2";
2000
+ var VERSION = "0.3.3";
1808
2001
  var program = new Command();
1809
2002
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1810
- program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
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) => {
1811
2004
  try {
1812
2005
  await initCommand(options);
1813
2006
  } catch (err) {