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.cjs CHANGED
@@ -149,15 +149,21 @@ async function promptIntegrations(hookManager) {
149
149
  value: "claude",
150
150
  label: "Claude Code hook",
151
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"
152
157
  }
153
158
  ],
154
- initialValues: ["preCommit", "claude"],
159
+ initialValues: ["preCommit", "claude", "claudeMd"],
155
160
  required: false
156
161
  });
157
162
  assertNotCancelled(result);
158
163
  return {
159
164
  preCommitHook: result.includes("preCommit"),
160
- claudeCodeHook: result.includes("claude")
165
+ claudeCodeHook: result.includes("claude"),
166
+ claudeMdRef: result.includes("claudeMd")
161
167
  };
162
168
  }
163
169
 
@@ -215,22 +221,21 @@ async function boundariesCommand(options, cwd) {
215
221
  displayRules(config);
216
222
  }
217
223
  function displayRules(config) {
218
- if (!config.boundaries || config.boundaries.length === 0) {
224
+ if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
219
225
  console.log(import_chalk.default.yellow("No boundary rules configured."));
220
226
  console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
221
227
  return;
222
228
  }
223
- const allowRules = config.boundaries.filter((r) => r.allow);
224
- 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);
225
232
  console.log(`
226
- ${import_chalk.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
233
+ ${import_chalk.default.bold(`Boundary rules (${totalRules} deny rules):`)}
227
234
  `);
228
- for (const r of allowRules) {
229
- console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
230
- }
231
- for (const r of denyRules) {
232
- const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
233
- console.log(` ${import_chalk.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
+ }
234
239
  }
235
240
  console.log(
236
241
  `
@@ -247,24 +252,22 @@ async function inferAndDisplay(projectRoot, config, configPath) {
247
252
  });
248
253
  console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
249
254
  const inferred = inferBoundaries(graph);
250
- if (inferred.length === 0) {
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) {
251
258
  console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
252
259
  return;
253
260
  }
254
- const allow = inferred.filter((r) => r.allow);
255
- const deny = inferred.filter((r) => !r.allow);
256
261
  console.log(`
257
262
  ${import_chalk.default.bold("Inferred boundary rules:")}
258
263
  `);
259
- for (const r of allow) {
260
- console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
261
- }
262
- for (const r of deny) {
263
- const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
264
- console.log(` ${import_chalk.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
+ }
265
268
  }
266
269
  console.log(`
267
- ${allow.length} allowed, ${deny.length} denied`);
270
+ ${totalRules} denied`);
268
271
  console.log("");
269
272
  const shouldSave = await confirm2("Save to viberails.config.json?");
270
273
  if (shouldSave) {
@@ -272,7 +275,7 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
272
275
  config.rules.enforceBoundaries = true;
273
276
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
274
277
  `);
275
- console.log(`${import_chalk.default.green("\u2713")} Saved ${inferred.length} rules`);
278
+ console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
276
279
  }
277
280
  }
278
281
  async function showGraph(projectRoot, config) {
@@ -631,7 +634,7 @@ async function checkCommand(options, cwd) {
631
634
  const testViolations = checkMissingTests(projectRoot, config, severity);
632
635
  violations.push(...testViolations);
633
636
  }
634
- 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) {
635
638
  const startTime = Date.now();
636
639
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
637
640
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -647,7 +650,7 @@ async function checkCommand(options, cwd) {
647
650
  violations.push({
648
651
  file: relFile,
649
652
  rule: "boundary-violation",
650
- 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}`,
651
654
  severity
652
655
  });
653
656
  }
@@ -1042,9 +1045,8 @@ var import_config4 = require("@viberails/config");
1042
1045
  var import_scanner = require("@viberails/scanner");
1043
1046
  var import_chalk8 = __toESM(require("chalk"), 1);
1044
1047
 
1045
- // src/display.ts
1046
- var import_types3 = require("@viberails/types");
1047
- var import_chalk6 = __toESM(require("chalk"), 1);
1048
+ // src/display-text.ts
1049
+ var import_types4 = require("@viberails/types");
1048
1050
 
1049
1051
  // src/display-helpers.ts
1050
1052
  var import_types = require("@viberails/types");
@@ -1095,6 +1097,10 @@ function formatRoleGroup(group) {
1095
1097
  return `${group.label} \u2014 ${dirs} (${files})`;
1096
1098
  }
1097
1099
 
1100
+ // src/display.ts
1101
+ var import_types3 = require("@viberails/types");
1102
+ var import_chalk6 = __toESM(require("chalk"), 1);
1103
+
1098
1104
  // src/display-monorepo.ts
1099
1105
  var import_types2 = require("@viberails/types");
1100
1106
  var import_chalk5 = __toESM(require("chalk"), 1);
@@ -1204,12 +1210,6 @@ function formatMonorepoResultsText(scanResult, config) {
1204
1210
  }
1205
1211
 
1206
1212
  // src/display.ts
