viberails 0.5.0 → 0.5.1

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