viberails 0.5.0 → 0.5.2

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,7 +34,7 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk12 = __toESM(require("chalk"), 1);
37
+ var import_chalk13 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -64,14 +64,66 @@ function findProjectRoot(startDir) {
64
64
  var clack5 = __toESM(require("@clack/prompts"), 1);
65
65
 
66
66
  // src/utils/prompt-integrations.ts
67
+ var import_node_child_process = require("child_process");
67
68
  var clack = __toESM(require("@clack/prompts"), 1);
68
- async function promptIntegrations(hookManager, tools) {
69
- const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
69
+ async function promptHookManagerInstall(projectRoot, packageManager) {
70
+ const choice = await clack.select({
71
+ message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
72
+ options: [
73
+ {
74
+ value: "install",
75
+ label: "Yes, install Lefthook",
76
+ hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
77
+ },
78
+ {
79
+ value: "skip",
80
+ label: "No, skip",
81
+ hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
82
+ }
83
+ ]
84
+ });
85
+ assertNotCancelled(choice);
86
+ if (choice !== "install") return void 0;
87
+ const pm = packageManager || "npm";
88
+ const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? "pnpm add -D lefthook" : "npm install -D lefthook";
89
+ const s = clack.spinner();
90
+ s.start("Installing Lefthook...");
91
+ const result = (0, import_node_child_process.spawnSync)(installCmd, {
92
+ cwd: projectRoot,
93
+ shell: true,
94
+ encoding: "utf-8",
95
+ stdio: "pipe"
96
+ });
97
+ if (result.status === 0) {
98
+ const fs20 = await import("fs");
99
+ const path20 = await import("path");
100
+ const lefthookPath = path20.join(projectRoot, "lefthook.yml");
101
+ if (!fs20.existsSync(lefthookPath)) {
102
+ fs20.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
103
+ }
104
+ s.stop("Installed Lefthook");
105
+ return "Lefthook";
106
+ }
107
+ s.stop("Failed to install Lefthook");
108
+ clack.log.warn(`Install manually: ${installCmd}`);
109
+ return void 0;
110
+ }
111
+ async function promptIntegrations(projectRoot, hookManager, tools) {
112
+ let resolvedHookManager = hookManager;
113
+ if (!resolvedHookManager) {
114
+ resolvedHookManager = await promptHookManagerInstall(
115
+ projectRoot,
116
+ tools?.packageManager ?? "npm"
117
+ );
118
+ }
119
+ const isBareHook = !resolvedHookManager;
120
+ const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
121
+ const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
70
122
  const options = [
71
123
  {
72
124
  value: "preCommit",
73
125
  label: hookLabel,
74
- hint: "runs viberails checks when you commit"
126
+ hint: hookHint
75
127
  }
76
128
  ];
77
129
  if (tools?.isTypeScript) {
@@ -106,7 +158,7 @@ async function promptIntegrations(hookManager, tools) {
106
158
  hint: "blocks PRs that fail viberails check"
107
159
  }
108
160
  );
109
- const initialValues = options.map((o) => o.value);
161
+ const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
110
162
  const result = await clack.multiselect({
111
163
  message: "Set up integrations?",
112
164
  options,
@@ -309,11 +361,10 @@ function buildMenuOptions(state, packageCount) {
309
361
  hint: state.fileNamingValue
310
362
  });
311
363
  }
312
- options.push({
313
- value: "testCoverage",
314
- label: "Test coverage target",
315
- hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
316
- });
364
+ const isMonorepo = packageCount > 0;
365
+ const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
366
+ const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
367
+ options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
317
368
  options.push({
318
369
  value: "enforceMissingTests",
319
370
  label: "Enforce missing tests",
@@ -323,16 +374,16 @@ function buildMenuOptions(state, packageCount) {
323
374
  options.push(
324
375
  {
325
376
  value: "coverageSummaryPath",
326
- label: "Coverage summary path",
377
+ label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
327
378
  hint: state.coverageSummaryPath
328
379
  },
329
380
  {
330
381
  value: "coverageCommand",
331
- label: "Coverage command",
382
+ label: isMonorepo ? "Default coverage command" : "Coverage command",
332
383
  hint: state.coverageCommand ?? "auto-detect from package.json test runner"
333
384
  }
334
385
  );
335
- if (packageCount > 0) {
386
+ if (isMonorepo) {
336
387
  options.push({
337
388
  value: "packageOverrides",
338
389
  label: "Per-package coverage overrides",
@@ -678,7 +729,7 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
678
729
  // src/commands/check.ts
679
730
  var fs7 = __toESM(require("fs"), 1);
680
731
  var path7 = __toESM(require("path"), 1);
681
- var import_config4 = require("@viberails/config");
732
+ var import_config5 = require("@viberails/config");
682
733
  var import_chalk2 = __toESM(require("chalk"), 1);
683
734
 
684
735
  // src/commands/check-config.ts
@@ -723,9 +774,10 @@ function resolveIgnoreForFile(relPath, config) {
723
774
  }
724
775
 
725
776
  // src/commands/check-coverage.ts
726
- var import_node_child_process = require("child_process");
777
+ var import_node_child_process2 = require("child_process");
727
778
  var fs4 = __toESM(require("fs"), 1);
728
779
  var path4 = __toESM(require("path"), 1);
780
+ var import_config3 = require("@viberails/config");
729
781
  var DEFAULT_SUMMARY_PATH = "coverage/coverage-summary.json";
730
782
  function packageRoot(projectRoot, pkg) {
731
783
  return pkg.path === "." ? projectRoot : path4.join(projectRoot, pkg.path);
@@ -765,7 +817,7 @@ function readCoveragePercentage(summaryPath) {
765
817
  }
766
818
  }
767
819
  function runCoverageCommand(pkgRoot, command) {
768
- const result = (0, import_node_child_process.spawnSync)(command, {
820
+ const result = (0, import_node_child_process2.spawnSync)(command, {
769
821
  cwd: pkgRoot,
770
822
  shell: true,
771
823
  encoding: "utf-8",
@@ -806,13 +858,14 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
806
858
  const violations = [];
807
859
  for (const target of targets) {
808
860
  if (target.rules.testCoverage <= 0) continue;
861
+ if (!target.pkg.stack?.testRunner) continue;
809
862
  const pkgRoot = packageRoot(projectRoot, target.pkg);
810
863
  const summaryPath = target.coverage.summaryPath ?? DEFAULT_SUMMARY_PATH;
811
864
  const summaryAbs = path4.join(pkgRoot, summaryPath);
812
865
  const summaryRel = violationFilePath(projectRoot, pkgRoot, summaryPath);
813
866
  let pct = readCoveragePercentage(summaryAbs);
814
867
  if (pct === void 0 && !options.staged) {
815
- const command = target.coverage.command;
868
+ const command = target.coverage.command ?? (0, import_config3.inferCoverageCommand)(target.pkg.stack.testRunner);
816
869
  if (!command) {
817
870
  const pkgLabel = target.pkg.path === "." ? "root package" : target.pkg.path;
818
871
  pushViolation(
@@ -823,6 +876,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
823
876
  );
824
877
  continue;
825
878
  }
879
+ options.onProgress?.(target.pkg.path === "." ? "root" : target.pkg.path);
826
880
  const run = runCoverageCommand(pkgRoot, command);
827
881
  if (!run.ok) {
828
882
  pushViolation(
@@ -857,10 +911,10 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
857
911
  }
858
912
 
859
913
  // src/commands/check-files.ts
860
- var import_node_child_process2 = require("child_process");
914
+ var import_node_child_process3 = require("child_process");
861
915
  var fs5 = __toESM(require("fs"), 1);
862
916
  var path5 = __toESM(require("path"), 1);
863
- var import_config3 = require("@viberails/config");
917
+ var import_config4 = require("@viberails/config");
864
918
  var import_picomatch = __toESM(require("picomatch"), 1);
865
919
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
866
920
  "node_modules",
@@ -930,7 +984,7 @@ function checkNaming(relPath, conventions) {
930
984
  }
931
985
  function getStagedFiles(projectRoot) {
932
986
  try {
933
- const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACM", {
987
+ const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACM", {
934
988
  cwd: projectRoot,
935
989
  encoding: "utf-8",
936
990
  stdio: ["ignore", "pipe", "ignore"]
@@ -942,12 +996,12 @@ function getStagedFiles(projectRoot) {
942
996
  }
943
997
  function getDiffFiles(projectRoot, base) {
944
998
  try {
945
- const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
999
+ const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
946
1000
  cwd: projectRoot,
947
1001
  encoding: "utf-8",
948
1002
  stdio: ["ignore", "pipe", "ignore"]
949
1003
  });
950
- const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1004
+ const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
951
1005
  cwd: projectRoot,
952
1006
  encoding: "utf-8",
953
1007
  stdio: ["ignore", "pipe", "ignore"]
@@ -961,7 +1015,7 @@ function getDiffFiles(projectRoot, base) {
961
1015
  }
962
1016
  }
963
1017
  function getAllSourceFiles(projectRoot, config) {
964
- const effectiveIgnore = [...import_config3.BUILTIN_IGNORE, ...config.ignore ?? []];
1018
+ const effectiveIgnore = [...import_config4.BUILTIN_IGNORE, ...config.ignore ?? []];
965
1019
  const files = [];
966
1020
  const walk = (dir) => {
967
1021
  let entries;
@@ -1141,7 +1195,7 @@ async function checkCommand(options, cwd) {
1141
1195
  );
1142
1196
  return 1;
1143
1197
  }
1144
- const config = await (0, import_config4.loadConfig)(configPath);
1198
+ const config = await (0, import_config5.loadConfig)(configPath);
1145
1199
  let filesToCheck;
1146
1200
  let diffAddedFiles = null;
1147
1201
  if (options.staged) {
@@ -1165,6 +1219,9 @@ async function checkCommand(options, cwd) {
1165
1219
  }
1166
1220
  const violations = [];
1167
1221
  const severity = options.enforce ? "error" : "warn";
1222
+ const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(import_chalk2.default.dim(msg)) : () => {
1223
+ };
1224
+ log7(" Checking files...");
1168
1225
  for (const file of filesToCheck) {
1169
1226
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1170
1227
  const relPath = path7.relative(projectRoot, absPath);
@@ -1197,18 +1254,22 @@ async function checkCommand(options, cwd) {
1197
1254
  }
1198
1255
  }
1199
1256
  }
1257
+ log7(" done\n");
1200
1258
  if (!options.staged && !options.files) {
1259
+ log7(" Checking missing tests...");
1201
1260
  const testViolations = checkMissingTests(projectRoot, config, severity);
1202
- if (diffAddedFiles) {
1203
- violations.push(...testViolations.filter((v) => diffAddedFiles.has(v.file)));
1204
- } else {
1205
- violations.push(...testViolations);
1206
- }
1261
+ violations.push(
1262
+ ...diffAddedFiles ? testViolations.filter((v) => diffAddedFiles.has(v.file)) : testViolations
1263
+ );
1264
+ log7(" done\n");
1207
1265
  }
1208
1266
  if (!options.files && !options.staged && !options.diffBase) {
1267
+ log7(" Running test coverage...\n");
1209
1268
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1210
1269
  staged: options.staged,
1211
- enforce: options.enforce
1270
+ enforce: options.enforce,
1271
+ onProgress: (pkg) => log7(` Coverage: ${pkg}...
1272
+ `)
1212
1273
  });
1213
1274
  violations.push(...coverageViolations);
1214
1275
  }
@@ -1232,10 +1293,8 @@ async function checkCommand(options, cwd) {
1232
1293
  severity
1233
1294
  });
1234
1295
  }
1235
- const elapsed = Date.now() - startTime;
1236
- if (options.format !== "json") {
1237
- console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
1238
- }
1296
+ log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1297
+ `);
1239
1298
  }
1240
1299
  if (options.format === "json") {
1241
1300
  console.log(
@@ -1307,431 +1366,367 @@ async function hookCheckCommand(cwd) {
1307
1366
  }
1308
1367
  }
1309
1368
 
1310
- // src/commands/fix.ts
1311
- var fs11 = __toESM(require("fs"), 1);
1312
- var path11 = __toESM(require("path"), 1);
1313
- var import_config5 = require("@viberails/config");
1369
+ // src/commands/config.ts
1370
+ var fs10 = __toESM(require("fs"), 1);
1371
+ var path9 = __toESM(require("path"), 1);
1372
+ var clack6 = __toESM(require("@clack/prompts"), 1);
1373
+ var import_config6 = require("@viberails/config");
1374
+ var import_scanner = require("@viberails/scanner");
1375
+ var import_chalk5 = __toESM(require("chalk"), 1);
1376
+
1377
+ // src/display-text.ts
1378
+ var import_types4 = require("@viberails/types");
1379
+
1380
+ // src/display.ts
1381
+ var import_types3 = require("@viberails/types");
1314
1382
  var import_chalk4 = __toESM(require("chalk"), 1);
1315
1383
 
1316
- // src/commands/fix-helpers.ts
1317
- var import_node_child_process3 = require("child_process");
1318
- var import_chalk3 = __toESM(require("chalk"), 1);
1319
- function printPlan(renames, stubs) {
1320
- if (renames.length > 0) {
1321
- console.log(import_chalk3.default.bold("\nFile renames:"));
1322
- for (const r of renames) {
1323
- console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
1384
+ // src/display-helpers.ts
1385
+ var import_types = require("@viberails/types");
1386
+ function groupByRole(directories) {
1387
+ const map = /* @__PURE__ */ new Map();
1388
+ for (const dir of directories) {
1389
+ if (dir.role === "unknown") continue;
1390
+ const existing = map.get(dir.role);
1391
+ if (existing) {
1392
+ existing.dirs.push(dir);
1393
+ } else {
1394
+ map.set(dir.role, { dirs: [dir] });
1324
1395
  }
1325
1396
  }
1326
- if (stubs.length > 0) {
1327
- console.log(import_chalk3.default.bold("\nTest stubs to create:"));
1328
- for (const s of stubs) {
1329
- console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
1330
- }
1397
+ const groups = [];
1398
+ for (const [role, { dirs }] of map) {
1399
+ const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1400
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1401
+ groups.push({
1402
+ role,
1403
+ label,
1404
+ dirCount: dirs.length,
1405
+ totalFiles,
1406
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1407
+ });
1331
1408
  }
1409
+ return groups;
1332
1410
  }
1333
- function checkGitDirty(projectRoot) {
1334
- try {
1335
- const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
1336
- cwd: projectRoot,
1337
- encoding: "utf-8",
1338
- stdio: ["ignore", "pipe", "ignore"]
1339
- });
1340
- return output.trim().length > 0;
1341
- } catch {
1342
- return false;
1411
+ function formatSummary(stats, packageCount) {
1412
+ const parts = [];
1413
+ if (packageCount && packageCount > 1) {
1414
+ parts.push(`${packageCount} packages`);
1343
1415
  }
1416
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1417
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1418
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1419
+ return parts.join(" \xB7 ");
1344
1420
  }
1345
- function getConventionValue(convention) {
1346
- if (typeof convention === "string") return convention;
1347
- return void 0;
1421
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1422
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1348
1423
  }
1349
-
1350
- // src/commands/fix-imports.ts
1351
- var path8 = __toESM(require("path"), 1);
1352
- function stripExtension(filePath) {
1353
- return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1424
+ function formatRoleGroup(group) {
1425
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1426
+ if (group.singlePath) {
1427
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1428
+ }
1429
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1430
+ return `${group.label} \u2014 ${dirs} (${files})`;
1354
1431
  }
1355
- function computeNewSpecifier(oldSpecifier, newBare) {
1356
- const hasJsExt = oldSpecifier.endsWith(".js");
1357
- const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
1358
- const dir = base.lastIndexOf("/");
1359
- const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
1360
- const newSpec = prefix + newBare;
1361
- return hasJsExt ? `${newSpec}.js` : newSpec;
1432
+
1433
+ // src/display-monorepo.ts
1434
+ var import_types2 = require("@viberails/types");
1435
+ var import_chalk3 = __toESM(require("chalk"), 1);
1436
+ function formatPackageSummary(pkg) {
1437
+ const parts = [];
1438
+ if (pkg.stack.framework) {
1439
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1440
+ }
1441
+ if (pkg.stack.styling) {
1442
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1443
+ }
1444
+ const files = `${pkg.statistics.totalFiles} files`;
1445
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1446
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1362
1447
  }
1363
- async function updateImportsAfterRenames(renames, projectRoot) {
1364
- if (renames.length === 0) return [];
1365
- const { Project, SyntaxKind } = await import("ts-morph");
1366
- const renameMap = /* @__PURE__ */ new Map();
1367
- for (const r of renames) {
1368
- const oldStripped = stripExtension(r.oldAbsPath);
1369
- const newFilename = path8.basename(r.newPath);
1370
- const newName = newFilename.slice(0, newFilename.indexOf("."));
1371
- renameMap.set(oldStripped, { newBare: newName });
1448
+ function displayMonorepoResults(scanResult) {
1449
+ const { stack, packages } = scanResult;
1450
+ console.log(`
1451
+ ${import_chalk3.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1452
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.language)}`);
1453
+ if (stack.packageManager) {
1454
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1372
1455
  }
1373
- const project = new Project({
1374
- tsConfigFilePath: void 0,
1375
- skipAddingFilesFromTsConfig: true
1376
- });
1377
- project.addSourceFilesAtPaths(path8.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
1378
- const updates = [];
1379
- const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
1380
- for (const sourceFile of project.getSourceFiles()) {
1381
- const filePath = sourceFile.getFilePath();
1382
- const segments = filePath.split(path8.sep);
1383
- if (segments.includes("node_modules") || segments.includes("dist")) continue;
1384
- const fileDir = path8.dirname(filePath);
1385
- for (const decl of sourceFile.getImportDeclarations()) {
1386
- const specifier = decl.getModuleSpecifierValue();
1387
- if (!specifier.startsWith(".")) continue;
1388
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
1389
- if (!match) continue;
1390
- const newSpec = computeNewSpecifier(specifier, match.newBare);
1391
- updates.push({
1392
- file: filePath,
1393
- oldSpecifier: specifier,
1394
- newSpecifier: newSpec,
1395
- line: decl.getStartLineNumber()
1396
- });
1397
- decl.setModuleSpecifier(newSpec);
1398
- }
1399
- for (const decl of sourceFile.getExportDeclarations()) {
1400
- const specifier = decl.getModuleSpecifierValue();
1401
- if (!specifier || !specifier.startsWith(".")) continue;
1402
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
1403
- if (!match) continue;
1404
- const newSpec = computeNewSpecifier(specifier, match.newBare);
1405
- updates.push({
1406
- file: filePath,
1407
- oldSpecifier: specifier,
1408
- newSpecifier: newSpec,
1409
- line: decl.getStartLineNumber()
1410
- });
1411
- decl.setModuleSpecifier(newSpec);
1456
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1457
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1458
+ } else {
1459
+ if (stack.linter) {
1460
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)}`);
1412
1461
  }
1413
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1414
- if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
1415
- const args = call.getArguments();
1416
- if (args.length === 0) continue;
1417
- const arg = args[0];
1418
- if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
1419
- const specifier = arg.getText().slice(1, -1);
1420
- if (!specifier.startsWith(".")) continue;
1421
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
1422
- if (!match) continue;
1423
- const newSpec = computeNewSpecifier(specifier, match.newBare);
1424
- updates.push({
1425
- file: filePath,
1426
- oldSpecifier: specifier,
1427
- newSpecifier: newSpec,
1428
- line: call.getStartLineNumber()
1429
- });
1430
- const quote = arg.getText()[0];
1431
- arg.replaceWithText(`${quote}${newSpec}${quote}`);
1462
+ if (stack.formatter) {
1463
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1432
1464
  }
1433
1465
  }
1434
- if (updates.length > 0) {
1435
- await project.save();
1466
+ if (stack.testRunner) {
1467
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1436
1468
  }
1437
- return updates;
1469
+ console.log("");
1470
+ for (const pkg of packages) {
1471
+ console.log(formatPackageSummary(pkg));
1472
+ }
1473
+ const packagesWithDirs = packages.filter(
1474
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1475
+ );
1476
+ if (packagesWithDirs.length > 0) {
1477
+ console.log(`
1478
+ ${import_chalk3.default.bold("Structure:")}`);
1479
+ for (const pkg of packagesWithDirs) {
1480
+ const groups = groupByRole(pkg.structure.directories);
1481
+ if (groups.length === 0) continue;
1482
+ console.log(` ${pkg.relativePath}:`);
1483
+ for (const group of groups) {
1484
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatRoleGroup(group)}`);
1485
+ }
1486
+ }
1487
+ }
1488
+ displayConventions(scanResult);
1489
+ displaySummarySection(scanResult);
1490
+ console.log("");
1438
1491
  }
1439
- function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
1440
- const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
1441
- const resolved = path8.resolve(fromDir, cleanSpec);
1442
- for (const ext of extensions) {
1443
- const candidate = resolved + ext;
1444
- const stripped = stripExtension(candidate);
1445
- const match = renameMap.get(stripped);
1446
- if (match) return match;
1492
+ function formatPackageSummaryPlain(pkg) {
1493
+ const parts = [];
1494
+ if (pkg.stack.framework) {
1495
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1447
1496
  }
1448
- return void 0;
1497
+ if (pkg.stack.styling) {
1498
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1499
+ }
1500
+ const files = `${pkg.statistics.totalFiles} files`;
1501
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1502
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1449
1503
  }
1450
-
1451
- // src/commands/fix-naming.ts
1452
- var fs9 = __toESM(require("fs"), 1);
1453
- var path9 = __toESM(require("path"), 1);
1454
-
1455
- // src/commands/convert-name.ts
1456
- function splitIntoWords(name) {
1457
- const parts = name.split(/[-_]/);
1458
- const words = [];
1459
- for (const part of parts) {
1460
- if (part === "") continue;
1461
- let current = "";
1462
- for (let i = 0; i < part.length; i++) {
1463
- const ch = part[i];
1464
- const isUpper = ch >= "A" && ch <= "Z";
1465
- if (isUpper && current.length > 0) {
1466
- const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
1467
- const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
1468
- if (!prevIsUpper || nextIsLower) {
1469
- words.push(current.toLowerCase());
1470
- current = "";
1471
- }
1504
+ function formatMonorepoResultsText(scanResult) {
1505
+ const lines = [];
1506
+ const { stack, packages } = scanResult;
1507
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1508
+ const sharedParts = [formatItem(stack.language)];
1509
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1510
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1511
+ sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
1512
+ } else {
1513
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1514
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1515
+ }
1516
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1517
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1518
+ lines.push("");
1519
+ for (const pkg of packages) {
1520
+ lines.push(formatPackageSummaryPlain(pkg));
1521
+ }
1522
+ const packagesWithDirs = packages.filter(
1523
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1524
+ );
1525
+ if (packagesWithDirs.length > 0) {
1526
+ lines.push("");
1527
+ lines.push("Structure:");
1528
+ for (const pkg of packagesWithDirs) {
1529
+ const groups = groupByRole(pkg.structure.directories);
1530
+ if (groups.length === 0) continue;
1531
+ lines.push(` ${pkg.relativePath}:`);
1532
+ for (const group of groups) {
1533
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1472
1534
  }
1473
- current += ch;
1474
1535
  }
1475
- if (current) words.push(current.toLowerCase());
1476
1536
  }
1477
- return words;
1478
- }
1479
- function convertName(bare, target) {
1480
- const words = splitIntoWords(bare);
1481
- if (words.length === 0) return bare;
1482
- switch (target) {
1483
- case "kebab-case":
1484
- return words.join("-");
1485
- case "camelCase":
1486
- return words[0] + words.slice(1).map(capitalize).join("");
1487
- case "PascalCase":
1488
- return words.map(capitalize).join("");
1489
- case "snake_case":
1490
- return words.join("_");
1491
- default:
1492
- return bare;
1537
+ lines.push(...formatConventionsText(scanResult));
1538
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1539
+ lines.push("");
1540
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1541
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1542
+ if (ext) {
1543
+ lines.push(ext);
1493
1544
  }
1494
- }
1495
- function capitalize(word) {
1496
- if (word.length === 0) return word;
1497
- return word[0].toUpperCase() + word.slice(1);
1545
+ return lines.join("\n");
1498
1546
  }
1499
1547
 
1500
- // src/commands/fix-naming.ts
1501
- function computeRename(relPath, targetConvention, projectRoot) {
1502
- const filename = path9.basename(relPath);
1503
- const dir = path9.dirname(relPath);
1504
- const dotIndex = filename.indexOf(".");
1505
- if (dotIndex === -1) return null;
1506
- const bare = filename.slice(0, dotIndex);
1507
- const suffix = filename.slice(dotIndex);
1508
- const newBare = convertName(bare, targetConvention);
1509
- if (newBare === bare) return null;
1510
- const newFilename = newBare + suffix;
1511
- const newRelPath = path9.join(dir, newFilename);
1512
- const oldAbsPath = path9.join(projectRoot, relPath);
1513
- const newAbsPath = path9.join(projectRoot, newRelPath);
1514
- if (fs9.existsSync(newAbsPath)) return null;
1515
- return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
1516
- }
1517
- function executeRename(rename) {
1518
- if (fs9.existsSync(rename.newAbsPath)) return false;
1519
- fs9.renameSync(rename.oldAbsPath, rename.newAbsPath);
1520
- return true;
1548
+ // src/display.ts
1549
+ function formatItem(item, nameMap) {
1550
+ const name = nameMap?.[item.name] ?? item.name;
1551
+ return item.version ? `${name} ${item.version}` : name;
1521
1552
  }
1522
- function deduplicateRenames(renames) {
1523
- const seen = /* @__PURE__ */ new Set();
1524
- const result = [];
1525
- for (const r of renames) {
1526
- if (seen.has(r.newAbsPath)) continue;
1527
- seen.add(r.newAbsPath);
1528
- result.push(r);
1553
+ function confidenceLabel(convention) {
1554
+ const pct = Math.round(convention.consistency);
1555
+ if (convention.confidence === "high") {
1556
+ return `${pct}% \u2014 high confidence, will enforce`;
1529
1557
  }
1530
- return result;
1558
+ return `${pct}% \u2014 medium confidence, suggested only`;
1531
1559
  }
1532
-
1533
- // src/commands/fix-tests.ts
1534
- var fs10 = __toESM(require("fs"), 1);
1535
- var path10 = __toESM(require("path"), 1);
1536
- function generateTestStub(sourceRelPath, config, projectRoot) {
1537
- const pkg = resolvePackageForFile(sourceRelPath, config);
1538
- const testPattern = pkg?.structure?.testPattern;
1539
- if (!testPattern) return null;
1540
- const basename8 = path10.basename(sourceRelPath);
1541
- const ext = path10.extname(basename8);
1542
- if (!ext) return null;
1543
- const stem = basename8.slice(0, -ext.length);
1544
- const testSuffix = testPattern.replace("*", "");
1545
- const testFilename = `${stem}${testSuffix}`;
1546
- const dir = path10.dirname(path10.join(projectRoot, sourceRelPath));
1547
- const testAbsPath = path10.join(dir, testFilename);
1548
- if (fs10.existsSync(testAbsPath)) return null;
1549
- return {
1550
- path: path10.relative(projectRoot, testAbsPath),
1551
- absPath: testAbsPath,
1552
- moduleName: stem
1553
- };
1560
+ function displayConventions(scanResult) {
1561
+ const conventionEntries = Object.entries(scanResult.conventions);
1562
+ if (conventionEntries.length === 0) return;
1563
+ console.log(`
1564
+ ${import_chalk4.default.bold("Conventions:")}`);
1565
+ for (const [key, convention] of conventionEntries) {
1566
+ if (convention.confidence === "low") continue;
1567
+ const label = import_types3.CONVENTION_LABELS[key] ?? key;
1568
+ if (scanResult.packages.length > 1) {
1569
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1570
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1571
+ if (allSame || pkgValues.length <= 1) {
1572
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
1573
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
1574
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1575
+ } else {
1576
+ console.log(` ${import_chalk4.default.yellow("~")} ${label}: varies by package`);
1577
+ for (const pv of pkgValues) {
1578
+ const pct = Math.round(pv.convention.consistency);
1579
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1580
+ }
1581
+ }
1582
+ } else {
1583
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
1584
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
1585
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1586
+ }
1587
+ }
1554
1588
  }
1555
- function writeTestStub(stub, config) {
1556
- const pkg = resolvePackageForFile(stub.path, config);
1557
- const testRunner = pkg?.stack?.testRunner ?? "";
1558
- const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
1559
- const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
1560
- const content = `${importLine}describe('${stub.moduleName}', () => {
1561
- it.todo('add tests');
1562
- });
1563
- `;
1564
- fs10.mkdirSync(path10.dirname(stub.absPath), { recursive: true });
1565
- fs10.writeFileSync(stub.absPath, content);
1589
+ function displaySummarySection(scanResult) {
1590
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1591
+ console.log(`
1592
+ ${import_chalk4.default.bold("Summary:")}`);
1593
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1594
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1595
+ if (ext) {
1596
+ console.log(` ${ext}`);
1597
+ }
1566
1598
  }
1567
-
1568
- // src/commands/fix.ts
1569
- var CONFIG_FILE3 = "viberails.config.json";
1570
- async function fixCommand(options, cwd) {
1571
- const startDir = cwd ?? process.cwd();
1572
- const projectRoot = findProjectRoot(startDir);
1573
- if (!projectRoot) {
1574
- console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
1575
- return 1;
1599
+ function displayScanResults(scanResult) {
1600
+ if (scanResult.packages.length > 1) {
1601
+ displayMonorepoResults(scanResult);
1602
+ return;
1576
1603
  }
1577
- const configPath = path11.join(projectRoot, CONFIG_FILE3);
1578
- if (!fs11.existsSync(configPath)) {
1579
- console.error(
1580
- `${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
1581
- );
1582
- return 1;
1604
+ const { stack } = scanResult;
1605
+ console.log(`
1606
+ ${import_chalk4.default.bold("Detected:")}`);
1607
+ if (stack.framework) {
1608
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1583
1609
  }
1584
- const config = await (0, import_config5.loadConfig)(configPath);
1585
- if (!options.dryRun) {
1586
- const isDirty = checkGitDirty(projectRoot);
1587
- if (isDirty) {
1588
- console.log(
1589
- import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
1590
- );
1591
- }
1610
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
1611
+ if (stack.styling) {
1612
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1592
1613
  }
1593
- const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
1594
- const shouldFixTests = !options.rule || options.rule.includes("missing-test");
1595
- const allFiles = getAllSourceFiles(projectRoot, config);
1596
- const renames = [];
1597
- if (shouldFixNaming) {
1598
- for (const file of allFiles) {
1599
- const resolved = resolveConfigForFile(file, config);
1600
- if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
1601
- const violation = checkNaming(file, resolved.conventions);
1602
- if (!violation) continue;
1603
- const convention = getConventionValue(resolved.conventions.fileNaming);
1604
- if (!convention) continue;
1605
- const rename = computeRename(file, convention, projectRoot);
1606
- if (rename) renames.push(rename);
1607
- }
1614
+ if (stack.backend) {
1615
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1608
1616
  }
1609
- const dedupedRenames = deduplicateRenames(renames);
1610
- const testStubs = [];
1611
- if (shouldFixTests) {
1612
- const testViolations = checkMissingTests(projectRoot, config, "warn");
1613
- for (const v of testViolations) {
1614
- const stub = generateTestStub(v.file, config, projectRoot);
1615
- if (stub) testStubs.push(stub);
1616
- }
1617
+ if (stack.orm) {
1618
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1617
1619
  }
1618
- if (dedupedRenames.length === 0 && testStubs.length === 0) {
1619
- console.log(`${import_chalk4.default.green("\u2713")} No fixable violations found.`);
1620
- return 0;
1620
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1621
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1622
+ } else {
1623
+ if (stack.linter) {
1624
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
1625
+ }
1626
+ if (stack.formatter) {
1627
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1628
+ }
1621
1629
  }
1622
- printPlan(dedupedRenames, testStubs);
1623
- if (options.dryRun) {
1624
- console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
1625
- return 0;
1630
+ if (stack.testRunner) {
1631
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1626
1632
  }
1627
- if (!options.yes) {
1628
- const confirmed = await confirmDangerous("Apply these fixes?");
1629
- if (!confirmed) {
1630
- console.log("Aborted.");
1631
- return 0;
1632
- }
1633
+ if (stack.packageManager) {
1634
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1633
1635
  }
1634
- let renameCount = 0;
1635
- for (const rename of dedupedRenames) {
1636
- if (executeRename(rename)) {
1637
- renameCount++;
1636
+ if (stack.libraries.length > 0) {
1637
+ for (const lib of stack.libraries) {
1638
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1638
1639
  }
1639
1640
  }
1640
- let importUpdateCount = 0;
1641
- if (renameCount > 0) {
1642
- const appliedRenames = dedupedRenames.filter((r) => fs11.existsSync(r.newAbsPath));
1643
- const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
1644
- importUpdateCount = updates.length;
1645
- }
1646
- let stubCount = 0;
1647
- for (const stub of testStubs) {
1648
- if (!fs11.existsSync(stub.absPath)) {
1649
- writeTestStub(stub, config);
1650
- stubCount++;
1641
+ const groups = groupByRole(scanResult.structure.directories);
1642
+ if (groups.length > 0) {
1643
+ console.log(`
1644
+ ${import_chalk4.default.bold("Structure:")}`);
1645
+ for (const group of groups) {
1646
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatRoleGroup(group)}`);
1651
1647
  }
1652
1648
  }
1649
+ displayConventions(scanResult);
1650
+ displaySummarySection(scanResult);
1653
1651
  console.log("");
1654
- if (renameCount > 0) {
1655
- console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
1656
- }
1657
- if (importUpdateCount > 0) {
1658
- console.log(
1659
- `${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
1660
- );
1661
- }
1662
- if (stubCount > 0) {
1663
- console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
1652
+ }
1653
+ function displayRulesPreview(config) {
1654
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1655
+ console.log(
1656
+ `${import_chalk4.default.bold("Rules:")} ${import_chalk4.default.dim("(warns on violation; use --enforce in CI to block)")}`
1657
+ );
1658
+ console.log(` ${import_chalk4.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1659
+ if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
1660
+ console.log(
1661
+ ` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
1662
+ );
1663
+ } else if (config.rules.testCoverage > 0) {
1664
+ console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
1665
+ } else {
1666
+ console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: disabled`);
1664
1667
  }
1665
- return 0;
1668
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1669
+ console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
1670
+ } else {
1671
+ console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: no`);
1672
+ }
1673
+ console.log(
1674
+ ` ${import_chalk4.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1675
+ );
1676
+ console.log("");
1666
1677
  }
1667
-
1668
- // src/commands/init.ts
1669
- var fs17 = __toESM(require("fs"), 1);
1670
- var path17 = __toESM(require("path"), 1);
1671
- var clack7 = __toESM(require("@clack/prompts"), 1);
1672
- var import_config6 = require("@viberails/config");
1673
- var import_scanner = require("@viberails/scanner");
1674
- var import_chalk10 = __toESM(require("chalk"), 1);
1675
-
1676
- // src/display.ts
1677
- var import_types4 = require("@viberails/types");
1678
- var import_chalk6 = __toESM(require("chalk"), 1);
1679
-
1680
- // src/display-helpers.ts
1681
- var import_types = require("@viberails/types");
1682
- function groupByRole(directories) {
1683
- const map = /* @__PURE__ */ new Map();
1684
- for (const dir of directories) {
1685
- if (dir.role === "unknown") continue;
1686
- const existing = map.get(dir.role);
1687
- if (existing) {
1688
- existing.dirs.push(dir);
1678
+ function displayInitSummary(config, exemptedPackages) {
1679
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1680
+ const isMonorepo = config.packages.length > 1;
1681
+ const ok = import_chalk4.default.green("\u2713");
1682
+ const off = import_chalk4.default.dim("\u25CB");
1683
+ console.log("");
1684
+ console.log(` ${import_chalk4.default.bold("Rules to apply:")}`);
1685
+ console.log(` ${ok} Max file size: ${import_chalk4.default.cyan(`${config.rules.maxFileLines} lines`)}`);
1686
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1687
+ console.log(` ${ok} File naming: ${import_chalk4.default.cyan(root.conventions.fileNaming)}`);
1688
+ } else {
1689
+ console.log(` ${off} File naming: ${import_chalk4.default.dim("not enforced")}`);
1690
+ }
1691
+ if (config.rules.enforceMissingTests && root?.structure?.testPattern) {
1692
+ console.log(` ${ok} Missing tests: ${import_chalk4.default.cyan(`enforced (${root.structure.testPattern})`)}`);
1693
+ } else if (config.rules.enforceMissingTests) {
1694
+ console.log(` ${ok} Missing tests: ${import_chalk4.default.cyan("enforced")}`);
1695
+ } else {
1696
+ console.log(` ${off} Missing tests: ${import_chalk4.default.dim("not enforced")}`);
1697
+ }
1698
+ if (config.rules.testCoverage > 0) {
1699
+ if (isMonorepo) {
1700
+ const withCoverage = config.packages.filter(
1701
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
1702
+ );
1703
+ console.log(
1704
+ ` ${ok} Coverage: ${import_chalk4.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk4.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
1705
+ );
1689
1706
  } else {
1690
- map.set(dir.role, { dirs: [dir] });
1707
+ console.log(` ${ok} Coverage: ${import_chalk4.default.cyan(`${config.rules.testCoverage}%`)}`);
1691
1708
  }
1709
+ } else {
1710
+ console.log(` ${off} Coverage: ${import_chalk4.default.dim("disabled")}`);
1692
1711
  }
1693
- const groups = [];
1694
- for (const [role, { dirs }] of map) {
1695
- const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1696
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1697
- groups.push({
1698
- role,
1699
- label,
1700
- dirCount: dirs.length,
1701
- totalFiles,
1702
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
1703
- });
1704
- }
1705
- return groups;
1706
- }
1707
- function formatSummary(stats, packageCount) {
1708
- const parts = [];
1709
- if (packageCount && packageCount > 1) {
1710
- parts.push(`${packageCount} packages`);
1712
+ if (exemptedPackages.length > 0) {
1713
+ console.log(
1714
+ ` ${import_chalk4.default.dim(" exempted:")} ${import_chalk4.default.dim(exemptedPackages.join(", "))} ${import_chalk4.default.dim("(types-only)")}`
1715
+ );
1711
1716
  }
1712
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1713
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1714
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1715
- return parts.join(" \xB7 ");
1716
- }
1717
- function formatExtensions(filesByExtension, maxEntries = 4) {
1718
- return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1719
- }
1720
- function formatRoleGroup(group) {
1721
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1722
- if (group.singlePath) {
1723
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1717
+ if (isMonorepo) {
1718
+ console.log(
1719
+ `
1720
+ ${import_chalk4.default.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
1721
+ );
1722
+ } else {
1723
+ console.log(`
1724
+ ${import_chalk4.default.dim("warns on violation \xB7 use --enforce in CI to block")}`);
1724
1725
  }
1725
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1726
- return `${group.label} \u2014 ${dirs} (${files})`;
1726
+ console.log("");
1727
1727
  }
1728
1728
 
1729
- // src/display-monorepo.ts
1730
- var import_types3 = require("@viberails/types");
1731
- var import_chalk5 = __toESM(require("chalk"), 1);
1732
-
1733
1729
  // src/display-text.ts
1734
- var import_types2 = require("@viberails/types");
1735
1730
  function plainConfidenceLabel(convention) {
1736
1731
  const pct = Math.round(convention.consistency);
1737
1732
  if (convention.confidence === "high") {
@@ -1747,7 +1742,7 @@ function formatConventionsText(scanResult) {
1747
1742
  lines.push("Conventions:");
1748
1743
  for (const [key, convention] of conventionEntries) {
1749
1744
  if (convention.confidence === "low") continue;
1750
- const label = import_types2.CONVENTION_LABELS[key] ?? key;
1745
+ const label = import_types4.CONVENTION_LABELS[key] ?? key;
1751
1746
  if (scanResult.packages.length > 1) {
1752
1747
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1753
1748
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
@@ -1770,10 +1765,13 @@ function formatConventionsText(scanResult) {
1770
1765
  }
1771
1766
  function formatRulesText(config) {
1772
1767
  const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1768
+ const isMonorepo = config.packages.length > 1;
1773
1769
  const lines = [];
1774
1770
  lines.push(`Max file size: ${config.rules.maxFileLines} lines`);
1775
1771
  if (config.rules.testCoverage > 0) {
1776
- lines.push(`Test coverage target: ${config.rules.testCoverage}%`);
1772
+ const label = isMonorepo ? "Default coverage target" : "Test coverage target";
1773
+ const suffix = isMonorepo ? " (per-package)" : "";
1774
+ lines.push(`${label}: ${config.rules.testCoverage}%${suffix}`);
1777
1775
  } else {
1778
1776
  lines.push("Test coverage target: disabled");
1779
1777
  }
@@ -1798,17 +1796,17 @@ function formatScanResultsText(scanResult) {
1798
1796
  const { stack } = scanResult;
1799
1797
  lines.push("Detected:");
1800
1798
  if (stack.framework) {
1801
- lines.push(` \u2713 ${formatItem(stack.framework, import_types2.FRAMEWORK_NAMES)}`);
1799
+ lines.push(` \u2713 ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
1802
1800
  }
1803
1801
  lines.push(` \u2713 ${formatItem(stack.language)}`);
1804
1802
  if (stack.styling) {
1805
- lines.push(` \u2713 ${formatItem(stack.styling, import_types2.STYLING_NAMES)}`);
1803
+ lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
1806
1804
  }
1807
1805
  if (stack.backend) {
1808
- lines.push(` \u2713 ${formatItem(stack.backend, import_types2.FRAMEWORK_NAMES)}`);
1806
+ lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
1809
1807
  }
1810
1808
  if (stack.orm) {
1811
- lines.push(` \u2713 ${formatItem(stack.orm, import_types2.ORM_NAMES)}`);
1809
+ lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
1812
1810
  }
1813
1811
  const secondaryParts = [];
1814
1812
  if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
@@ -1820,7 +1818,7 @@ function formatScanResultsText(scanResult) {
1820
1818
  }
1821
1819
  if (stack.libraries.length > 0) {
1822
1820
  for (const lib of stack.libraries) {
1823
- lines.push(` \u2713 ${formatItem(lib, import_types2.LIBRARY_NAMES)}`);
1821
+ lines.push(` \u2713 ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
1824
1822
  }
1825
1823
  }
1826
1824
  const groups = groupByRole(scanResult.structure.directories);
@@ -1842,256 +1840,652 @@ function formatScanResultsText(scanResult) {
1842
1840
  return lines.join("\n");
1843
1841
  }
1844
1842
 
1845
- // src/display-monorepo.ts
1846
- function formatPackageSummary(pkg) {
1847
- const parts = [];
1848
- if (pkg.stack.framework) {
1849
- parts.push(formatItem(pkg.stack.framework, import_types3.FRAMEWORK_NAMES));
1843
+ // src/utils/apply-rule-overrides.ts
1844
+ function applyRuleOverrides(config, overrides) {
1845
+ if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
1846
+ config.rules.maxFileLines = overrides.maxFileLines;
1847
+ config.rules.testCoverage = overrides.testCoverage;
1848
+ config.rules.enforceMissingTests = overrides.enforceMissingTests;
1849
+ config.rules.enforceNaming = overrides.enforceNaming;
1850
+ for (const pkg of config.packages) {
1851
+ pkg.coverage = pkg.coverage ?? {};
1852
+ if (pkg.coverage.summaryPath === void 0) {
1853
+ pkg.coverage.summaryPath = overrides.coverageSummaryPath;
1854
+ }
1855
+ if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
1856
+ pkg.coverage.command = overrides.coverageCommand;
1857
+ }
1850
1858
  }
1851
- if (pkg.stack.styling) {
1852
- parts.push(formatItem(pkg.stack.styling, import_types3.STYLING_NAMES));
1859
+ if (overrides.fileNamingValue) {
1860
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1861
+ const oldNaming = rootPkg.conventions?.fileNaming;
1862
+ rootPkg.conventions = rootPkg.conventions ?? {};
1863
+ rootPkg.conventions.fileNaming = overrides.fileNamingValue;
1864
+ if (oldNaming && oldNaming !== overrides.fileNamingValue) {
1865
+ for (const pkg of config.packages) {
1866
+ if (pkg.conventions?.fileNaming === oldNaming) {
1867
+ pkg.conventions.fileNaming = overrides.fileNamingValue;
1868
+ }
1869
+ }
1870
+ }
1853
1871
  }
1854
- const files = `${pkg.statistics.totalFiles} files`;
1855
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1856
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1857
1872
  }
1858
- function displayMonorepoResults(scanResult) {
1859
- const { stack, packages } = scanResult;
1860
- console.log(`
1861
- ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1862
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
1863
- if (stack.packageManager) {
1864
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1873
+
1874
+ // src/utils/diff-configs.ts
1875
+ var import_types5 = require("@viberails/types");
1876
+ function parseStackString(s) {
1877
+ const atIdx = s.indexOf("@");
1878
+ if (atIdx > 0) {
1879
+ return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1865
1880
  }
1866
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1867
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1868
- } else {
1869
- if (stack.linter) {
1870
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
1881
+ return { name: s };
1882
+ }
1883
+ function displayStackName(s) {
1884
+ const { name, version } = parseStackString(s);
1885
+ const allMaps = {
1886
+ ...import_types5.FRAMEWORK_NAMES,
1887
+ ...import_types5.STYLING_NAMES,
1888
+ ...import_types5.ORM_NAMES
1889
+ };
1890
+ const display = allMaps[name] ?? name;
1891
+ return version ? `${display} ${version}` : display;
1892
+ }
1893
+ function isNewlyDetected(config, pkgPath, key) {
1894
+ return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
1895
+ }
1896
+ var STACK_FIELDS = [
1897
+ "framework",
1898
+ "styling",
1899
+ "backend",
1900
+ "orm",
1901
+ "linter",
1902
+ "formatter",
1903
+ "testRunner"
1904
+ ];
1905
+ var CONVENTION_KEYS = [
1906
+ "fileNaming",
1907
+ "componentNaming",
1908
+ "hookNaming",
1909
+ "importAlias"
1910
+ ];
1911
+ var STRUCTURE_FIELDS = [
1912
+ { key: "srcDir", label: "source directory" },
1913
+ { key: "pages", label: "pages directory" },
1914
+ { key: "components", label: "components directory" },
1915
+ { key: "hooks", label: "hooks directory" },
1916
+ { key: "utils", label: "utilities directory" },
1917
+ { key: "types", label: "types directory" },
1918
+ { key: "tests", label: "tests directory" },
1919
+ { key: "testPattern", label: "test pattern" }
1920
+ ];
1921
+ function diffPackage(existing, merged, mergedConfig) {
1922
+ const changes = [];
1923
+ const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
1924
+ for (const field of STACK_FIELDS) {
1925
+ const oldVal = existing.stack?.[field];
1926
+ const newVal = merged.stack?.[field];
1927
+ if (!oldVal && newVal) {
1928
+ changes.push({
1929
+ type: "added",
1930
+ description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
1931
+ });
1932
+ } else if (oldVal && newVal && oldVal !== newVal) {
1933
+ changes.push({
1934
+ type: "changed",
1935
+ description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1936
+ });
1871
1937
  }
1872
- if (stack.formatter) {
1873
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1938
+ }
1939
+ for (const key of CONVENTION_KEYS) {
1940
+ const oldVal = existing.conventions?.[key];
1941
+ const newVal = merged.conventions?.[key];
1942
+ const label = import_types5.CONVENTION_LABELS[key] ?? key;
1943
+ if (!oldVal && newVal) {
1944
+ changes.push({
1945
+ type: "added",
1946
+ description: `${pkgPrefix}New convention: ${label} (${newVal})`
1947
+ });
1948
+ } else if (oldVal && newVal && oldVal !== newVal) {
1949
+ const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
1950
+ changes.push({
1951
+ type: "changed",
1952
+ description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
1953
+ });
1874
1954
  }
1875
1955
  }
1876
- if (stack.testRunner) {
1877
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1956
+ for (const { key, label } of STRUCTURE_FIELDS) {
1957
+ const oldVal = existing.structure?.[key];
1958
+ const newVal = merged.structure?.[key];
1959
+ if (!oldVal && newVal) {
1960
+ changes.push({
1961
+ type: "added",
1962
+ description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
1963
+ });
1964
+ }
1878
1965
  }
1879
- console.log("");
1880
- for (const pkg of packages) {
1881
- console.log(formatPackageSummary(pkg));
1966
+ return changes;
1967
+ }
1968
+ function diffConfigs(existing, merged) {
1969
+ const changes = [];
1970
+ const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
1971
+ const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
1972
+ for (const existingPkg of existing.packages) {
1973
+ const mergedPkg = mergedByPath.get(existingPkg.path);
1974
+ if (mergedPkg) {
1975
+ changes.push(...diffPackage(existingPkg, mergedPkg, merged));
1976
+ }
1882
1977
  }
1883
- const packagesWithDirs = packages.filter(
1884
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1885
- );
1886
- if (packagesWithDirs.length > 0) {
1887
- console.log(`
1888
- ${import_chalk5.default.bold("Structure:")}`);
1889
- for (const pkg of packagesWithDirs) {
1890
- const groups = groupByRole(pkg.structure.directories);
1891
- if (groups.length === 0) continue;
1892
- console.log(` ${pkg.relativePath}:`);
1893
- for (const group of groups) {
1894
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
1895
- }
1978
+ for (const mergedPkg of merged.packages) {
1979
+ if (!existingByPath.has(mergedPkg.path)) {
1980
+ changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
1896
1981
  }
1897
1982
  }
1898
- displayConventions(scanResult);
1899
- displaySummarySection(scanResult);
1900
- console.log("");
1983
+ return changes;
1901
1984
  }
1902
- function formatPackageSummaryPlain(pkg) {
1985
+ function formatStatsDelta(oldStats, newStats) {
1986
+ const fileDelta = newStats.totalFiles - oldStats.totalFiles;
1987
+ const lineDelta = newStats.totalLines - oldStats.totalLines;
1988
+ if (fileDelta === 0 && lineDelta === 0) return void 0;
1903
1989
  const parts = [];
1904
- if (pkg.stack.framework) {
1905
- parts.push(formatItem(pkg.stack.framework, import_types3.FRAMEWORK_NAMES));
1990
+ if (fileDelta !== 0) {
1991
+ const sign = fileDelta > 0 ? "+" : "";
1992
+ parts.push(`${sign}${fileDelta.toLocaleString()} files`);
1906
1993
  }
1907
- if (pkg.stack.styling) {
1908
- parts.push(formatItem(pkg.stack.styling, import_types3.STYLING_NAMES));
1994
+ if (lineDelta !== 0) {
1995
+ const sign = lineDelta > 0 ? "+" : "";
1996
+ parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
1909
1997
  }
1910
- const files = `${pkg.statistics.totalFiles} files`;
1911
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1912
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1998
+ return `${parts.join(", ")} since last sync`;
1913
1999
  }
1914
- function formatMonorepoResultsText(scanResult) {
1915
- const lines = [];
1916
- const { stack, packages } = scanResult;
1917
- lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1918
- const sharedParts = [formatItem(stack.language)];
1919
- if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1920
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1921
- sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
1922
- } else {
1923
- if (stack.linter) sharedParts.push(formatItem(stack.linter));
1924
- if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
2000
+
2001
+ // src/utils/write-generated-files.ts
2002
+ var fs9 = __toESM(require("fs"), 1);
2003
+ var path8 = __toESM(require("path"), 1);
2004
+ var import_context = require("@viberails/context");
2005
+ var CONTEXT_DIR = ".viberails";
2006
+ var CONTEXT_FILE = "context.md";
2007
+ var SCAN_RESULT_FILE = "scan-result.json";
2008
+ function writeGeneratedFiles(projectRoot, config, scanResult) {
2009
+ const contextDir = path8.join(projectRoot, CONTEXT_DIR);
2010
+ try {
2011
+ if (!fs9.existsSync(contextDir)) {
2012
+ fs9.mkdirSync(contextDir, { recursive: true });
2013
+ }
2014
+ const context = (0, import_context.generateContext)(config);
2015
+ fs9.writeFileSync(path8.join(contextDir, CONTEXT_FILE), context);
2016
+ fs9.writeFileSync(
2017
+ path8.join(contextDir, SCAN_RESULT_FILE),
2018
+ `${JSON.stringify(scanResult, null, 2)}
2019
+ `
2020
+ );
2021
+ } catch (err) {
2022
+ const message = err instanceof Error ? err.message : String(err);
2023
+ throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
1925
2024
  }
1926
- if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1927
- lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1928
- lines.push("");
1929
- for (const pkg of packages) {
1930
- lines.push(formatPackageSummaryPlain(pkg));
2025
+ }
2026
+
2027
+ // src/commands/config.ts
2028
+ var CONFIG_FILE3 = "viberails.config.json";
2029
+ async function configCommand(options, cwd) {
2030
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
2031
+ if (!projectRoot) {
2032
+ throw new Error("No package.json found. Make sure you are inside a JS/TS project.");
1931
2033
  }
1932
- const packagesWithDirs = packages.filter(
1933
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1934
- );
1935
- if (packagesWithDirs.length > 0) {
1936
- lines.push("");
1937
- lines.push("Structure:");
1938
- for (const pkg of packagesWithDirs) {
1939
- const groups = groupByRole(pkg.structure.directories);
1940
- if (groups.length === 0) continue;
1941
- lines.push(` ${pkg.relativePath}:`);
1942
- for (const group of groups) {
1943
- lines.push(` \u2713 ${formatRoleGroup(group)}`);
2034
+ const configPath = path9.join(projectRoot, CONFIG_FILE3);
2035
+ if (!fs10.existsSync(configPath)) {
2036
+ console.log(`${import_chalk5.default.yellow("!")} No config found. Run ${import_chalk5.default.cyan("viberails init")} first.`);
2037
+ return;
2038
+ }
2039
+ clack6.intro("viberails config");
2040
+ const config = await (0, import_config6.loadConfig)(configPath);
2041
+ let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2042
+ clack6.note(formatRulesText(config).join("\n"), "Current rules");
2043
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2044
+ const overrides = await promptRuleMenu({
2045
+ maxFileLines: config.rules.maxFileLines,
2046
+ testCoverage: config.rules.testCoverage,
2047
+ enforceMissingTests: config.rules.enforceMissingTests,
2048
+ enforceNaming: config.rules.enforceNaming,
2049
+ fileNamingValue: rootPkg.conventions?.fileNaming,
2050
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
2051
+ coverageCommand: config.defaults?.coverage?.command,
2052
+ packageOverrides: config.packages
2053
+ });
2054
+ applyRuleOverrides(config, overrides);
2055
+ if (options.rescan && config.packages.length > 1) {
2056
+ const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
2057
+ if (shouldInfer) {
2058
+ const bs = clack6.spinner();
2059
+ bs.start("Building import graph...");
2060
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2061
+ const packages = resolveWorkspacePackages(projectRoot, config.packages);
2062
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
2063
+ const inferred = inferBoundaries(graph);
2064
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
2065
+ if (denyCount > 0) {
2066
+ config.boundaries = inferred;
2067
+ config.rules.enforceBoundaries = true;
2068
+ bs.stop(`Inferred ${denyCount} boundary rules`);
2069
+ } else {
2070
+ bs.stop("No boundary rules inferred");
1944
2071
  }
1945
2072
  }
1946
2073
  }
1947
- lines.push(...formatConventionsText(scanResult));
1948
- const pkgCount = packages.length > 1 ? packages.length : void 0;
1949
- lines.push("");
1950
- lines.push(formatSummary(scanResult.statistics, pkgCount));
1951
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
1952
- if (ext) {
1953
- lines.push(ext);
2074
+ const shouldWrite = await confirm3("Save updated configuration?");
2075
+ if (!shouldWrite) {
2076
+ clack6.outro("No changes written.");
2077
+ return;
2078
+ }
2079
+ const compacted = (0, import_config6.compactConfig)(config);
2080
+ fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2081
+ `);
2082
+ if (!scanResult) {
2083
+ const s = clack6.spinner();
2084
+ s.start("Scanning for context generation...");
2085
+ scanResult = await (0, import_scanner.scan)(projectRoot);
2086
+ s.stop("Scan complete");
2087
+ }
2088
+ writeGeneratedFiles(projectRoot, config, scanResult);
2089
+ clack6.log.success(
2090
+ `Updated:
2091
+ ${CONFIG_FILE3}
2092
+ .viberails/context.md
2093
+ .viberails/scan-result.json`
2094
+ );
2095
+ clack6.outro("Done! Run viberails check to verify.");
2096
+ }
2097
+ async function rescanAndMerge(projectRoot, config) {
2098
+ const s = clack6.spinner();
2099
+ s.start("Re-scanning project...");
2100
+ const scanResult = await (0, import_scanner.scan)(projectRoot);
2101
+ const merged = (0, import_config6.mergeConfig)(config, scanResult);
2102
+ s.stop("Scan complete");
2103
+ const changes = diffConfigs(config, merged);
2104
+ if (changes.length > 0) {
2105
+ const changeLines = changes.map((c) => {
2106
+ const icon = c.type === "removed" ? "-" : "+";
2107
+ return `${icon} ${c.description}`;
2108
+ }).join("\n");
2109
+ clack6.note(changeLines, "Changes detected");
2110
+ } else {
2111
+ clack6.log.info("No new changes detected from scan.");
2112
+ }
2113
+ Object.assign(config, merged);
2114
+ return scanResult;
2115
+ }
2116
+
2117
+ // src/commands/fix.ts
2118
+ var fs13 = __toESM(require("fs"), 1);
2119
+ var path13 = __toESM(require("path"), 1);
2120
+ var import_config7 = require("@viberails/config");
2121
+ var import_chalk7 = __toESM(require("chalk"), 1);
2122
+
2123
+ // src/commands/fix-helpers.ts
2124
+ var import_node_child_process4 = require("child_process");
2125
+ var import_chalk6 = __toESM(require("chalk"), 1);
2126
+ function printPlan(renames, stubs) {
2127
+ if (renames.length > 0) {
2128
+ console.log(import_chalk6.default.bold("\nFile renames:"));
2129
+ for (const r of renames) {
2130
+ console.log(` ${import_chalk6.default.red(r.oldPath)} \u2192 ${import_chalk6.default.green(r.newPath)}`);
2131
+ }
2132
+ }
2133
+ if (stubs.length > 0) {
2134
+ console.log(import_chalk6.default.bold("\nTest stubs to create:"));
2135
+ for (const s of stubs) {
2136
+ console.log(` ${import_chalk6.default.green("+")} ${s.path}`);
2137
+ }
2138
+ }
2139
+ }
2140
+ function checkGitDirty(projectRoot) {
2141
+ try {
2142
+ const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
2143
+ cwd: projectRoot,
2144
+ encoding: "utf-8",
2145
+ stdio: ["ignore", "pipe", "ignore"]
2146
+ });
2147
+ return output.trim().length > 0;
2148
+ } catch {
2149
+ return false;
2150
+ }
2151
+ }
2152
+ function getConventionValue(convention) {
2153
+ if (typeof convention === "string") return convention;
2154
+ return void 0;
2155
+ }
2156
+
2157
+ // src/commands/fix-imports.ts
2158
+ var path10 = __toESM(require("path"), 1);
2159
+ function stripExtension(filePath) {
2160
+ return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2161
+ }
2162
+ function computeNewSpecifier(oldSpecifier, newBare) {
2163
+ const hasJsExt = oldSpecifier.endsWith(".js");
2164
+ const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
2165
+ const dir = base.lastIndexOf("/");
2166
+ const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
2167
+ const newSpec = prefix + newBare;
2168
+ return hasJsExt ? `${newSpec}.js` : newSpec;
2169
+ }
2170
+ async function updateImportsAfterRenames(renames, projectRoot) {
2171
+ if (renames.length === 0) return [];
2172
+ const { Project, SyntaxKind } = await import("ts-morph");
2173
+ const renameMap = /* @__PURE__ */ new Map();
2174
+ for (const r of renames) {
2175
+ const oldStripped = stripExtension(r.oldAbsPath);
2176
+ const newFilename = path10.basename(r.newPath);
2177
+ const newName = newFilename.slice(0, newFilename.indexOf("."));
2178
+ renameMap.set(oldStripped, { newBare: newName });
2179
+ }
2180
+ const project = new Project({
2181
+ tsConfigFilePath: void 0,
2182
+ skipAddingFilesFromTsConfig: true
2183
+ });
2184
+ project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
2185
+ const updates = [];
2186
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
2187
+ for (const sourceFile of project.getSourceFiles()) {
2188
+ const filePath = sourceFile.getFilePath();
2189
+ const segments = filePath.split(path10.sep);
2190
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
2191
+ const fileDir = path10.dirname(filePath);
2192
+ for (const decl of sourceFile.getImportDeclarations()) {
2193
+ const specifier = decl.getModuleSpecifierValue();
2194
+ if (!specifier.startsWith(".")) continue;
2195
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2196
+ if (!match) continue;
2197
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2198
+ updates.push({
2199
+ file: filePath,
2200
+ oldSpecifier: specifier,
2201
+ newSpecifier: newSpec,
2202
+ line: decl.getStartLineNumber()
2203
+ });
2204
+ decl.setModuleSpecifier(newSpec);
2205
+ }
2206
+ for (const decl of sourceFile.getExportDeclarations()) {
2207
+ const specifier = decl.getModuleSpecifierValue();
2208
+ if (!specifier || !specifier.startsWith(".")) continue;
2209
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2210
+ if (!match) continue;
2211
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2212
+ updates.push({
2213
+ file: filePath,
2214
+ oldSpecifier: specifier,
2215
+ newSpecifier: newSpec,
2216
+ line: decl.getStartLineNumber()
2217
+ });
2218
+ decl.setModuleSpecifier(newSpec);
2219
+ }
2220
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2221
+ if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
2222
+ const args = call.getArguments();
2223
+ if (args.length === 0) continue;
2224
+ const arg = args[0];
2225
+ if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
2226
+ const specifier = arg.getText().slice(1, -1);
2227
+ if (!specifier.startsWith(".")) continue;
2228
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2229
+ if (!match) continue;
2230
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2231
+ updates.push({
2232
+ file: filePath,
2233
+ oldSpecifier: specifier,
2234
+ newSpecifier: newSpec,
2235
+ line: call.getStartLineNumber()
2236
+ });
2237
+ const quote = arg.getText()[0];
2238
+ arg.replaceWithText(`${quote}${newSpec}${quote}`);
2239
+ }
2240
+ }
2241
+ if (updates.length > 0) {
2242
+ await project.save();
1954
2243
  }
1955
- return lines.join("\n");
1956
- }
1957
-
1958
- // src/display.ts
1959
- function formatItem(item, nameMap) {
1960
- const name = nameMap?.[item.name] ?? item.name;
1961
- return item.version ? `${name} ${item.version}` : name;
2244
+ return updates;
1962
2245
  }
1963
- function confidenceLabel(convention) {
1964
- const pct = Math.round(convention.consistency);
1965
- if (convention.confidence === "high") {
1966
- return `${pct}% \u2014 high confidence, will enforce`;
2246
+ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
2247
+ const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
2248
+ const resolved = path10.resolve(fromDir, cleanSpec);
2249
+ for (const ext of extensions) {
2250
+ const candidate = resolved + ext;
2251
+ const stripped = stripExtension(candidate);
2252
+ const match = renameMap.get(stripped);
2253
+ if (match) return match;
1967
2254
  }
1968
- return `${pct}% \u2014 medium confidence, suggested only`;
2255
+ return void 0;
1969
2256
  }
1970
- function displayConventions(scanResult) {
1971
- const conventionEntries = Object.entries(scanResult.conventions);
1972
- if (conventionEntries.length === 0) return;
1973
- console.log(`
1974
- ${import_chalk6.default.bold("Conventions:")}`);
1975
- for (const [key, convention] of conventionEntries) {
1976
- if (convention.confidence === "low") continue;
1977
- const label = import_types4.CONVENTION_LABELS[key] ?? key;
1978
- if (scanResult.packages.length > 1) {
1979
- const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1980
- const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1981
- if (allSame || pkgValues.length <= 1) {
1982
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1983
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1984
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1985
- } else {
1986
- console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
1987
- for (const pv of pkgValues) {
1988
- const pct = Math.round(pv.convention.consistency);
1989
- console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
2257
+
2258
+ // src/commands/fix-naming.ts
2259
+ var fs11 = __toESM(require("fs"), 1);
2260
+ var path11 = __toESM(require("path"), 1);
2261
+
2262
+ // src/commands/convert-name.ts
2263
+ function splitIntoWords(name) {
2264
+ const parts = name.split(/[-_]/);
2265
+ const words = [];
2266
+ for (const part of parts) {
2267
+ if (part === "") continue;
2268
+ let current = "";
2269
+ for (let i = 0; i < part.length; i++) {
2270
+ const ch = part[i];
2271
+ const isUpper = ch >= "A" && ch <= "Z";
2272
+ if (isUpper && current.length > 0) {
2273
+ const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
2274
+ const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
2275
+ if (!prevIsUpper || nextIsLower) {
2276
+ words.push(current.toLowerCase());
2277
+ current = "";
1990
2278
  }
1991
2279
  }
1992
- } else {
1993
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1994
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1995
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
2280
+ current += ch;
1996
2281
  }
2282
+ if (current) words.push(current.toLowerCase());
1997
2283
  }
2284
+ return words;
1998
2285
  }
1999
- function displaySummarySection(scanResult) {
2000
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
2001
- console.log(`
2002
- ${import_chalk6.default.bold("Summary:")}`);
2003
- console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
2004
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
2005
- if (ext) {
2006
- console.log(` ${ext}`);
2286
+ function convertName(bare, target) {
2287
+ const words = splitIntoWords(bare);
2288
+ if (words.length === 0) return bare;
2289
+ switch (target) {
2290
+ case "kebab-case":
2291
+ return words.join("-");
2292
+ case "camelCase":
2293
+ return words[0] + words.slice(1).map(capitalize).join("");
2294
+ case "PascalCase":
2295
+ return words.map(capitalize).join("");
2296
+ case "snake_case":
2297
+ return words.join("_");
2298
+ default:
2299
+ return bare;
2007
2300
  }
2008
2301
  }
2009
- function displayScanResults(scanResult) {
2010
- if (scanResult.packages.length > 1) {
2011
- displayMonorepoResults(scanResult);
2012
- return;
2013
- }
2014
- const { stack } = scanResult;
2015
- console.log(`
2016
- ${import_chalk6.default.bold("Detected:")}`);
2017
- if (stack.framework) {
2018
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
2302
+ function capitalize(word) {
2303
+ if (word.length === 0) return word;
2304
+ return word[0].toUpperCase() + word.slice(1);
2305
+ }
2306
+
2307
+ // src/commands/fix-naming.ts
2308
+ function computeRename(relPath, targetConvention, projectRoot) {
2309
+ const filename = path11.basename(relPath);
2310
+ const dir = path11.dirname(relPath);
2311
+ const dotIndex = filename.indexOf(".");
2312
+ if (dotIndex === -1) return null;
2313
+ const bare = filename.slice(0, dotIndex);
2314
+ const suffix = filename.slice(dotIndex);
2315
+ const newBare = convertName(bare, targetConvention);
2316
+ if (newBare === bare) return null;
2317
+ const newFilename = newBare + suffix;
2318
+ const newRelPath = path11.join(dir, newFilename);
2319
+ const oldAbsPath = path11.join(projectRoot, relPath);
2320
+ const newAbsPath = path11.join(projectRoot, newRelPath);
2321
+ if (fs11.existsSync(newAbsPath)) return null;
2322
+ return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
2323
+ }
2324
+ function executeRename(rename) {
2325
+ if (fs11.existsSync(rename.newAbsPath)) return false;
2326
+ fs11.renameSync(rename.oldAbsPath, rename.newAbsPath);
2327
+ return true;
2328
+ }
2329
+ function deduplicateRenames(renames) {
2330
+ const seen = /* @__PURE__ */ new Set();
2331
+ const result = [];
2332
+ for (const r of renames) {
2333
+ if (seen.has(r.newAbsPath)) continue;
2334
+ seen.add(r.newAbsPath);
2335
+ result.push(r);
2019
2336
  }
2020
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
2021
- if (stack.styling) {
2022
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
2337
+ return result;
2338
+ }
2339
+
2340
+ // src/commands/fix-tests.ts
2341
+ var fs12 = __toESM(require("fs"), 1);
2342
+ var path12 = __toESM(require("path"), 1);
2343
+ function generateTestStub(sourceRelPath, config, projectRoot) {
2344
+ const pkg = resolvePackageForFile(sourceRelPath, config);
2345
+ const testPattern = pkg?.structure?.testPattern;
2346
+ if (!testPattern) return null;
2347
+ const basename8 = path12.basename(sourceRelPath);
2348
+ const ext = path12.extname(basename8);
2349
+ if (!ext) return null;
2350
+ const stem = basename8.slice(0, -ext.length);
2351
+ const testSuffix = testPattern.replace("*", "");
2352
+ const testFilename = `${stem}${testSuffix}`;
2353
+ const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
2354
+ const testAbsPath = path12.join(dir, testFilename);
2355
+ if (fs12.existsSync(testAbsPath)) return null;
2356
+ return {
2357
+ path: path12.relative(projectRoot, testAbsPath),
2358
+ absPath: testAbsPath,
2359
+ moduleName: stem
2360
+ };
2361
+ }
2362
+ function writeTestStub(stub, config) {
2363
+ const pkg = resolvePackageForFile(stub.path, config);
2364
+ const testRunner = pkg?.stack?.testRunner ?? "";
2365
+ const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
2366
+ const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
2367
+ const content = `${importLine}describe('${stub.moduleName}', () => {
2368
+ it.todo('add tests');
2369
+ });
2370
+ `;
2371
+ fs12.mkdirSync(path12.dirname(stub.absPath), { recursive: true });
2372
+ fs12.writeFileSync(stub.absPath, content);
2373
+ }
2374
+
2375
+ // src/commands/fix.ts
2376
+ var CONFIG_FILE4 = "viberails.config.json";
2377
+ async function fixCommand(options, cwd) {
2378
+ const startDir = cwd ?? process.cwd();
2379
+ const projectRoot = findProjectRoot(startDir);
2380
+ if (!projectRoot) {
2381
+ console.error(`${import_chalk7.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
2382
+ return 1;
2023
2383
  }
2024
- if (stack.backend) {
2025
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
2384
+ const configPath = path13.join(projectRoot, CONFIG_FILE4);
2385
+ if (!fs13.existsSync(configPath)) {
2386
+ console.error(
2387
+ `${import_chalk7.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
2388
+ );
2389
+ return 1;
2026
2390
  }
2027
- if (stack.orm) {
2028
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
2391
+ const config = await (0, import_config7.loadConfig)(configPath);
2392
+ if (!options.dryRun) {
2393
+ const isDirty = checkGitDirty(projectRoot);
2394
+ if (isDirty) {
2395
+ console.log(
2396
+ import_chalk7.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
2397
+ );
2398
+ }
2029
2399
  }
2030
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
2031
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
2032
- } else {
2033
- if (stack.linter) {
2034
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
2400
+ const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
2401
+ const shouldFixTests = !options.rule || options.rule.includes("missing-test");
2402
+ const allFiles = getAllSourceFiles(projectRoot, config);
2403
+ const renames = [];
2404
+ if (shouldFixNaming) {
2405
+ for (const file of allFiles) {
2406
+ const resolved = resolveConfigForFile(file, config);
2407
+ if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
2408
+ const violation = checkNaming(file, resolved.conventions);
2409
+ if (!violation) continue;
2410
+ const convention = getConventionValue(resolved.conventions.fileNaming);
2411
+ if (!convention) continue;
2412
+ const rename = computeRename(file, convention, projectRoot);
2413
+ if (rename) renames.push(rename);
2035
2414
  }
2036
- if (stack.formatter) {
2037
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
2415
+ }
2416
+ const dedupedRenames = deduplicateRenames(renames);
2417
+ const testStubs = [];
2418
+ if (shouldFixTests) {
2419
+ const testViolations = checkMissingTests(projectRoot, config, "warn");
2420
+ for (const v of testViolations) {
2421
+ const stub = generateTestStub(v.file, config, projectRoot);
2422
+ if (stub) testStubs.push(stub);
2038
2423
  }
2039
2424
  }
2040
- if (stack.testRunner) {
2041
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
2425
+ if (dedupedRenames.length === 0 && testStubs.length === 0) {
2426
+ console.log(`${import_chalk7.default.green("\u2713")} No fixable violations found.`);
2427
+ return 0;
2042
2428
  }
2043
- if (stack.packageManager) {
2044
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
2429
+ printPlan(dedupedRenames, testStubs);
2430
+ if (options.dryRun) {
2431
+ console.log(import_chalk7.default.dim("\nDry run \u2014 no changes applied."));
2432
+ return 0;
2045
2433
  }
2046
- if (stack.libraries.length > 0) {
2047
- for (const lib of stack.libraries) {
2048
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
2434
+ if (!options.yes) {
2435
+ const confirmed = await confirmDangerous("Apply these fixes?");
2436
+ if (!confirmed) {
2437
+ console.log("Aborted.");
2438
+ return 0;
2049
2439
  }
2050
2440
  }
2051
- const groups = groupByRole(scanResult.structure.directories);
2052
- if (groups.length > 0) {
2053
- console.log(`
2054
- ${import_chalk6.default.bold("Structure:")}`);
2055
- for (const group of groups) {
2056
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
2441
+ let renameCount = 0;
2442
+ for (const rename of dedupedRenames) {
2443
+ if (executeRename(rename)) {
2444
+ renameCount++;
2057
2445
  }
2058
2446
  }
2059
- displayConventions(scanResult);
2060
- displaySummarySection(scanResult);
2061
- console.log("");
2062
- }
2063
- function displayRulesPreview(config) {
2064
- const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2065
- console.log(
2066
- `${import_chalk6.default.bold("Rules:")} ${import_chalk6.default.dim("(warns on violation; use --enforce in CI to block)")}`
2067
- );
2068
- console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
2069
- if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
2447
+ let importUpdateCount = 0;
2448
+ if (renameCount > 0) {
2449
+ const appliedRenames = dedupedRenames.filter((r) => fs13.existsSync(r.newAbsPath));
2450
+ const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
2451
+ importUpdateCount = updates.length;
2452
+ }
2453
+ let stubCount = 0;
2454
+ for (const stub of testStubs) {
2455
+ if (!fs13.existsSync(stub.absPath)) {
2456
+ writeTestStub(stub, config);
2457
+ stubCount++;
2458
+ }
2459
+ }
2460
+ console.log("");
2461
+ if (renameCount > 0) {
2462
+ console.log(`${import_chalk7.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
2463
+ }
2464
+ if (importUpdateCount > 0) {
2070
2465
  console.log(
2071
- ` ${import_chalk6.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
2466
+ `${import_chalk7.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
2072
2467
  );
2073
- } else if (config.rules.testCoverage > 0) {
2074
- console.log(` ${import_chalk6.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
2075
- } else {
2076
- console.log(` ${import_chalk6.default.dim("\u2022")} Test coverage target: disabled`);
2077
2468
  }
2078
- if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
2079
- console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
2080
- } else {
2081
- console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
2469
+ if (stubCount > 0) {
2470
+ console.log(`${import_chalk7.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
2082
2471
  }
2083
- console.log(
2084
- ` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
2085
- );
2086
- console.log("");
2472
+ return 0;
2087
2473
  }
2088
2474
 
2475
+ // src/commands/init.ts
2476
+ var fs18 = __toESM(require("fs"), 1);
2477
+ var path18 = __toESM(require("path"), 1);
2478
+ var clack8 = __toESM(require("@clack/prompts"), 1);
2479
+ var import_config8 = require("@viberails/config");
2480
+ var import_scanner2 = require("@viberails/scanner");
2481
+ var import_chalk11 = __toESM(require("chalk"), 1);
2482
+
2089
2483
  // src/utils/check-prerequisites.ts
2090
- var import_node_child_process4 = require("child_process");
2091
- var fs12 = __toESM(require("fs"), 1);
2092
- var path12 = __toESM(require("path"), 1);
2093
- var clack6 = __toESM(require("@clack/prompts"), 1);
2094
- var import_chalk7 = __toESM(require("chalk"), 1);
2484
+ var import_node_child_process5 = require("child_process");
2485
+ var fs14 = __toESM(require("fs"), 1);
2486
+ var path14 = __toESM(require("path"), 1);
2487
+ var clack7 = __toESM(require("@clack/prompts"), 1);
2488
+ var import_chalk8 = __toESM(require("chalk"), 1);
2095
2489
  function checkCoveragePrereqs(projectRoot, scanResult) {
2096
2490
  const testRunner = scanResult.stack.testRunner;
2097
2491
  if (!testRunner) return [];
@@ -2116,9 +2510,9 @@ function checkCoveragePrereqs(projectRoot, scanResult) {
2116
2510
  function displayMissingPrereqs(prereqs) {
2117
2511
  const missing = prereqs.filter((p) => !p.installed);
2118
2512
  for (const m of missing) {
2119
- console.log(` ${import_chalk7.default.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
2513
+ console.log(` ${import_chalk8.default.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
2120
2514
  if (m.installCommand) {
2121
- console.log(` Install: ${import_chalk7.default.cyan(m.installCommand)}`);
2515
+ console.log(` Install: ${import_chalk8.default.cyan(m.installCommand)}`);
2122
2516
  }
2123
2517
  }
2124
2518
  }
@@ -2128,11 +2522,11 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2128
2522
  const prereqLines = prereqs.map(
2129
2523
  (p) => `${p.installed ? "\u2713" : "\u2717"} ${p.label}${p.installed ? "" : ` \u2014 ${p.reason}`}`
2130
2524
  ).join("\n");
2131
- clack6.note(prereqLines, "Coverage prerequisites");
2525
+ clack7.note(prereqLines, "Coverage prerequisites");
2132
2526
  let disableCoverage = false;
2133
2527
  for (const m of missing) {
2134
2528
  if (!m.installCommand) continue;
2135
- const choice = await clack6.select({
2529
+ const choice = await clack7.select({
2136
2530
  message: `${m.label} is not installed. It is required for coverage percentage checks.`,
2137
2531
  options: [
2138
2532
  {
@@ -2154,9 +2548,9 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2154
2548
  });
2155
2549
  assertNotCancelled(choice);
2156
2550
  if (choice === "install") {
2157
- const is = clack6.spinner();
2551
+ const is = clack7.spinner();
2158
2552
  is.start(`Installing ${m.label}...`);
2159
- const result = (0, import_node_child_process4.spawnSync)(m.installCommand, {
2553
+ const result = (0, import_node_child_process5.spawnSync)(m.installCommand, {
2160
2554
  cwd: projectRoot,
2161
2555
  shell: true,
2162
2556
  encoding: "utf-8",
@@ -2166,16 +2560,16 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2166
2560
  is.stop(`Installed ${m.label}`);
2167
2561
  } else {
2168
2562
  is.stop(`Failed to install ${m.label}`);
2169
- clack6.log.warn(
2563
+ clack7.log.warn(
2170
2564
  `Install manually: ${m.installCommand}
2171
2565
  Coverage percentage checks will not work until the dependency is installed.`
2172
2566
  );
2173
2567
  }
2174
2568
  } else if (choice === "disable") {
2175
2569
  disableCoverage = true;
2176
- clack6.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2570
+ clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2177
2571
  } else {
2178
- clack6.log.info(
2572
+ clack7.log.info(
2179
2573
  `Coverage percentage checks will fail until ${m.label} is installed.
2180
2574
  Install later: ${m.installCommand}`
2181
2575
  );
@@ -2185,8 +2579,8 @@ Install later: ${m.installCommand}`
2185
2579
  }
2186
2580
  function hasDependency(projectRoot, name) {
2187
2581
  try {
2188
- const pkgPath = path12.join(projectRoot, "package.json");
2189
- const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2582
+ const pkgPath = path14.join(projectRoot, "package.json");
2583
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2190
2584
  return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2191
2585
  } catch {
2192
2586
  return false;
@@ -2208,84 +2602,58 @@ function filterHighConfidence(conventions, meta) {
2208
2602
  }
2209
2603
 
2210
2604
  // src/utils/update-gitignore.ts
2211
- var fs13 = __toESM(require("fs"), 1);
2212
- var path13 = __toESM(require("path"), 1);
2605
+ var fs15 = __toESM(require("fs"), 1);
2606
+ var path15 = __toESM(require("path"), 1);
2213
2607
  function updateGitignore(projectRoot) {
2214
- const gitignorePath = path13.join(projectRoot, ".gitignore");
2608
+ const gitignorePath = path15.join(projectRoot, ".gitignore");
2215
2609
  let content = "";
2216
- if (fs13.existsSync(gitignorePath)) {
2217
- content = fs13.readFileSync(gitignorePath, "utf-8");
2610
+ if (fs15.existsSync(gitignorePath)) {
2611
+ content = fs15.readFileSync(gitignorePath, "utf-8");
2218
2612
  }
2219
2613
  if (!content.includes(".viberails/scan-result.json")) {
2220
2614
  const block = "\n# viberails\n.viberails/scan-result.json\n";
2221
2615
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
2222
2616
  `;
2223
- fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
2224
- }
2225
- }
2226
-
2227
- // src/utils/write-generated-files.ts
2228
- var fs14 = __toESM(require("fs"), 1);
2229
- var path14 = __toESM(require("path"), 1);
2230
- var import_context = require("@viberails/context");
2231
- var CONTEXT_DIR = ".viberails";
2232
- var CONTEXT_FILE = "context.md";
2233
- var SCAN_RESULT_FILE = "scan-result.json";
2234
- function writeGeneratedFiles(projectRoot, config, scanResult) {
2235
- const contextDir = path14.join(projectRoot, CONTEXT_DIR);
2236
- try {
2237
- if (!fs14.existsSync(contextDir)) {
2238
- fs14.mkdirSync(contextDir, { recursive: true });
2239
- }
2240
- const context = (0, import_context.generateContext)(config);
2241
- fs14.writeFileSync(path14.join(contextDir, CONTEXT_FILE), context);
2242
- fs14.writeFileSync(
2243
- path14.join(contextDir, SCAN_RESULT_FILE),
2244
- `${JSON.stringify(scanResult, null, 2)}
2245
- `
2246
- );
2247
- } catch (err) {
2248
- const message = err instanceof Error ? err.message : String(err);
2249
- throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
2617
+ fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
2250
2618
  }
2251
2619
  }
2252
2620
 
2253
2621
  // src/commands/init-hooks.ts
2254
- var fs15 = __toESM(require("fs"), 1);
2255
- var path15 = __toESM(require("path"), 1);
2256
- var import_chalk8 = __toESM(require("chalk"), 1);
2622
+ var fs16 = __toESM(require("fs"), 1);
2623
+ var path16 = __toESM(require("path"), 1);
2624
+ var import_chalk9 = __toESM(require("chalk"), 1);
2257
2625
  var import_yaml = require("yaml");
2258
2626
  function setupPreCommitHook(projectRoot) {
2259
- const lefthookPath = path15.join(projectRoot, "lefthook.yml");
2260
- if (fs15.existsSync(lefthookPath)) {
2627
+ const lefthookPath = path16.join(projectRoot, "lefthook.yml");
2628
+ if (fs16.existsSync(lefthookPath)) {
2261
2629
  addLefthookPreCommit(lefthookPath);
2262
- console.log(` ${import_chalk8.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2630
+ console.log(` ${import_chalk9.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2263
2631
  return "lefthook.yml";
2264
2632
  }
2265
- const huskyDir = path15.join(projectRoot, ".husky");
2266
- if (fs15.existsSync(huskyDir)) {
2633
+ const huskyDir = path16.join(projectRoot, ".husky");
2634
+ if (fs16.existsSync(huskyDir)) {
2267
2635
  writeHuskyPreCommit(huskyDir);
2268
- console.log(` ${import_chalk8.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2636
+ console.log(` ${import_chalk9.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2269
2637
  return ".husky/pre-commit";
2270
2638
  }
2271
- const gitDir = path15.join(projectRoot, ".git");
2272
- if (fs15.existsSync(gitDir)) {
2273
- const hooksDir = path15.join(gitDir, "hooks");
2274
- if (!fs15.existsSync(hooksDir)) {
2275
- fs15.mkdirSync(hooksDir, { recursive: true });
2639
+ const gitDir = path16.join(projectRoot, ".git");
2640
+ if (fs16.existsSync(gitDir)) {
2641
+ const hooksDir = path16.join(gitDir, "hooks");
2642
+ if (!fs16.existsSync(hooksDir)) {
2643
+ fs16.mkdirSync(hooksDir, { recursive: true });
2276
2644
  }
2277
2645
  writeGitHookPreCommit(hooksDir);
2278
- console.log(` ${import_chalk8.default.green("\u2713")} .git/hooks/pre-commit`);
2646
+ console.log(` ${import_chalk9.default.green("\u2713")} .git/hooks/pre-commit`);
2279
2647
  return ".git/hooks/pre-commit";
2280
2648
  }
2281
2649
  return void 0;
2282
2650
  }
2283
2651
  function writeGitHookPreCommit(hooksDir) {
2284
- const hookPath = path15.join(hooksDir, "pre-commit");
2285
- if (fs15.existsSync(hookPath)) {
2286
- const existing = fs15.readFileSync(hookPath, "utf-8");
2652
+ const hookPath = path16.join(hooksDir, "pre-commit");
2653
+ if (fs16.existsSync(hookPath)) {
2654
+ const existing = fs16.readFileSync(hookPath, "utf-8");
2287
2655
  if (existing.includes("viberails")) return;
2288
- fs15.writeFileSync(
2656
+ fs16.writeFileSync(
2289
2657
  hookPath,
2290
2658
  `${existing.trimEnd()}
2291
2659
 
@@ -2302,10 +2670,10 @@ npx viberails check --staged
2302
2670
  "npx viberails check --staged",
2303
2671
  ""
2304
2672
  ].join("\n");
2305
- fs15.writeFileSync(hookPath, script, { mode: 493 });
2673
+ fs16.writeFileSync(hookPath, script, { mode: 493 });
2306
2674
  }
2307
2675
  function addLefthookPreCommit(lefthookPath) {
2308
- const content = fs15.readFileSync(lefthookPath, "utf-8");
2676
+ const content = fs16.readFileSync(lefthookPath, "utf-8");
2309
2677
  if (content.includes("viberails")) return;
2310
2678
  const doc = (0, import_yaml.parse)(content) ?? {};
2311
2679
  if (!doc["pre-commit"]) {
@@ -2317,29 +2685,29 @@ function addLefthookPreCommit(lefthookPath) {
2317
2685
  doc["pre-commit"].commands.viberails = {
2318
2686
  run: "npx viberails check --staged"
2319
2687
  };
2320
- fs15.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
2688
+ fs16.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
2321
2689
  }
2322
2690
  function detectHookManager(projectRoot) {
2323
- if (fs15.existsSync(path15.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2324
- if (fs15.existsSync(path15.join(projectRoot, ".husky"))) return "Husky";
2325
- if (fs15.existsSync(path15.join(projectRoot, ".git"))) return "git hook";
2691
+ if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2692
+ if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
2693
+ if (fs16.existsSync(path16.join(projectRoot, ".git"))) return "git hook";
2326
2694
  return void 0;
2327
2695
  }
2328
2696
  function setupClaudeCodeHook(projectRoot) {
2329
- const claudeDir = path15.join(projectRoot, ".claude");
2330
- if (!fs15.existsSync(claudeDir)) {
2331
- fs15.mkdirSync(claudeDir, { recursive: true });
2697
+ const claudeDir = path16.join(projectRoot, ".claude");
2698
+ if (!fs16.existsSync(claudeDir)) {
2699
+ fs16.mkdirSync(claudeDir, { recursive: true });
2332
2700
  }
2333
- const settingsPath = path15.join(claudeDir, "settings.json");
2701
+ const settingsPath = path16.join(claudeDir, "settings.json");
2334
2702
  let settings = {};
2335
- if (fs15.existsSync(settingsPath)) {
2703
+ if (fs16.existsSync(settingsPath)) {
2336
2704
  try {
2337
- settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
2705
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
2338
2706
  } catch {
2339
2707
  console.warn(
2340
- ` ${import_chalk8.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2708
+ ` ${import_chalk9.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2341
2709
  );
2342
- console.warn(` Fix the JSON manually, then re-run ${import_chalk8.default.cyan("viberails init --force")}`);
2710
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk9.default.cyan("viberails init --force")}`);
2343
2711
  return;
2344
2712
  }
2345
2713
  }
@@ -2360,30 +2728,30 @@ function setupClaudeCodeHook(projectRoot) {
2360
2728
  }
2361
2729
  ];
2362
2730
  settings.hooks = hooks;
2363
- fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2731
+ fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2364
2732
  `);
2365
- console.log(` ${import_chalk8.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2733
+ console.log(` ${import_chalk9.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2366
2734
  }
2367
2735
  function setupClaudeMdReference(projectRoot) {
2368
- const claudeMdPath = path15.join(projectRoot, "CLAUDE.md");
2736
+ const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
2369
2737
  let content = "";
2370
- if (fs15.existsSync(claudeMdPath)) {
2371
- content = fs15.readFileSync(claudeMdPath, "utf-8");
2738
+ if (fs16.existsSync(claudeMdPath)) {
2739
+ content = fs16.readFileSync(claudeMdPath, "utf-8");
2372
2740
  }
2373
2741
  if (content.includes("@.viberails/context.md")) return;
2374
2742
  const ref = "\n@.viberails/context.md\n";
2375
2743
  const prefix = content.length === 0 ? "" : content.trimEnd();
2376
- fs15.writeFileSync(claudeMdPath, prefix + ref);
2377
- console.log(` ${import_chalk8.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2744
+ fs16.writeFileSync(claudeMdPath, prefix + ref);
2745
+ console.log(` ${import_chalk9.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2378
2746
  }
2379
2747
  function setupGithubAction(projectRoot, packageManager) {
2380
- const workflowDir = path15.join(projectRoot, ".github", "workflows");
2381
- const workflowPath = path15.join(workflowDir, "viberails.yml");
2382
- if (fs15.existsSync(workflowPath)) {
2383
- const existing = fs15.readFileSync(workflowPath, "utf-8");
2748
+ const workflowDir = path16.join(projectRoot, ".github", "workflows");
2749
+ const workflowPath = path16.join(workflowDir, "viberails.yml");
2750
+ if (fs16.existsSync(workflowPath)) {
2751
+ const existing = fs16.readFileSync(workflowPath, "utf-8");
2384
2752
  if (existing.includes("viberails")) return void 0;
2385
2753
  }
2386
- fs15.mkdirSync(workflowDir, { recursive: true });
2754
+ fs16.mkdirSync(workflowDir, { recursive: true });
2387
2755
  const pm = packageManager || "npm";
2388
2756
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
2389
2757
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -2417,71 +2785,71 @@ function setupGithubAction(projectRoot, packageManager) {
2417
2785
  ""
2418
2786
  );
2419
2787
  const content = lines.filter((l) => l !== void 0).join("\n");
2420
- fs15.writeFileSync(workflowPath, content);
2788
+ fs16.writeFileSync(workflowPath, content);
2421
2789
  return ".github/workflows/viberails.yml";
2422
2790
  }
2423
2791
  function writeHuskyPreCommit(huskyDir) {
2424
- const hookPath = path15.join(huskyDir, "pre-commit");
2425
- if (fs15.existsSync(hookPath)) {
2426
- const existing = fs15.readFileSync(hookPath, "utf-8");
2792
+ const hookPath = path16.join(huskyDir, "pre-commit");
2793
+ if (fs16.existsSync(hookPath)) {
2794
+ const existing = fs16.readFileSync(hookPath, "utf-8");
2427
2795
  if (!existing.includes("viberails")) {
2428
- fs15.writeFileSync(hookPath, `${existing.trimEnd()}
2796
+ fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2429
2797
  npx viberails check --staged
2430
2798
  `);
2431
2799
  }
2432
2800
  return;
2433
2801
  }
2434
- fs15.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
2802
+ fs16.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
2435
2803
  }
2436
2804
 
2437
2805
  // src/commands/init-hooks-extra.ts
2438
- var fs16 = __toESM(require("fs"), 1);
2439
- var path16 = __toESM(require("path"), 1);
2440
- var import_chalk9 = __toESM(require("chalk"), 1);
2806
+ var fs17 = __toESM(require("fs"), 1);
2807
+ var path17 = __toESM(require("path"), 1);
2808
+ var import_chalk10 = __toESM(require("chalk"), 1);
2441
2809
  var import_yaml2 = require("yaml");
2442
2810
  function addPreCommitStep(projectRoot, name, command, marker) {
2443
- const lefthookPath = path16.join(projectRoot, "lefthook.yml");
2444
- if (fs16.existsSync(lefthookPath)) {
2445
- const content = fs16.readFileSync(lefthookPath, "utf-8");
2811
+ const lefthookPath = path17.join(projectRoot, "lefthook.yml");
2812
+ if (fs17.existsSync(lefthookPath)) {
2813
+ const content = fs17.readFileSync(lefthookPath, "utf-8");
2446
2814
  if (content.includes(marker)) return void 0;
2447
2815
  const doc = (0, import_yaml2.parse)(content) ?? {};
2448
2816
  if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
2449
2817
  if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
2450
2818
  doc["pre-commit"].commands[name] = { run: command };
2451
- fs16.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
2819
+ fs17.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
2452
2820
  return "lefthook.yml";
2453
2821
  }
2454
- const huskyDir = path16.join(projectRoot, ".husky");
2455
- if (fs16.existsSync(huskyDir)) {
2456
- const hookPath = path16.join(huskyDir, "pre-commit");
2457
- if (fs16.existsSync(hookPath)) {
2458
- const existing = fs16.readFileSync(hookPath, "utf-8");
2822
+ const huskyDir = path17.join(projectRoot, ".husky");
2823
+ if (fs17.existsSync(huskyDir)) {
2824
+ const hookPath = path17.join(huskyDir, "pre-commit");
2825
+ if (fs17.existsSync(hookPath)) {
2826
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2459
2827
  if (existing.includes(marker)) return void 0;
2460
- fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2828
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2461
2829
  ${command}
2462
2830
  `);
2463
2831
  } else {
2464
- fs16.writeFileSync(hookPath, `#!/bin/sh
2832
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2465
2833
  ${command}
2466
2834
  `, { mode: 493 });
2467
2835
  }
2468
2836
  return ".husky/pre-commit";
2469
2837
  }
2470
- const gitDir = path16.join(projectRoot, ".git");
2471
- if (fs16.existsSync(gitDir)) {
2472
- const hooksDir = path16.join(gitDir, "hooks");
2473
- if (!fs16.existsSync(hooksDir)) fs16.mkdirSync(hooksDir, { recursive: true });
2474
- const hookPath = path16.join(hooksDir, "pre-commit");
2475
- if (fs16.existsSync(hookPath)) {
2476
- const existing = fs16.readFileSync(hookPath, "utf-8");
2838
+ const gitDir = path17.join(projectRoot, ".git");
2839
+ if (fs17.existsSync(gitDir)) {
2840
+ const hooksDir = path17.join(gitDir, "hooks");
2841
+ if (!fs17.existsSync(hooksDir)) fs17.mkdirSync(hooksDir, { recursive: true });
2842
+ const hookPath = path17.join(hooksDir, "pre-commit");
2843
+ if (fs17.existsSync(hookPath)) {
2844
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2477
2845
  if (existing.includes(marker)) return void 0;
2478
- fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2846
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2479
2847
 
2480
2848
  # ${name}
2481
2849
  ${command}
2482
2850
  `);
2483
2851
  } else {
2484
- fs16.writeFileSync(hookPath, `#!/bin/sh
2852
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2485
2853
  # Generated by viberails
2486
2854
 
2487
2855
  # ${name}
@@ -2497,7 +2865,7 @@ ${command}
2497
2865
  function setupTypecheckHook(projectRoot) {
2498
2866
  const target = addPreCommitStep(projectRoot, "typecheck", "npx tsc --noEmit", "tsc");
2499
2867
  if (target) {
2500
- console.log(` ${import_chalk9.default.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
2868
+ console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
2501
2869
  }
2502
2870
  return target;
2503
2871
  }
@@ -2506,7 +2874,7 @@ function setupLintHook(projectRoot, linter) {
2506
2874
  const linterName = linter === "biome" ? "Biome" : "ESLint";
2507
2875
  const target = addPreCommitStep(projectRoot, "lint", command, linter);
2508
2876
  if (target) {
2509
- console.log(` ${import_chalk9.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2877
+ console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2510
2878
  }
2511
2879
  return target;
2512
2880
  }
@@ -2540,7 +2908,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
2540
2908
  }
2541
2909
 
2542
2910
  // src/commands/init.ts
2543
- var CONFIG_FILE4 = "viberails.config.json";
2911
+ var CONFIG_FILE5 = "viberails.config.json";
2544
2912
  function getExemptedPackages(config) {
2545
2913
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
2546
2914
  }
@@ -2551,11 +2919,11 @@ async function initCommand(options, cwd) {
2551
2919
  "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
2552
2920
  );
2553
2921
  }
2554
- const configPath = path17.join(projectRoot, CONFIG_FILE4);
2555
- if (fs17.existsSync(configPath) && !options.force) {
2922
+ const configPath = path18.join(projectRoot, CONFIG_FILE5);
2923
+ if (fs18.existsSync(configPath) && !options.force) {
2556
2924
  console.log(
2557
- `${import_chalk10.default.yellow("!")} viberails is already initialized.
2558
- Run ${import_chalk10.default.cyan("viberails sync")} to update, or ${import_chalk10.default.cyan("viberails init --force")} to start fresh.`
2925
+ `${import_chalk11.default.yellow("!")} viberails is already initialized.
2926
+ Run ${import_chalk11.default.cyan("viberails config")} to edit rules, ${import_chalk11.default.cyan("viberails sync")} to update, or ${import_chalk11.default.cyan("viberails init --force")} to start fresh.`
2559
2927
  );
2560
2928
  return;
2561
2929
  }
@@ -2563,9 +2931,9 @@ async function initCommand(options, cwd) {
2563
2931
  await initInteractive(projectRoot, configPath, options);
2564
2932
  }
2565
2933
  async function initNonInteractive(projectRoot, configPath) {
2566
- console.log(import_chalk10.default.dim("Scanning project..."));
2567
- const scanResult = await (0, import_scanner.scan)(projectRoot);
2568
- const config = (0, import_config6.generateConfig)(scanResult);
2934
+ console.log(import_chalk11.default.dim("Scanning project..."));
2935
+ const scanResult = await (0, import_scanner2.scan)(projectRoot);
2936
+ const config = (0, import_config8.generateConfig)(scanResult);
2569
2937
  for (const pkg of config.packages) {
2570
2938
  const pkgMeta = config._meta?.packages?.[pkg.path]?.conventions;
2571
2939
  pkg.conventions = filterHighConfidence(pkg.conventions ?? {}, pkgMeta);
@@ -2576,11 +2944,11 @@ async function initNonInteractive(projectRoot, configPath) {
2576
2944
  const exempted = getExemptedPackages(config);
2577
2945
  if (exempted.length > 0) {
2578
2946
  console.log(
2579
- ` ${import_chalk10.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk10.default.dim("(types-only)")}`
2947
+ ` ${import_chalk11.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk11.default.dim("(types-only)")}`
2580
2948
  );
2581
2949
  }
2582
2950
  if (config.packages.length > 1) {
2583
- console.log(import_chalk10.default.dim("Building import graph..."));
2951
+ console.log(import_chalk11.default.dim("Building import graph..."));
2584
2952
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2585
2953
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
2586
2954
  const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
@@ -2592,51 +2960,51 @@ async function initNonInteractive(projectRoot, configPath) {
2592
2960
  console.log(` Inferred ${denyCount} boundary rules`);
2593
2961
  }
2594
2962
  }
2595
- const compacted = (0, import_config6.compactConfig)(config);
2596
- fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2963
+ const compacted = (0, import_config8.compactConfig)(config);
2964
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2597
2965
  `);
2598
2966
  writeGeneratedFiles(projectRoot, config, scanResult);
2599
2967
  updateGitignore(projectRoot);
2600
2968
  setupClaudeCodeHook(projectRoot);
2601
2969
  setupClaudeMdReference(projectRoot);
2602
- const preCommitTarget = setupPreCommitHook(projectRoot);
2603
2970
  const rootPkg = config.packages[0];
2604
2971
  const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
2605
2972
  const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
2606
- const typecheckTarget = rootPkg?.stack?.language === "typescript" ? setupTypecheckHook(projectRoot) : void 0;
2973
+ const hookManager = detectHookManager(projectRoot);
2974
+ const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
2975
+ const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
2607
2976
  const linter = rootPkg?.stack?.linter?.split("@")[0];
2608
- const lintTarget = linter ? setupLintHook(projectRoot, linter) : void 0;
2609
- const ok = import_chalk10.default.green("\u2713");
2977
+ const ok = import_chalk11.default.green("\u2713");
2610
2978
  const created = [
2611
- `${ok} ${path17.basename(configPath)}`,
2979
+ `${ok} ${path18.basename(configPath)}`,
2612
2980
  `${ok} .viberails/context.md`,
2613
2981
  `${ok} .viberails/scan-result.json`,
2614
2982
  `${ok} .claude/settings.json \u2014 added viberails hook`,
2615
2983
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
2616
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk10.default.yellow("!")} pre-commit hook skipped`,
2617
- typecheckTarget ? `${ok} ${typecheckTarget} \u2014 added typecheck` : "",
2618
- lintTarget ? `${ok} ${lintTarget} \u2014 added lint check` : "",
2984
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk11.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
2619
2985
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
2620
2986
  ].filter(Boolean);
2987
+ if (hasHookManager && rootPkg?.stack?.language === "typescript") setupTypecheckHook(projectRoot);
2988
+ if (hasHookManager && linter) setupLintHook(projectRoot, linter);
2621
2989
  console.log(`
2622
2990
  Created:
2623
2991
  ${created.map((f) => ` ${f}`).join("\n")}`);
2624
2992
  }
2625
2993
  async function initInteractive(projectRoot, configPath, options) {
2626
- clack7.intro("viberails");
2627
- if (fs17.existsSync(configPath) && options.force) {
2994
+ clack8.intro("viberails");
2995
+ if (fs18.existsSync(configPath) && options.force) {
2628
2996
  const replace = await confirmDangerous(
2629
- `${path17.basename(configPath)} already exists and will be replaced. Continue?`
2997
+ `${path18.basename(configPath)} already exists and will be replaced. Continue?`
2630
2998
  );
2631
2999
  if (!replace) {
2632
- clack7.outro("Aborted. No files were written.");
3000
+ clack8.outro("Aborted. No files were written.");
2633
3001
  return;
2634
3002
  }
2635
3003
  }
2636
- const s = clack7.spinner();
3004
+ const s = clack8.spinner();
2637
3005
  s.start("Scanning project...");
2638
- const scanResult = await (0, import_scanner.scan)(projectRoot);
2639
- const config = (0, import_config6.generateConfig)(scanResult);
3006
+ const scanResult = await (0, import_scanner2.scan)(projectRoot);
3007
+ const config = (0, import_config8.generateConfig)(scanResult);
2640
3008
  s.stop("Scan complete");
2641
3009
  const prereqResult = await promptMissingPrereqs(
2642
3010
  projectRoot,
@@ -2646,16 +3014,13 @@ async function initInteractive(projectRoot, configPath, options) {
2646
3014
  config.rules.testCoverage = 0;
2647
3015
  }
2648
3016
  if (scanResult.statistics.totalFiles === 0) {
2649
- clack7.log.warn(
3017
+ clack8.log.warn(
2650
3018
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
2651
3019
  );
2652
3020
  }
2653
- clack7.note(formatScanResultsText(scanResult), "Scan results");
2654
- const rulesLines = formatRulesText(config);
3021
+ clack8.note(formatScanResultsText(scanResult), "Scan results");
2655
3022
  const exemptedPkgs = getExemptedPackages(config);
2656
- if (exemptedPkgs.length > 0)
2657
- rulesLines.push(`Auto-exempted from coverage: ${exemptedPkgs.join(", ")} (types-only)`);
2658
- clack7.note(rulesLines.join("\n"), "Rules");
3023
+ displayInitSummary(config, exemptedPkgs);
2659
3024
  const decision = await promptInitDecision();
2660
3025
  if (decision === "customize") {
2661
3026
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
@@ -2669,41 +3034,16 @@ async function initInteractive(projectRoot, configPath, options) {
2669
3034
  coverageCommand: config.defaults?.coverage?.command,
2670
3035
  packageOverrides: config.packages
2671
3036
  });
2672
- if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
2673
- config.rules.maxFileLines = overrides.maxFileLines;
2674
- config.rules.testCoverage = overrides.testCoverage;
2675
- config.rules.enforceMissingTests = overrides.enforceMissingTests;
2676
- config.rules.enforceNaming = overrides.enforceNaming;
2677
- for (const pkg of config.packages) {
2678
- pkg.coverage = pkg.coverage ?? {};
2679
- if (pkg.coverage.summaryPath === void 0) {
2680
- pkg.coverage.summaryPath = overrides.coverageSummaryPath;
2681
- }
2682
- if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
2683
- pkg.coverage.command = overrides.coverageCommand;
2684
- }
2685
- }
2686
- if (overrides.fileNamingValue) {
2687
- const oldNaming = rootPkg.conventions?.fileNaming;
2688
- rootPkg.conventions = rootPkg.conventions ?? {};
2689
- rootPkg.conventions.fileNaming = overrides.fileNamingValue;
2690
- if (oldNaming && oldNaming !== overrides.fileNamingValue) {
2691
- for (const pkg of config.packages) {
2692
- if (pkg.conventions?.fileNaming === oldNaming) {
2693
- pkg.conventions.fileNaming = overrides.fileNamingValue;
2694
- }
2695
- }
2696
- }
2697
- }
3037
+ applyRuleOverrides(config, overrides);
2698
3038
  }
2699
3039
  if (config.packages.length > 1) {
2700
- clack7.note(
3040
+ clack8.note(
2701
3041
  "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
2702
3042
  "Boundaries"
2703
3043
  );
2704
3044
  const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
2705
3045
  if (shouldInfer) {
2706
- const bs = clack7.spinner();
3046
+ const bs = clack8.spinner();
2707
3047
  bs.start("Building import graph...");
2708
3048
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2709
3049
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -2715,7 +3055,7 @@ async function initInteractive(projectRoot, configPath, options) {
2715
3055
  config.rules.enforceBoundaries = true;
2716
3056
  bs.stop(`Inferred ${denyCount} boundary rules`);
2717
3057
  const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
2718
- clack7.note(boundaryLines, "Boundary rules");
3058
+ clack8.note(boundaryLines, "Boundary rules");
2719
3059
  } else {
2720
3060
  bs.stop("No boundary rules inferred");
2721
3061
  }
@@ -2723,22 +3063,23 @@ async function initInteractive(projectRoot, configPath, options) {
2723
3063
  }
2724
3064
  const hookManager = detectHookManager(projectRoot);
2725
3065
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
2726
- const integrations = await promptIntegrations(hookManager, {
3066
+ const integrations = await promptIntegrations(projectRoot, hookManager, {
2727
3067
  isTypeScript: rootPkgStack?.language === "typescript",
2728
- linter: rootPkgStack?.linter?.split("@")[0]
3068
+ linter: rootPkgStack?.linter?.split("@")[0],
3069
+ packageManager: rootPkgStack?.packageManager
2729
3070
  });
2730
3071
  const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
2731
3072
  if (!shouldWrite) {
2732
- clack7.outro("Aborted. No files were written.");
3073
+ clack8.outro("Aborted. No files were written.");
2733
3074
  return;
2734
3075
  }
2735
- const compacted = (0, import_config6.compactConfig)(config);
2736
- fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3076
+ const compacted = (0, import_config8.compactConfig)(config);
3077
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2737
3078
  `);
2738
3079
  writeGeneratedFiles(projectRoot, config, scanResult);
2739
3080
  updateGitignore(projectRoot);
2740
3081
  const createdFiles = [
2741
- path17.basename(configPath),
3082
+ path18.basename(configPath),
2742
3083
  ".viberails/context.md",
2743
3084
  ".viberails/scan-result.json",
2744
3085
  ...setupSelectedIntegrations(projectRoot, integrations, {
@@ -2746,155 +3087,27 @@ async function initInteractive(projectRoot, configPath, options) {
2746
3087
  packageManager: rootPkgStack?.packageManager
2747
3088
  })
2748
3089
  ];
2749
- clack7.log.success(`Created:
3090
+ clack8.log.success(`Created:
2750
3091
  ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
2751
- clack7.outro(
3092
+ clack8.outro(
2752
3093
  `Done! Next: review viberails.config.json, then run viberails check
2753
- ${import_chalk10.default.dim("Tip: use")} ${import_chalk10.default.cyan("viberails check --enforce")} ${import_chalk10.default.dim("in CI to block PRs on violations.")}`
3094
+ ${import_chalk11.default.dim("Tip: use")} ${import_chalk11.default.cyan("viberails check --enforce")} ${import_chalk11.default.dim("in CI to block PRs on violations.")}`
2754
3095
  );
2755
3096
  }
2756
3097
 
2757
3098
  // src/commands/sync.ts
2758
- var fs18 = __toESM(require("fs"), 1);
2759
- var path18 = __toESM(require("path"), 1);
2760
- var import_config7 = require("@viberails/config");
2761
- var import_scanner2 = require("@viberails/scanner");
2762
- var import_chalk11 = __toESM(require("chalk"), 1);
2763
-
2764
- // src/utils/diff-configs.ts
2765
- var import_types5 = require("@viberails/types");
2766
- function parseStackString(s) {
2767
- const atIdx = s.indexOf("@");
2768
- if (atIdx > 0) {
2769
- return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
2770
- }
2771
- return { name: s };
2772
- }
2773
- function displayStackName(s) {
2774
- const { name, version } = parseStackString(s);
2775
- const allMaps = {
2776
- ...import_types5.FRAMEWORK_NAMES,
2777
- ...import_types5.STYLING_NAMES,
2778
- ...import_types5.ORM_NAMES
2779
- };
2780
- const display = allMaps[name] ?? name;
2781
- return version ? `${display} ${version}` : display;
2782
- }
2783
- function isNewlyDetected(config, pkgPath, key) {
2784
- return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
2785
- }
2786
- var STACK_FIELDS = [
2787
- "framework",
2788
- "styling",
2789
- "backend",
2790
- "orm",
2791
- "linter",
2792
- "formatter",
2793
- "testRunner"
2794
- ];
2795
- var CONVENTION_KEYS = [
2796
- "fileNaming",
2797
- "componentNaming",
2798
- "hookNaming",
2799
- "importAlias"
2800
- ];
2801
- var STRUCTURE_FIELDS = [
2802
- { key: "srcDir", label: "source directory" },
2803
- { key: "pages", label: "pages directory" },
2804
- { key: "components", label: "components directory" },
2805
- { key: "hooks", label: "hooks directory" },
2806
- { key: "utils", label: "utilities directory" },
2807
- { key: "types", label: "types directory" },
2808
- { key: "tests", label: "tests directory" },
2809
- { key: "testPattern", label: "test pattern" }
2810
- ];
2811
- function diffPackage(existing, merged, mergedConfig) {
2812
- const changes = [];
2813
- const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
2814
- for (const field of STACK_FIELDS) {
2815
- const oldVal = existing.stack?.[field];
2816
- const newVal = merged.stack?.[field];
2817
- if (!oldVal && newVal) {
2818
- changes.push({
2819
- type: "added",
2820
- description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
2821
- });
2822
- } else if (oldVal && newVal && oldVal !== newVal) {
2823
- changes.push({
2824
- type: "changed",
2825
- description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
2826
- });
2827
- }
2828
- }
2829
- for (const key of CONVENTION_KEYS) {
2830
- const oldVal = existing.conventions?.[key];
2831
- const newVal = merged.conventions?.[key];
2832
- const label = import_types5.CONVENTION_LABELS[key] ?? key;
2833
- if (!oldVal && newVal) {
2834
- changes.push({
2835
- type: "added",
2836
- description: `${pkgPrefix}New convention: ${label} (${newVal})`
2837
- });
2838
- } else if (oldVal && newVal && oldVal !== newVal) {
2839
- const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
2840
- changes.push({
2841
- type: "changed",
2842
- description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
2843
- });
2844
- }
2845
- }
2846
- for (const { key, label } of STRUCTURE_FIELDS) {
2847
- const oldVal = existing.structure?.[key];
2848
- const newVal = merged.structure?.[key];
2849
- if (!oldVal && newVal) {
2850
- changes.push({
2851
- type: "added",
2852
- description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
2853
- });
2854
- }
2855
- }
2856
- return changes;
2857
- }
2858
- function diffConfigs(existing, merged) {
2859
- const changes = [];
2860
- const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
2861
- const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
2862
- for (const existingPkg of existing.packages) {
2863
- const mergedPkg = mergedByPath.get(existingPkg.path);
2864
- if (mergedPkg) {
2865
- changes.push(...diffPackage(existingPkg, mergedPkg, merged));
2866
- }
2867
- }
2868
- for (const mergedPkg of merged.packages) {
2869
- if (!existingByPath.has(mergedPkg.path)) {
2870
- changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
2871
- }
2872
- }
2873
- return changes;
2874
- }
2875
- function formatStatsDelta(oldStats, newStats) {
2876
- const fileDelta = newStats.totalFiles - oldStats.totalFiles;
2877
- const lineDelta = newStats.totalLines - oldStats.totalLines;
2878
- if (fileDelta === 0 && lineDelta === 0) return void 0;
2879
- const parts = [];
2880
- if (fileDelta !== 0) {
2881
- const sign = fileDelta > 0 ? "+" : "";
2882
- parts.push(`${sign}${fileDelta.toLocaleString()} files`);
2883
- }
2884
- if (lineDelta !== 0) {
2885
- const sign = lineDelta > 0 ? "+" : "";
2886
- parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
2887
- }
2888
- return `${parts.join(", ")} since last sync`;
2889
- }
2890
-
2891
- // src/commands/sync.ts
2892
- var CONFIG_FILE5 = "viberails.config.json";
3099
+ var fs19 = __toESM(require("fs"), 1);
3100
+ var path19 = __toESM(require("path"), 1);
3101
+ var clack9 = __toESM(require("@clack/prompts"), 1);
3102
+ var import_config9 = require("@viberails/config");
3103
+ var import_scanner3 = require("@viberails/scanner");
3104
+ var import_chalk12 = __toESM(require("chalk"), 1);
3105
+ var CONFIG_FILE6 = "viberails.config.json";
2893
3106
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
2894
3107
  function loadPreviousStats(projectRoot) {
2895
- const scanResultPath = path18.join(projectRoot, SCAN_RESULT_FILE2);
3108
+ const scanResultPath = path19.join(projectRoot, SCAN_RESULT_FILE2);
2896
3109
  try {
2897
- const raw = fs18.readFileSync(scanResultPath, "utf-8");
3110
+ const raw = fs19.readFileSync(scanResultPath, "utf-8");
2898
3111
  const parsed = JSON.parse(raw);
2899
3112
  if (parsed?.statistics?.totalFiles !== void 0) {
2900
3113
  return parsed.statistics;
@@ -2903,7 +3116,7 @@ function loadPreviousStats(projectRoot) {
2903
3116
  }
2904
3117
  return void 0;
2905
3118
  }
2906
- async function syncCommand(cwd) {
3119
+ async function syncCommand(options, cwd) {
2907
3120
  const startDir = cwd ?? process.cwd();
2908
3121
  const projectRoot = findProjectRoot(startDir);
2909
3122
  if (!projectRoot) {
@@ -2911,15 +3124,15 @@ async function syncCommand(cwd) {
2911
3124
  "No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
2912
3125
  );
2913
3126
  }
2914
- const configPath = path18.join(projectRoot, CONFIG_FILE5);
2915
- const existing = await (0, import_config7.loadConfig)(configPath);
3127
+ const configPath = path19.join(projectRoot, CONFIG_FILE6);
3128
+ const existing = await (0, import_config9.loadConfig)(configPath);
2916
3129
  const previousStats = loadPreviousStats(projectRoot);
2917
- console.log(import_chalk11.default.dim("Scanning project..."));
2918
- const scanResult = await (0, import_scanner2.scan)(projectRoot);
2919
- const merged = (0, import_config7.mergeConfig)(existing, scanResult);
2920
- const compacted = (0, import_config7.compactConfig)(merged);
3130
+ console.log(import_chalk12.default.dim("Scanning project..."));
3131
+ const scanResult = await (0, import_scanner3.scan)(projectRoot);
3132
+ const merged = (0, import_config9.mergeConfig)(existing, scanResult);
3133
+ const compacted = (0, import_config9.compactConfig)(merged);
2921
3134
  const compactedJson = JSON.stringify(compacted, null, 2);
2922
- const rawDisk = fs18.readFileSync(configPath, "utf-8").trim();
3135
+ const rawDisk = fs19.readFileSync(configPath, "utf-8").trim();
2923
3136
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
2924
3137
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
2925
3138
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -2927,31 +3140,69 @@ async function syncCommand(cwd) {
2927
3140
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
2928
3141
  if (changes.length > 0 || statsDelta) {
2929
3142
  console.log(`
2930
- ${import_chalk11.default.bold("Changes:")}`);
3143
+ ${import_chalk12.default.bold("Changes:")}`);
2931
3144
  for (const change of changes) {
2932
- const icon = change.type === "removed" ? import_chalk11.default.red("-") : import_chalk11.default.green("+");
3145
+ const icon = change.type === "removed" ? import_chalk12.default.red("-") : import_chalk12.default.green("+");
2933
3146
  console.log(` ${icon} ${change.description}`);
2934
3147
  }
2935
3148
  if (statsDelta) {
2936
- console.log(` ${import_chalk11.default.dim(statsDelta)}`);
3149
+ console.log(` ${import_chalk12.default.dim(statsDelta)}`);
2937
3150
  }
2938
3151
  }
2939
- fs18.writeFileSync(configPath, `${compactedJson}
3152
+ if (options?.interactive) {
3153
+ clack9.intro("viberails sync (interactive)");
3154
+ clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
3155
+ const decision = await clack9.select({
3156
+ message: "How would you like to proceed?",
3157
+ options: [
3158
+ { value: "accept", label: "Accept changes" },
3159
+ { value: "customize", label: "Customize rules" },
3160
+ { value: "cancel", label: "Cancel (no changes written)" }
3161
+ ]
3162
+ });
3163
+ assertNotCancelled(decision);
3164
+ if (decision === "cancel") {
3165
+ clack9.outro("Sync cancelled. No files were written.");
3166
+ return;
3167
+ }
3168
+ if (decision === "customize") {
3169
+ const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
3170
+ const overrides = await promptRuleMenu({
3171
+ maxFileLines: merged.rules.maxFileLines,
3172
+ testCoverage: merged.rules.testCoverage,
3173
+ enforceMissingTests: merged.rules.enforceMissingTests,
3174
+ enforceNaming: merged.rules.enforceNaming,
3175
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3176
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3177
+ coverageCommand: merged.defaults?.coverage?.command,
3178
+ packageOverrides: merged.packages
3179
+ });
3180
+ applyRuleOverrides(merged, overrides);
3181
+ const recompacted = (0, import_config9.compactConfig)(merged);
3182
+ fs19.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3183
+ `);
3184
+ writeGeneratedFiles(projectRoot, merged, scanResult);
3185
+ clack9.log.success("Updated config with your customizations.");
3186
+ clack9.outro("Done! Run viberails check to verify.");
3187
+ return;
3188
+ }
3189
+ }
3190
+ fs19.writeFileSync(configPath, `${compactedJson}
2940
3191
  `);
2941
3192
  writeGeneratedFiles(projectRoot, merged, scanResult);
2942
3193
  console.log(`
2943
- ${import_chalk11.default.bold("Synced:")}`);
3194
+ ${import_chalk12.default.bold("Synced:")}`);
2944
3195
  if (configChanged) {
2945
- console.log(` ${import_chalk11.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
3196
+ console.log(` ${import_chalk12.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
2946
3197
  } else {
2947
- console.log(` ${import_chalk11.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
3198
+ console.log(` ${import_chalk12.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
2948
3199
  }
2949
- console.log(` ${import_chalk11.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
2950
- console.log(` ${import_chalk11.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3200
+ console.log(` ${import_chalk12.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3201
+ console.log(` ${import_chalk12.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
2951
3202
  }
2952
3203
 
2953
3204
  // src/index.ts
2954
- var VERSION = "0.5.0";
3205
+ var VERSION = "0.5.2";
2955
3206
  var program = new import_commander.Command();
2956
3207
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
2957
3208
  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) => {
@@ -2959,16 +3210,25 @@ program.command("init", { isDefault: true }).description("Scan your project and
2959
3210
  await initCommand(options);
2960
3211
  } catch (err) {
2961
3212
  const message = err instanceof Error ? err.message : String(err);
2962
- console.error(`${import_chalk12.default.red("Error:")} ${message}`);
3213
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
3214
+ process.exit(1);
3215
+ }
3216
+ });
3217
+ program.command("sync").description("Re-scan and update generated files").option("-i, --interactive", "Review changes before writing").action(async (options) => {
3218
+ try {
3219
+ await syncCommand(options);
3220
+ } catch (err) {
3221
+ const message = err instanceof Error ? err.message : String(err);
3222
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2963
3223
  process.exit(1);
2964
3224
  }
2965
3225
  });
2966
- program.command("sync").description("Re-scan and update generated files").action(async () => {
3226
+ program.command("config").description("Interactively edit existing config rules").option("--rescan", "Re-scan project first (picks up new packages, stack changes)").action(async (options) => {
2967
3227
  try {
2968
- await syncCommand();
3228
+ await configCommand(options);
2969
3229
  } catch (err) {
2970
3230
  const message = err instanceof Error ? err.message : String(err);
2971
- console.error(`${import_chalk12.default.red("Error:")} ${message}`);
3231
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2972
3232
  process.exit(1);
2973
3233
  }
2974
3234
  });
@@ -2989,7 +3249,7 @@ program.command("check").description("Check files against enforced rules").optio
2989
3249
  process.exit(exitCode);
2990
3250
  } catch (err) {
2991
3251
  const message = err instanceof Error ? err.message : String(err);
2992
- console.error(`${import_chalk12.default.red("Error:")} ${message}`);
3252
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2993
3253
  process.exit(1);
2994
3254
  }
2995
3255
  }
@@ -3000,7 +3260,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
3000
3260
  process.exit(exitCode);
3001
3261
  } catch (err) {
3002
3262
  const message = err instanceof Error ? err.message : String(err);
3003
- console.error(`${import_chalk12.default.red("Error:")} ${message}`);
3263
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
3004
3264
  process.exit(1);
3005
3265
  }
3006
3266
  });
@@ -3009,7 +3269,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
3009
3269
  await boundariesCommand(options);
3010
3270
  } catch (err) {
3011
3271
  const message = err instanceof Error ? err.message : String(err);
3012
- console.error(`${import_chalk12.default.red("Error:")} ${message}`);
3272
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
3013
3273
  process.exit(1);
3014
3274
  }
3015
3275
  });