1207
- var CONVENTION_LABELS = {
1208
- fileNaming: "File naming",
1209
- componentNaming: "Component naming",
1210
- hookNaming: "Hook naming",
1211
- importAlias: "Import alias"
1212
- };
1213
1213
  function formatItem(item, nameMap) {
1214
1214
  const name = nameMap?.[item.name] ?? item.name;
1215
1215
  return item.version ? `${name} ${item.version}` : name;
@@ -1228,7 +1228,7 @@ function displayConventions(scanResult) {
1228
1228
  ${import_chalk6.default.bold("Conventions:")}`);
1229
1229
  for (const [key, convention] of conventionEntries) {
1230
1230
  if (convention.confidence === "low") continue;
1231
- const label = CONVENTION_LABELS[key] ?? key;
1231
+ const label = import_types3.CONVENTION_LABELS[key] ?? key;
1232
1232
  if (scanResult.packages.length > 1) {
1233
1233
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1234
1234
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
@@ -1345,6 +1345,11 @@ function displayRulesPreview(config) {
1345
1345
  }
1346
1346
  console.log("");
1347
1347
  }
1348
+
1349
+ // src/display-text.ts
1350
+ function getConventionStr2(cv) {
1351
+ return typeof cv === "string" ? cv : cv.value;
1352
+ }
1348
1353
  function plainConfidenceLabel(convention) {
1349
1354
  const pct = Math.round(convention.consistency);
1350
1355
  if (convention.confidence === "high") {
@@ -1360,7 +1365,7 @@ function formatConventionsText(scanResult) {
1360
1365
  lines.push("Conventions:");
1361
1366
  for (const [key, convention] of conventionEntries) {
1362
1367
  if (convention.confidence === "low") continue;
1363
- const label = CONVENTION_LABELS[key] ?? key;
1368
+ const label = import_types4.CONVENTION_LABELS[key] ?? key;
1364
1369
  if (scanResult.packages.length > 1) {
1365
1370
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1366
1371
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
@@ -1394,7 +1399,7 @@ function formatRulesText(config) {
1394
1399
  lines.push(" \u2022 Require test files: no");
1395
1400
  }
1396
1401
  if (config.rules.enforceNaming && config.conventions.fileNaming) {
1397
- lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
1402
+ lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
1398
1403
  } else {
1399
1404
  lines.push(" \u2022 Enforce file naming: no");
1400
1405
  }
@@ -1409,17 +1414,17 @@ function formatScanResultsText(scanResult, config) {
1409
1414
  const { stack } = scanResult;
1410
1415
  lines.push("Detected:");
1411
1416
  if (stack.framework) {
1412
- lines.push(` \u2713 ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1417
+ lines.push(` \u2713 ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
1413
1418
  }
1414
1419
  lines.push(` \u2713 ${formatItem(stack.language)}`);
1415
1420
  if (stack.styling) {
1416
- lines.push(` \u2713 ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1421
+ lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
1417
1422
  }
1418
1423
  if (stack.backend) {
1419
- lines.push(` \u2713 ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1424
+ lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
1420
1425
  }
1421
1426
  if (stack.orm) {
1422
- lines.push(` \u2713 ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1427
+ lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
1423
1428
  }
1424
1429
  const secondaryParts = [];
1425
1430
  if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
@@ -1431,7 +1436,7 @@ function formatScanResultsText(scanResult, config) {
1431
1436
  }
1432
1437
  if (stack.libraries.length > 0) {
1433
1438
  for (const lib of stack.libraries) {
1434
- lines.push(` \u2713 ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1439
+ lines.push(` \u2713 ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
1435
1440
  }
1436
1441
  }
1437
1442
  const groups = groupByRole(scanResult.structure.directories);
@@ -1583,7 +1588,17 @@ function setupClaudeCodeHook(projectRoot) {
1583
1588
  const existing = hooks.PostToolUse ?? [];
1584
1589
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1585
1590
  const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1586
- 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;
1587
1602
  hooks.PostToolUse = [
1588
1603
  ...existing,
1589
1604
  {
@@ -1601,6 +1616,18 @@ function setupClaudeCodeHook(projectRoot) {
1601
1616
  `);
1602
1617
  console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1603
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`);
1630
+ }
1604
1631
  function writeHuskyPreCommit(huskyDir) {
1605
1632
  const hookPath = path12.join(huskyDir, "pre-commit");
1606
1633
  if (fs11.existsSync(hookPath)) {
@@ -1629,7 +1656,7 @@ function filterHighConfidence(conventions) {
1629
1656
  }
1630
1657
  return filtered;
1631
1658
  }
1632
- function getConventionStr2(cv) {
1659
+ function getConventionStr3(cv) {
1633
1660
  if (!cv) return void 0;
1634
1661
  return typeof cv === "string" ? cv : cv.value;
1635
1662
  }
@@ -1646,10 +1673,10 @@ async function initCommand(options, cwd) {
1646
1673
  );
1647
1674
  }
1648
1675
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1649
- if (fs12.existsSync(configPath)) {
1676
+ if (fs12.existsSync(configPath) && !options.force) {
1650
1677
  console.log(
1651
1678
  `${import_chalk8.default.yellow("!")} viberails is already initialized.
1652
- Run ${import_chalk8.default.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
1679
+ Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
1653
1680
  );
1654
1681
  return;
1655
1682
  }
@@ -1669,16 +1696,18 @@ async function initCommand(options, cwd) {
1669
1696
  ignore: config2.ignore
1670
1697
  });
1671
1698
  const inferred = inferBoundaries(graph);
1672
- if (inferred.length > 0) {
1699
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1700
+ if (denyCount > 0) {
1673
1701
  config2.boundaries = inferred;
1674
1702
  config2.rules.enforceBoundaries = true;
1675
- console.log(` Inferred ${inferred.length} boundary rules`);
1703
+ console.log(` Inferred ${denyCount} boundary rules`);
1676
1704
  }
1677
1705
  }
1678
1706
  fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1679
1707
  `);
1680
1708
  writeGeneratedFiles(projectRoot, config2, scanResult2);
1681
1709
  updateGitignore(projectRoot);
1710
+ setupClaudeMdReference(projectRoot);
1682
1711
  console.log(`
1683
1712
  Created:`);
1684
1713
  console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
@@ -1710,7 +1739,7 @@ Created:`);
1710
1739
  requireTests: config.rules.requireTests,
1711
1740
  enforceNaming: config.rules.enforceNaming,
1712
1741
  enforcement: config.enforcement,
1713
- fileNamingValue: getConventionStr2(config.conventions.fileNaming)
1742
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1714
1743
  });
1715
1744
  config.rules.maxFileLines = overrides.maxFileLines;
1716
1745
  config.rules.requireTests = overrides.requireTests;
@@ -1739,10 +1768,11 @@ Created:`);
1739
1768
  ignore: config.ignore
1740
1769
  });
1741
1770
  const inferred = inferBoundaries(graph);
1742
- if (inferred.length > 0) {
1771
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1772
+ if (denyCount > 0) {
1743
1773
  config.boundaries = inferred;
1744
1774
  config.rules.enforceBoundaries = true;
1745
- bs.stop(`Inferred ${inferred.length} boundary rules`);
1775
+ bs.stop(`Inferred ${denyCount} boundary rules`);
1746
1776
  } else {
1747
1777
  bs.stop("No boundary rules inferred");
1748
1778
  }
@@ -1776,6 +1806,10 @@ Created:`);
1776
1806
  setupClaudeCodeHook(projectRoot);
1777
1807
  createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1778
1808
  }
1809
+ if (integrations.claudeMdRef) {
1810
+ setupClaudeMdReference(projectRoot);
1811
+ createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
1812
+ }
1779
1813
  clack2.log.success(`Created:
1780
1814
  ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1781
1815
  clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
@@ -1800,7 +1834,145 @@ var path14 = __toESM(require("path"), 1);
1800
1834
  var import_config5 = require("@viberails/config");
1801
1835
  var import_scanner2 = require("@viberails/scanner");
1802
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
1803
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
+ }
1804
1976
  async function syncCommand(cwd) {
1805
1977
  const startDir = cwd ?? process.cwd();
1806
1978
  const projectRoot = findProjectRoot(startDir);
@@ -1811,16 +1983,25 @@ async function syncCommand(cwd) {
1811
1983
  }
1812
1984
  const configPath = path14.join(projectRoot, CONFIG_FILE5);
1813
1985
  const existing = await (0, import_config5.loadConfig)(configPath);
1986
+ const previousStats = loadPreviousStats(projectRoot);
1814
1987
  console.log(import_chalk9.default.dim("Scanning project..."));
1815
1988
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
1816
1989
  const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1817
1990
  const existingJson = JSON.stringify(existing, null, 2);
1818
1991
  const mergedJson = JSON.stringify(merged, null, 2);
1819
1992
  const configChanged = existingJson !== mergedJson;
1820
- if (configChanged) {
1821
- console.log(
1822
- ` ${import_chalk9.default.yellow("!")} Config updated \u2014 review ${import_chalk9.default.cyan(CONFIG_FILE5)} for changes`
1823
- );
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
+ }
1824
2005
  }
1825
2006
  fs13.writeFileSync(configPath, `${mergedJson}
1826
2007
  `);
@@ -1837,10 +2018,10 @@ ${import_chalk9.default.bold("Synced:")}`);
1837
2018
  }
1838
2019
 
1839
2020
  // src/index.ts
1840
- var VERSION = "0.3.2";
2021
+ var VERSION = "0.3.3";
1841
2022
  var program = new import_commander.Command();
1842
2023
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1843
- 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) => {
1844
2025
  try {
1845
2026
  await initCommand(options);
1846
2027
  } catch (err) {