viberails 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import chalk12 from "chalk";
4
+ import chalk13 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/boundaries.ts
@@ -31,14 +31,66 @@ function findProjectRoot(startDir) {
31
31
  import * as clack5 from "@clack/prompts";
32
32
 
33
33
  // src/utils/prompt-integrations.ts
34
+ import { spawnSync } from "child_process";
34
35
  import * as clack from "@clack/prompts";
35
- async function promptIntegrations(hookManager, tools) {
36
- const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
36
+ async function promptHookManagerInstall(projectRoot, packageManager) {
37
+ const choice = await clack.select({
38
+ message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
39
+ options: [
40
+ {
41
+ value: "install",
42
+ label: "Yes, install Lefthook",
43
+ hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
44
+ },
45
+ {
46
+ value: "skip",
47
+ label: "No, skip",
48
+ hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
49
+ }
50
+ ]
51
+ });
52
+ assertNotCancelled(choice);
53
+ if (choice !== "install") return void 0;
54
+ const pm = packageManager || "npm";
55
+ const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? "pnpm add -D lefthook" : "npm install -D lefthook";
56
+ const s = clack.spinner();
57
+ s.start("Installing Lefthook...");
58
+ const result = spawnSync(installCmd, {
59
+ cwd: projectRoot,
60
+ shell: true,
61
+ encoding: "utf-8",
62
+ stdio: "pipe"
63
+ });
64
+ if (result.status === 0) {
65
+ const fs20 = await import("fs");
66
+ const path20 = await import("path");
67
+ const lefthookPath = path20.join(projectRoot, "lefthook.yml");
68
+ if (!fs20.existsSync(lefthookPath)) {
69
+ fs20.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
70
+ }
71
+ s.stop("Installed Lefthook");
72
+ return "Lefthook";
73
+ }
74
+ s.stop("Failed to install Lefthook");
75
+ clack.log.warn(`Install manually: ${installCmd}`);
76
+ return void 0;
77
+ }
78
+ async function promptIntegrations(projectRoot, hookManager, tools) {
79
+ let resolvedHookManager = hookManager;
80
+ if (!resolvedHookManager) {
81
+ resolvedHookManager = await promptHookManagerInstall(
82
+ projectRoot,
83
+ tools?.packageManager ?? "npm"
84
+ );
85
+ }
86
+ const isBareHook = !resolvedHookManager;
87
+ const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
88
+ const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
37
89
  const options = [
38
90
  {
39
91
  value: "preCommit",
40
92
  label: hookLabel,
41
- hint: "runs viberails checks when you commit"
93
+ hint: hookHint
42
94
  }
43
95
  ];
44
96
  if (tools?.isTypeScript) {
@@ -73,7 +125,7 @@ async function promptIntegrations(hookManager, tools) {
73
125
  hint: "blocks PRs that fail viberails check"
74
126
  }
75
127
  );
76
- const initialValues = options.map((o) => o.value);
128
+ const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
77
129
  const result = await clack.multiselect({
78
130
  message: "Set up integrations?",
79
131
  options,
@@ -276,11 +328,10 @@ function buildMenuOptions(state, packageCount) {
276
328
  hint: state.fileNamingValue
277
329
  });
278
330
  }
279
- options.push({
280
- value: "testCoverage",
281
- label: "Test coverage target",
282
- hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
283
- });
331
+ const isMonorepo = packageCount > 0;
332
+ const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
333
+ const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
334
+ options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
284
335
  options.push({
285
336
  value: "enforceMissingTests",
286
337
  label: "Enforce missing tests",
@@ -290,16 +341,16 @@ function buildMenuOptions(state, packageCount) {
290
341
  options.push(
291
342
  {
292
343
  value: "coverageSummaryPath",
293
- label: "Coverage summary path",
344
+ label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
294
345
  hint: state.coverageSummaryPath
295
346
  },
296
347
  {
297
348
  value: "coverageCommand",
298
- label: "Coverage command",
349
+ label: isMonorepo ? "Default coverage command" : "Coverage command",
299
350
  hint: state.coverageCommand ?? "auto-detect from package.json test runner"
300
351
  }
301
352
  );
302
- if (packageCount > 0) {
353
+ if (isMonorepo) {
303
354
  options.push({
304
355
  value: "packageOverrides",
305
356
  label: "Per-package coverage overrides",
@@ -690,9 +741,10 @@ function resolveIgnoreForFile(relPath, config) {
690
741
  }
691
742
 
692
743
  // src/commands/check-coverage.ts
693
- import { spawnSync } from "child_process";
744
+ import { spawnSync as spawnSync2 } from "child_process";
694
745
  import * as fs4 from "fs";
695
746
  import * as path4 from "path";
747
+ import { inferCoverageCommand } from "@viberails/config";
696
748
  var DEFAULT_SUMMARY_PATH = "coverage/coverage-summary.json";
697
749
  function packageRoot(projectRoot, pkg) {
698
750
  return pkg.path === "." ? projectRoot : path4.join(projectRoot, pkg.path);
@@ -732,7 +784,7 @@ function readCoveragePercentage(summaryPath) {
732
784
  }
733
785
  }
734
786
  function runCoverageCommand(pkgRoot, command) {
735
- const result = spawnSync(command, {
787
+ const result = spawnSync2(command, {
736
788
  cwd: pkgRoot,
737
789
  shell: true,
738
790
  encoding: "utf-8",
@@ -773,13 +825,14 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
773
825
  const violations = [];
774
826
  for (const target of targets) {
775
827
  if (target.rules.testCoverage <= 0) continue;
828
+ if (!target.pkg.stack?.testRunner) continue;
776
829
  const pkgRoot = packageRoot(projectRoot, target.pkg);
777
830
  const summaryPath = target.coverage.summaryPath ?? DEFAULT_SUMMARY_PATH;
778
831
  const summaryAbs = path4.join(pkgRoot, summaryPath);
779
832
  const summaryRel = violationFilePath(projectRoot, pkgRoot, summaryPath);
780
833
  let pct = readCoveragePercentage(summaryAbs);
781
834
  if (pct === void 0 && !options.staged) {
782
- const command = target.coverage.command;
835
+ const command = target.coverage.command ?? inferCoverageCommand(target.pkg.stack.testRunner);
783
836
  if (!command) {
784
837
  const pkgLabel = target.pkg.path === "." ? "root package" : target.pkg.path;
785
838
  pushViolation(
@@ -790,6 +843,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
790
843
  );
791
844
  continue;
792
845
  }
846
+ options.onProgress?.(target.pkg.path === "." ? "root" : target.pkg.path);
793
847
  const run = runCoverageCommand(pkgRoot, command);
794
848
  if (!run.ok) {
795
849
  pushViolation(
@@ -1132,6 +1186,9 @@ async function checkCommand(options, cwd) {
1132
1186
  }
1133
1187
  const violations = [];
1134
1188
  const severity = options.enforce ? "error" : "warn";
1189
+ const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(chalk2.dim(msg)) : () => {
1190
+ };
1191
+ log7(" Checking files...");
1135
1192
  for (const file of filesToCheck) {
1136
1193
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1137
1194
  const relPath = path7.relative(projectRoot, absPath);
@@ -1164,18 +1221,22 @@ async function checkCommand(options, cwd) {
1164
1221
  }
1165
1222
  }
1166
1223
  }
1224
+ log7(" done\n");
1167
1225
  if (!options.staged && !options.files) {
1226
+ log7(" Checking missing tests...");
1168
1227
  const testViolations = checkMissingTests(projectRoot, config, severity);
1169
- if (diffAddedFiles) {
1170
- violations.push(...testViolations.filter((v) => diffAddedFiles.has(v.file)));
1171
- } else {
1172
- violations.push(...testViolations);
1173
- }
1228
+ violations.push(
1229
+ ...diffAddedFiles ? testViolations.filter((v) => diffAddedFiles.has(v.file)) : testViolations
1230
+ );
1231
+ log7(" done\n");
1174
1232
  }
1175
1233
  if (!options.files && !options.staged && !options.diffBase) {
1234
+ log7(" Running test coverage...\n");
1176
1235
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1177
1236
  staged: options.staged,
1178
- enforce: options.enforce
1237
+ enforce: options.enforce,
1238
+ onProgress: (pkg) => log7(` Coverage: ${pkg}...
1239
+ `)
1179
1240
  });
1180
1241
  violations.push(...coverageViolations);
1181
1242
  }
@@ -1199,10 +1260,8 @@ async function checkCommand(options, cwd) {
1199
1260
  severity
1200
1261
  });
1201
1262
  }
1202
- const elapsed = Date.now() - startTime;
1203
- if (options.format !== "json") {
1204
- console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
1205
- }
1263
+ log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1264
+ `);
1206
1265
  }
1207
1266
  if (options.format === "json") {
1208
1267
  console.log(
@@ -1274,443 +1333,379 @@ async function hookCheckCommand(cwd) {
1274
1333
  }
1275
1334
  }
1276
1335
 
1277
- // src/commands/fix.ts
1278
- import * as fs11 from "fs";
1279
- import * as path11 from "path";
1280
- import { loadConfig as loadConfig3 } from "@viberails/config";
1336
+ // src/commands/config.ts
1337
+ import * as fs10 from "fs";
1338
+ import * as path9 from "path";
1339
+ import * as clack6 from "@clack/prompts";
1340
+ import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
1341
+ import { scan } from "@viberails/scanner";
1342
+ import chalk5 from "chalk";
1343
+
1344
+ // src/display-text.ts
1345
+ import {
1346
+ CONVENTION_LABELS as CONVENTION_LABELS2,
1347
+ FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
1348
+ LIBRARY_NAMES as LIBRARY_NAMES2,
1349
+ ORM_NAMES as ORM_NAMES2,
1350
+ STYLING_NAMES as STYLING_NAMES3
1351
+ } from "@viberails/types";
1352
+
1353
+ // src/display.ts
1354
+ import {
1355
+ CONVENTION_LABELS,
1356
+ FRAMEWORK_NAMES as FRAMEWORK_NAMES2,
1357
+ LIBRARY_NAMES,
1358
+ ORM_NAMES,
1359
+ STYLING_NAMES as STYLING_NAMES2
1360
+ } from "@viberails/types";
1281
1361
  import chalk4 from "chalk";
1282
1362
 
1283
- // src/commands/fix-helpers.ts
1284
- import { execSync as execSync2 } from "child_process";
1285
- import chalk3 from "chalk";
1286
- function printPlan(renames, stubs) {
1287
- if (renames.length > 0) {
1288
- console.log(chalk3.bold("\nFile renames:"));
1289
- for (const r of renames) {
1290
- console.log(` ${chalk3.red(r.oldPath)} \u2192 ${chalk3.green(r.newPath)}`);
1363
+ // src/display-helpers.ts
1364
+ import { ROLE_DESCRIPTIONS } from "@viberails/types";
1365
+ function groupByRole(directories) {
1366
+ const map = /* @__PURE__ */ new Map();
1367
+ for (const dir of directories) {
1368
+ if (dir.role === "unknown") continue;
1369
+ const existing = map.get(dir.role);
1370
+ if (existing) {
1371
+ existing.dirs.push(dir);
1372
+ } else {
1373
+ map.set(dir.role, { dirs: [dir] });
1291
1374
  }
1292
1375
  }
1293
- if (stubs.length > 0) {
1294
- console.log(chalk3.bold("\nTest stubs to create:"));
1295
- for (const s of stubs) {
1296
- console.log(` ${chalk3.green("+")} ${s.path}`);
1297
- }
1376
+ const groups = [];
1377
+ for (const [role, { dirs }] of map) {
1378
+ const label = ROLE_DESCRIPTIONS[role] ?? role;
1379
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1380
+ groups.push({
1381
+ role,
1382
+ label,
1383
+ dirCount: dirs.length,
1384
+ totalFiles,
1385
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1386
+ });
1298
1387
  }
1388
+ return groups;
1299
1389
  }
1300
- function checkGitDirty(projectRoot) {
1301
- try {
1302
- const output = execSync2("git status --porcelain", {
1303
- cwd: projectRoot,
1304
- encoding: "utf-8",
1305
- stdio: ["ignore", "pipe", "ignore"]
1306
- });
1307
- return output.trim().length > 0;
1308
- } catch {
1309
- return false;
1390
+ function formatSummary(stats, packageCount) {
1391
+ const parts = [];
1392
+ if (packageCount && packageCount > 1) {
1393
+ parts.push(`${packageCount} packages`);
1310
1394
  }
1395
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1396
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1397
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1398
+ return parts.join(" \xB7 ");
1311
1399
  }
1312
- function getConventionValue(convention) {
1313
- if (typeof convention === "string") return convention;
1314
- return void 0;
1400
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1401
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1315
1402
  }
1316
-
1317
- // src/commands/fix-imports.ts
1318
- import * as path8 from "path";
1319
- function stripExtension(filePath) {
1320
- return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1403
+ function formatRoleGroup(group) {
1404
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1405
+ if (group.singlePath) {
1406
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1407
+ }
1408
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1409
+ return `${group.label} \u2014 ${dirs} (${files})`;
1321
1410
  }
1322
- function computeNewSpecifier(oldSpecifier, newBare) {
1323
- const hasJsExt = oldSpecifier.endsWith(".js");
1324
- const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
1325
- const dir = base.lastIndexOf("/");
1326
- const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
1327
- const newSpec = prefix + newBare;
1328
- return hasJsExt ? `${newSpec}.js` : newSpec;
1411
+
1412
+ // src/display-monorepo.ts
1413
+ import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
1414
+ import chalk3 from "chalk";
1415
+ function formatPackageSummary(pkg) {
1416
+ const parts = [];
1417
+ if (pkg.stack.framework) {
1418
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1419
+ }
1420
+ if (pkg.stack.styling) {
1421
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1422
+ }
1423
+ const files = `${pkg.statistics.totalFiles} files`;
1424
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1425
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1329
1426
  }
1330
- async function updateImportsAfterRenames(renames, projectRoot) {
1331
- if (renames.length === 0) return [];
1332
- const { Project, SyntaxKind } = await import("ts-morph");
1333
- const renameMap = /* @__PURE__ */ new Map();
1334
- for (const r of renames) {
1335
- const oldStripped = stripExtension(r.oldAbsPath);
1336
- const newFilename = path8.basename(r.newPath);
1337
- const newName = newFilename.slice(0, newFilename.indexOf("."));
1338
- renameMap.set(oldStripped, { newBare: newName });
1427
+ function displayMonorepoResults(scanResult) {
1428
+ const { stack, packages } = scanResult;
1429
+ console.log(`
1430
+ ${chalk3.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1431
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.language)}`);
1432
+ if (stack.packageManager) {
1433
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.packageManager)}`);
1339
1434
  }
1340
- const project = new Project({
1341
- tsConfigFilePath: void 0,
1342
- skipAddingFilesFromTsConfig: true
1343
- });
1344
- project.addSourceFilesAtPaths(path8.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
1345
- const updates = [];
1346
- const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
1347
- for (const sourceFile of project.getSourceFiles()) {
1348
- const filePath = sourceFile.getFilePath();
1349
- const segments = filePath.split(path8.sep);
1350
- if (segments.includes("node_modules") || segments.includes("dist")) continue;
1351
- const fileDir = path8.dirname(filePath);
1352
- for (const decl of sourceFile.getImportDeclarations()) {
1353
- const specifier = decl.getModuleSpecifierValue();
1354
- if (!specifier.startsWith(".")) continue;
1355
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
1356
- if (!match) continue;
1357
- const newSpec = computeNewSpecifier(specifier, match.newBare);
1358
- updates.push({
1359
- file: filePath,
1360
- oldSpecifier: specifier,
1361
- newSpecifier: newSpec,
1362
- line: decl.getStartLineNumber()
1363
- });
1364
- decl.setModuleSpecifier(newSpec);
1365
- }
1366
- for (const decl of sourceFile.getExportDeclarations()) {
1367
- const specifier = decl.getModuleSpecifierValue();
1368
- if (!specifier || !specifier.startsWith(".")) continue;
1369
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
1370
- if (!match) continue;
1371
- const newSpec = computeNewSpecifier(specifier, match.newBare);
1372
- updates.push({
1373
- file: filePath,
1374
- oldSpecifier: specifier,
1375
- newSpecifier: newSpec,
1376
- line: decl.getStartLineNumber()
1377
- });
1378
- decl.setModuleSpecifier(newSpec);
1435
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1436
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1437
+ } else {
1438
+ if (stack.linter) {
1439
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.linter)}`);
1379
1440
  }
1380
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1381
- if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
1382
- const args = call.getArguments();
1383
- if (args.length === 0) continue;
1384
- const arg = args[0];
1385
- if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
1386
- const specifier = arg.getText().slice(1, -1);
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: call.getStartLineNumber()
1396
- });
1397
- const quote = arg.getText()[0];
1398
- arg.replaceWithText(`${quote}${newSpec}${quote}`);
1441
+ if (stack.formatter) {
1442
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.formatter)}`);
1399
1443
  }
1400
1444
  }
1401
- if (updates.length > 0) {
1402
- await project.save();
1445
+ if (stack.testRunner) {
1446
+ console.log(` ${chalk3.green("\u2713")} ${formatItem(stack.testRunner)}`);
1403
1447
  }
1404
- return updates;
1448
+ console.log("");
1449
+ for (const pkg of packages) {
1450
+ console.log(formatPackageSummary(pkg));
1451
+ }
1452
+ const packagesWithDirs = packages.filter(
1453
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1454
+ );
1455
+ if (packagesWithDirs.length > 0) {
1456
+ console.log(`
1457
+ ${chalk3.bold("Structure:")}`);
1458
+ for (const pkg of packagesWithDirs) {
1459
+ const groups = groupByRole(pkg.structure.directories);
1460
+ if (groups.length === 0) continue;
1461
+ console.log(` ${pkg.relativePath}:`);
1462
+ for (const group of groups) {
1463
+ console.log(` ${chalk3.green("\u2713")} ${formatRoleGroup(group)}`);
1464
+ }
1465
+ }
1466
+ }
1467
+ displayConventions(scanResult);
1468
+ displaySummarySection(scanResult);
1469
+ console.log("");
1405
1470
  }
1406
- function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
1407
- const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
1408
- const resolved = path8.resolve(fromDir, cleanSpec);
1409
- for (const ext of extensions) {
1410
- const candidate = resolved + ext;
1411
- const stripped = stripExtension(candidate);
1412
- const match = renameMap.get(stripped);
1413
- if (match) return match;
1471
+ function formatPackageSummaryPlain(pkg) {
1472
+ const parts = [];
1473
+ if (pkg.stack.framework) {
1474
+ parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
1414
1475
  }
1415
- return void 0;
1476
+ if (pkg.stack.styling) {
1477
+ parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1478
+ }
1479
+ const files = `${pkg.statistics.totalFiles} files`;
1480
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1481
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1416
1482
  }
1417
-
1418
- // src/commands/fix-naming.ts
1419
- import * as fs9 from "fs";
1420
- import * as path9 from "path";
1421
-
1422
- // src/commands/convert-name.ts
1423
- function splitIntoWords(name) {
1424
- const parts = name.split(/[-_]/);
1425
- const words = [];
1426
- for (const part of parts) {
1427
- if (part === "") continue;
1428
- let current = "";
1429
- for (let i = 0; i < part.length; i++) {
1430
- const ch = part[i];
1431
- const isUpper = ch >= "A" && ch <= "Z";
1432
- if (isUpper && current.length > 0) {
1433
- const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
1434
- const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
1435
- if (!prevIsUpper || nextIsLower) {
1436
- words.push(current.toLowerCase());
1437
- current = "";
1438
- }
1483
+ function formatMonorepoResultsText(scanResult) {
1484
+ const lines = [];
1485
+ const { stack, packages } = scanResult;
1486
+ lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1487
+ const sharedParts = [formatItem(stack.language)];
1488
+ if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1489
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1490
+ sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
1491
+ } else {
1492
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1493
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1494
+ }
1495
+ if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1496
+ lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1497
+ lines.push("");
1498
+ for (const pkg of packages) {
1499
+ lines.push(formatPackageSummaryPlain(pkg));
1500
+ }
1501
+ const packagesWithDirs = packages.filter(
1502
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1503
+ );
1504
+ if (packagesWithDirs.length > 0) {
1505
+ lines.push("");
1506
+ lines.push("Structure:");
1507
+ for (const pkg of packagesWithDirs) {
1508
+ const groups = groupByRole(pkg.structure.directories);
1509
+ if (groups.length === 0) continue;
1510
+ lines.push(` ${pkg.relativePath}:`);
1511
+ for (const group of groups) {
1512
+ lines.push(` \u2713 ${formatRoleGroup(group)}`);
1439
1513
  }
1440
- current += ch;
1441
1514
  }
1442
- if (current) words.push(current.toLowerCase());
1443
1515
  }
1444
- return words;
1445
- }
1446
- function convertName(bare, target) {
1447
- const words = splitIntoWords(bare);
1448
- if (words.length === 0) return bare;
1449
- switch (target) {
1450
- case "kebab-case":
1451
- return words.join("-");
1452
- case "camelCase":
1453
- return words[0] + words.slice(1).map(capitalize).join("");
1454
- case "PascalCase":
1455
- return words.map(capitalize).join("");
1456
- case "snake_case":
1457
- return words.join("_");
1458
- default:
1459
- return bare;
1516
+ lines.push(...formatConventionsText(scanResult));
1517
+ const pkgCount = packages.length > 1 ? packages.length : void 0;
1518
+ lines.push("");
1519
+ lines.push(formatSummary(scanResult.statistics, pkgCount));
1520
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1521
+ if (ext) {
1522
+ lines.push(ext);
1460
1523
  }
1461
- }
1462
- function capitalize(word) {
1463
- if (word.length === 0) return word;
1464
- return word[0].toUpperCase() + word.slice(1);
1524
+ return lines.join("\n");
1465
1525
  }
1466
1526
 
1467
- // src/commands/fix-naming.ts
1468
- function computeRename(relPath, targetConvention, projectRoot) {
1469
- const filename = path9.basename(relPath);
1470
- const dir = path9.dirname(relPath);
1471
- const dotIndex = filename.indexOf(".");
1472
- if (dotIndex === -1) return null;
1473
- const bare = filename.slice(0, dotIndex);
1474
- const suffix = filename.slice(dotIndex);
1475
- const newBare = convertName(bare, targetConvention);
1476
- if (newBare === bare) return null;
1477
- const newFilename = newBare + suffix;
1478
- const newRelPath = path9.join(dir, newFilename);
1479
- const oldAbsPath = path9.join(projectRoot, relPath);
1480
- const newAbsPath = path9.join(projectRoot, newRelPath);
1481
- if (fs9.existsSync(newAbsPath)) return null;
1482
- return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
1483
- }
1484
- function executeRename(rename) {
1485
- if (fs9.existsSync(rename.newAbsPath)) return false;
1486
- fs9.renameSync(rename.oldAbsPath, rename.newAbsPath);
1487
- return true;
1527
+ // src/display.ts
1528
+ function formatItem(item, nameMap) {
1529
+ const name = nameMap?.[item.name] ?? item.name;
1530
+ return item.version ? `${name} ${item.version}` : name;
1488
1531
  }
1489
- function deduplicateRenames(renames) {
1490
- const seen = /* @__PURE__ */ new Set();
1491
- const result = [];
1492
- for (const r of renames) {
1493
- if (seen.has(r.newAbsPath)) continue;
1494
- seen.add(r.newAbsPath);
1495
- result.push(r);
1532
+ function confidenceLabel(convention) {
1533
+ const pct = Math.round(convention.consistency);
1534
+ if (convention.confidence === "high") {
1535
+ return `${pct}% \u2014 high confidence, will enforce`;
1496
1536
  }
1497
- return result;
1537
+ return `${pct}% \u2014 medium confidence, suggested only`;
1498
1538
  }
1499
-
1500
- // src/commands/fix-tests.ts
1501
- import * as fs10 from "fs";
1502
- import * as path10 from "path";
1503
- function generateTestStub(sourceRelPath, config, projectRoot) {
1504
- const pkg = resolvePackageForFile(sourceRelPath, config);
1505
- const testPattern = pkg?.structure?.testPattern;
1506
- if (!testPattern) return null;
1507
- const basename8 = path10.basename(sourceRelPath);
1508
- const ext = path10.extname(basename8);
1509
- if (!ext) return null;
1510
- const stem = basename8.slice(0, -ext.length);
1511
- const testSuffix = testPattern.replace("*", "");
1512
- const testFilename = `${stem}${testSuffix}`;
1513
- const dir = path10.dirname(path10.join(projectRoot, sourceRelPath));
1514
- const testAbsPath = path10.join(dir, testFilename);
1515
- if (fs10.existsSync(testAbsPath)) return null;
1516
- return {
1517
- path: path10.relative(projectRoot, testAbsPath),
1518
- absPath: testAbsPath,
1519
- moduleName: stem
1520
- };
1539
+ function displayConventions(scanResult) {
1540
+ const conventionEntries = Object.entries(scanResult.conventions);
1541
+ if (conventionEntries.length === 0) return;
1542
+ console.log(`
1543
+ ${chalk4.bold("Conventions:")}`);
1544
+ for (const [key, convention] of conventionEntries) {
1545
+ if (convention.confidence === "low") continue;
1546
+ const label = CONVENTION_LABELS[key] ?? key;
1547
+ if (scanResult.packages.length > 1) {
1548
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1549
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1550
+ if (allSame || pkgValues.length <= 1) {
1551
+ const ind = convention.confidence === "high" ? chalk4.green("\u2713") : chalk4.yellow("~");
1552
+ const detail = chalk4.dim(`(${confidenceLabel(convention)})`);
1553
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1554
+ } else {
1555
+ console.log(` ${chalk4.yellow("~")} ${label}: varies by package`);
1556
+ for (const pv of pkgValues) {
1557
+ const pct = Math.round(pv.convention.consistency);
1558
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1559
+ }
1560
+ }
1561
+ } else {
1562
+ const ind = convention.confidence === "high" ? chalk4.green("\u2713") : chalk4.yellow("~");
1563
+ const detail = chalk4.dim(`(${confidenceLabel(convention)})`);
1564
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1565
+ }
1566
+ }
1521
1567
  }
1522
- function writeTestStub(stub, config) {
1523
- const pkg = resolvePackageForFile(stub.path, config);
1524
- const testRunner = pkg?.stack?.testRunner ?? "";
1525
- const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
1526
- const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
1527
- const content = `${importLine}describe('${stub.moduleName}', () => {
1528
- it.todo('add tests');
1529
- });
1530
- `;
1531
- fs10.mkdirSync(path10.dirname(stub.absPath), { recursive: true });
1532
- fs10.writeFileSync(stub.absPath, content);
1568
+ function displaySummarySection(scanResult) {
1569
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1570
+ console.log(`
1571
+ ${chalk4.bold("Summary:")}`);
1572
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1573
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1574
+ if (ext) {
1575
+ console.log(` ${ext}`);
1576
+ }
1533
1577
  }
1534
-
1535
- // src/commands/fix.ts
1536
- var CONFIG_FILE3 = "viberails.config.json";
1537
- async function fixCommand(options, cwd) {
1538
- const startDir = cwd ?? process.cwd();
1539
- const projectRoot = findProjectRoot(startDir);
1540
- if (!projectRoot) {
1541
- console.error(`${chalk4.red("Error:")} No package.json found. Are you in a JS/TS project?`);
1542
- return 1;
1578
+ function displayScanResults(scanResult) {
1579
+ if (scanResult.packages.length > 1) {
1580
+ displayMonorepoResults(scanResult);
1581
+ return;
1543
1582
  }
1544
- const configPath = path11.join(projectRoot, CONFIG_FILE3);
1545
- if (!fs11.existsSync(configPath)) {
1546
- console.error(
1547
- `${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
1548
- );
1549
- return 1;
1583
+ const { stack } = scanResult;
1584
+ console.log(`
1585
+ ${chalk4.bold("Detected:")}`);
1586
+ if (stack.framework) {
1587
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
1550
1588
  }
1551
- const config = await loadConfig3(configPath);
1552
- if (!options.dryRun) {
1553
- const isDirty = checkGitDirty(projectRoot);
1554
- if (isDirty) {
1555
- console.log(
1556
- chalk4.yellow("Warning: You have uncommitted changes. Consider committing first.")
1557
- );
1558
- }
1589
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.language)}`);
1590
+ if (stack.styling) {
1591
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
1559
1592
  }
1560
- const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
1561
- const shouldFixTests = !options.rule || options.rule.includes("missing-test");
1562
- const allFiles = getAllSourceFiles(projectRoot, config);
1563
- const renames = [];
1564
- if (shouldFixNaming) {
1565
- for (const file of allFiles) {
1566
- const resolved = resolveConfigForFile(file, config);
1567
- if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
1568
- const violation = checkNaming(file, resolved.conventions);
1569
- if (!violation) continue;
1570
- const convention = getConventionValue(resolved.conventions.fileNaming);
1571
- if (!convention) continue;
1572
- const rename = computeRename(file, convention, projectRoot);
1573
- if (rename) renames.push(rename);
1574
- }
1593
+ if (stack.backend) {
1594
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
1575
1595
  }
1576
- const dedupedRenames = deduplicateRenames(renames);
1577
- const testStubs = [];
1578
- if (shouldFixTests) {
1579
- const testViolations = checkMissingTests(projectRoot, config, "warn");
1580
- for (const v of testViolations) {
1581
- const stub = generateTestStub(v.file, config, projectRoot);
1582
- if (stub) testStubs.push(stub);
1583
- }
1596
+ if (stack.orm) {
1597
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
1584
1598
  }
1585
- if (dedupedRenames.length === 0 && testStubs.length === 0) {
1586
- console.log(`${chalk4.green("\u2713")} No fixable violations found.`);
1587
- return 0;
1599
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1600
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1601
+ } else {
1602
+ if (stack.linter) {
1603
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)}`);
1604
+ }
1605
+ if (stack.formatter) {
1606
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.formatter)}`);
1607
+ }
1588
1608
  }
1589
- printPlan(dedupedRenames, testStubs);
1590
- if (options.dryRun) {
1591
- console.log(chalk4.dim("\nDry run \u2014 no changes applied."));
1592
- return 0;
1609
+ if (stack.testRunner) {
1610
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.testRunner)}`);
1593
1611
  }
1594
- if (!options.yes) {
1595
- const confirmed = await confirmDangerous("Apply these fixes?");
1596
- if (!confirmed) {
1597
- console.log("Aborted.");
1598
- return 0;
1599
- }
1612
+ if (stack.packageManager) {
1613
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.packageManager)}`);
1600
1614
  }
1601
- let renameCount = 0;
1602
- for (const rename of dedupedRenames) {
1603
- if (executeRename(rename)) {
1604
- renameCount++;
1615
+ if (stack.libraries.length > 0) {
1616
+ for (const lib of stack.libraries) {
1617
+ console.log(` ${chalk4.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
1605
1618
  }
1606
1619
  }
1607
- let importUpdateCount = 0;
1608
- if (renameCount > 0) {
1609
- const appliedRenames = dedupedRenames.filter((r) => fs11.existsSync(r.newAbsPath));
1610
- const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
1611
- importUpdateCount = updates.length;
1612
- }
1613
- let stubCount = 0;
1614
- for (const stub of testStubs) {
1615
- if (!fs11.existsSync(stub.absPath)) {
1616
- writeTestStub(stub, config);
1617
- stubCount++;
1620
+ const groups = groupByRole(scanResult.structure.directories);
1621
+ if (groups.length > 0) {
1622
+ console.log(`
1623
+ ${chalk4.bold("Structure:")}`);
1624
+ for (const group of groups) {
1625
+ console.log(` ${chalk4.green("\u2713")} ${formatRoleGroup(group)}`);
1618
1626
  }
1619
1627
  }
1628
+ displayConventions(scanResult);
1629
+ displaySummarySection(scanResult);
1620
1630
  console.log("");
1621
- if (renameCount > 0) {
1622
- console.log(`${chalk4.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
1623
- }
1624
- if (importUpdateCount > 0) {
1625
- console.log(
1626
- `${chalk4.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
1627
- );
1631
+ }
1632
+ function displayRulesPreview(config) {
1633
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1634
+ console.log(
1635
+ `${chalk4.bold("Rules:")} ${chalk4.dim("(warns on violation; use --enforce in CI to block)")}`
1636
+ );
1637
+ console.log(` ${chalk4.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1638
+ if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
1639
+ console.log(
1640
+ ` ${chalk4.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
1641
+ );
1642
+ } else if (config.rules.testCoverage > 0) {
1643
+ console.log(` ${chalk4.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
1644
+ } else {
1645
+ console.log(` ${chalk4.dim("\u2022")} Test coverage target: disabled`);
1628
1646
  }
1629
- if (stubCount > 0) {
1630
- console.log(`${chalk4.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
1647
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1648
+ console.log(` ${chalk4.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
1649
+ } else {
1650
+ console.log(` ${chalk4.dim("\u2022")} Enforce file naming: no`);
1631
1651
  }
1632
- return 0;
1652
+ console.log(
1653
+ ` ${chalk4.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1654
+ );
1655
+ console.log("");
1633
1656
  }
1634
-
1635
- // src/commands/init.ts
1636
- import * as fs17 from "fs";
1637
- import * as path17 from "path";
1638
- import * as clack7 from "@clack/prompts";
1639
- import { compactConfig as compactConfig2, generateConfig } from "@viberails/config";
1640
- import { scan } from "@viberails/scanner";
1641
- import chalk10 from "chalk";
1642
-
1643
- // src/display.ts
1644
- import {
1645
- CONVENTION_LABELS as CONVENTION_LABELS2,
1646
- FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
1647
- LIBRARY_NAMES as LIBRARY_NAMES2,
1648
- ORM_NAMES as ORM_NAMES2,
1649
- STYLING_NAMES as STYLING_NAMES3
1650
- } from "@viberails/types";
1651
- import chalk6 from "chalk";
1652
-
1653
- // src/display-helpers.ts
1654
- import { ROLE_DESCRIPTIONS } from "@viberails/types";
1655
- function groupByRole(directories) {
1656
- const map = /* @__PURE__ */ new Map();
1657
- for (const dir of directories) {
1658
- if (dir.role === "unknown") continue;
1659
- const existing = map.get(dir.role);
1660
- if (existing) {
1661
- existing.dirs.push(dir);
1657
+ function displayInitSummary(config, exemptedPackages) {
1658
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1659
+ const isMonorepo = config.packages.length > 1;
1660
+ const ok = chalk4.green("\u2713");
1661
+ const off = chalk4.dim("\u25CB");
1662
+ console.log("");
1663
+ console.log(` ${chalk4.bold("Rules to apply:")}`);
1664
+ console.log(` ${ok} Max file size: ${chalk4.cyan(`${config.rules.maxFileLines} lines`)}`);
1665
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1666
+ console.log(` ${ok} File naming: ${chalk4.cyan(root.conventions.fileNaming)}`);
1667
+ } else {
1668
+ console.log(` ${off} File naming: ${chalk4.dim("not enforced")}`);
1669
+ }
1670
+ if (config.rules.enforceMissingTests && root?.structure?.testPattern) {
1671
+ console.log(` ${ok} Missing tests: ${chalk4.cyan(`enforced (${root.structure.testPattern})`)}`);
1672
+ } else if (config.rules.enforceMissingTests) {
1673
+ console.log(` ${ok} Missing tests: ${chalk4.cyan("enforced")}`);
1674
+ } else {
1675
+ console.log(` ${off} Missing tests: ${chalk4.dim("not enforced")}`);
1676
+ }
1677
+ if (config.rules.testCoverage > 0) {
1678
+ if (isMonorepo) {
1679
+ const withCoverage = config.packages.filter(
1680
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
1681
+ );
1682
+ console.log(
1683
+ ` ${ok} Coverage: ${chalk4.cyan(`${config.rules.testCoverage}%`)} default ${chalk4.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
1684
+ );
1662
1685
  } else {
1663
- map.set(dir.role, { dirs: [dir] });
1686
+ console.log(` ${ok} Coverage: ${chalk4.cyan(`${config.rules.testCoverage}%`)}`);
1664
1687
  }
1688
+ } else {
1689
+ console.log(` ${off} Coverage: ${chalk4.dim("disabled")}`);
1665
1690
  }
1666
- const groups = [];
1667
- for (const [role, { dirs }] of map) {
1668
- const label = ROLE_DESCRIPTIONS[role] ?? role;
1669
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1670
- groups.push({
1671
- role,
1672
- label,
1673
- dirCount: dirs.length,
1674
- totalFiles,
1675
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
1676
- });
1677
- }
1678
- return groups;
1679
- }
1680
- function formatSummary(stats, packageCount) {
1681
- const parts = [];
1682
- if (packageCount && packageCount > 1) {
1683
- parts.push(`${packageCount} packages`);
1691
+ if (exemptedPackages.length > 0) {
1692
+ console.log(
1693
+ ` ${chalk4.dim(" exempted:")} ${chalk4.dim(exemptedPackages.join(", "))} ${chalk4.dim("(types-only)")}`
1694
+ );
1684
1695
  }
1685
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1686
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1687
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1688
- return parts.join(" \xB7 ");
1689
- }
1690
- function formatExtensions(filesByExtension, maxEntries = 4) {
1691
- return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1692
- }
1693
- function formatRoleGroup(group) {
1694
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1695
- if (group.singlePath) {
1696
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1696
+ if (isMonorepo) {
1697
+ console.log(
1698
+ `
1699
+ ${chalk4.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
1700
+ );
1701
+ } else {
1702
+ console.log(`
1703
+ ${chalk4.dim("warns on violation \xB7 use --enforce in CI to block")}`);
1697
1704
  }
1698
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1699
- return `${group.label} \u2014 ${dirs} (${files})`;
1705
+ console.log("");
1700
1706
  }
1701
1707
 
1702
- // src/display-monorepo.ts
1703
- import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
1704
- import chalk5 from "chalk";
1705
-
1706
1708
  // src/display-text.ts
1707
- import {
1708
- CONVENTION_LABELS,
1709
- FRAMEWORK_NAMES,
1710
- LIBRARY_NAMES,
1711
- ORM_NAMES,
1712
- STYLING_NAMES
1713
- } from "@viberails/types";
1714
1709
  function plainConfidenceLabel(convention) {
1715
1710
  const pct = Math.round(convention.consistency);
1716
1711
  if (convention.confidence === "high") {
@@ -1726,7 +1721,7 @@ function formatConventionsText(scanResult) {
1726
1721
  lines.push("Conventions:");
1727
1722
  for (const [key, convention] of conventionEntries) {
1728
1723
  if (convention.confidence === "low") continue;
1729
- const label = CONVENTION_LABELS[key] ?? key;
1724
+ const label = CONVENTION_LABELS2[key] ?? key;
1730
1725
  if (scanResult.packages.length > 1) {
1731
1726
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1732
1727
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
@@ -1749,10 +1744,13 @@ function formatConventionsText(scanResult) {
1749
1744
  }
1750
1745
  function formatRulesText(config) {
1751
1746
  const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1747
+ const isMonorepo = config.packages.length > 1;
1752
1748
  const lines = [];
1753
1749
  lines.push(`Max file size: ${config.rules.maxFileLines} lines`);
1754
1750
  if (config.rules.testCoverage > 0) {
1755
- lines.push(`Test coverage target: ${config.rules.testCoverage}%`);
1751
+ const label = isMonorepo ? "Default coverage target" : "Test coverage target";
1752
+ const suffix = isMonorepo ? " (per-package)" : "";
1753
+ lines.push(`${label}: ${config.rules.testCoverage}%${suffix}`);
1756
1754
  } else {
1757
1755
  lines.push("Test coverage target: disabled");
1758
1756
  }
@@ -1777,17 +1775,17 @@ function formatScanResultsText(scanResult) {
1777
1775
  const { stack } = scanResult;
1778
1776
  lines.push("Detected:");
1779
1777
  if (stack.framework) {
1780
- lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES)}`);
1778
+ lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
1781
1779
  }
1782
1780
  lines.push(` \u2713 ${formatItem(stack.language)}`);
1783
1781
  if (stack.styling) {
1784
- lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES)}`);
1782
+ lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
1785
1783
  }
1786
1784
  if (stack.backend) {
1787
- lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES)}`);
1785
+ lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
1788
1786
  }
1789
1787
  if (stack.orm) {
1790
- lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES)}`);
1788
+ lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
1791
1789
  }
1792
1790
  const secondaryParts = [];
1793
1791
  if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
@@ -1799,7 +1797,7 @@ function formatScanResultsText(scanResult) {
1799
1797
  }
1800
1798
  if (stack.libraries.length > 0) {
1801
1799
  for (const lib of stack.libraries) {
1802
- lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES)}`);
1800
+ lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
1803
1801
  }
1804
1802
  }
1805
1803
  const groups = groupByRole(scanResult.structure.directories);
@@ -1821,256 +1819,652 @@ function formatScanResultsText(scanResult) {
1821
1819
  return lines.join("\n");
1822
1820
  }
1823
1821
 
1824
- // src/display-monorepo.ts
1825
- function formatPackageSummary(pkg) {
1826
- const parts = [];
1827
- if (pkg.stack.framework) {
1828
- parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES2));
1822
+ // src/utils/apply-rule-overrides.ts
1823
+ function applyRuleOverrides(config, overrides) {
1824
+ if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
1825
+ config.rules.maxFileLines = overrides.maxFileLines;
1826
+ config.rules.testCoverage = overrides.testCoverage;
1827
+ config.rules.enforceMissingTests = overrides.enforceMissingTests;
1828
+ config.rules.enforceNaming = overrides.enforceNaming;
1829
+ for (const pkg of config.packages) {
1830
+ pkg.coverage = pkg.coverage ?? {};
1831
+ if (pkg.coverage.summaryPath === void 0) {
1832
+ pkg.coverage.summaryPath = overrides.coverageSummaryPath;
1833
+ }
1834
+ if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
1835
+ pkg.coverage.command = overrides.coverageCommand;
1836
+ }
1829
1837
  }
1830
- if (pkg.stack.styling) {
1831
- parts.push(formatItem(pkg.stack.styling, STYLING_NAMES2));
1838
+ if (overrides.fileNamingValue) {
1839
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1840
+ const oldNaming = rootPkg.conventions?.fileNaming;
1841
+ rootPkg.conventions = rootPkg.conventions ?? {};
1842
+ rootPkg.conventions.fileNaming = overrides.fileNamingValue;
1843
+ if (oldNaming && oldNaming !== overrides.fileNamingValue) {
1844
+ for (const pkg of config.packages) {
1845
+ if (pkg.conventions?.fileNaming === oldNaming) {
1846
+ pkg.conventions.fileNaming = overrides.fileNamingValue;
1847
+ }
1848
+ }
1849
+ }
1832
1850
  }
1833
- const files = `${pkg.statistics.totalFiles} files`;
1834
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1835
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1836
1851
  }
1837
- function displayMonorepoResults(scanResult) {
1838
- const { stack, packages } = scanResult;
1839
- console.log(`
1840
- ${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1841
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
1842
- if (stack.packageManager) {
1843
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
1852
+
1853
+ // src/utils/diff-configs.ts
1854
+ import { CONVENTION_LABELS as CONVENTION_LABELS3, FRAMEWORK_NAMES as FRAMEWORK_NAMES4, ORM_NAMES as ORM_NAMES3, STYLING_NAMES as STYLING_NAMES4 } from "@viberails/types";
1855
+ function parseStackString(s) {
1856
+ const atIdx = s.indexOf("@");
1857
+ if (atIdx > 0) {
1858
+ return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1844
1859
  }
1845
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1846
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1847
- } else {
1848
- if (stack.linter) {
1849
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
1860
+ return { name: s };
1861
+ }
1862
+ function displayStackName(s) {
1863
+ const { name, version } = parseStackString(s);
1864
+ const allMaps = {
1865
+ ...FRAMEWORK_NAMES4,
1866
+ ...STYLING_NAMES4,
1867
+ ...ORM_NAMES3
1868
+ };
1869
+ const display = allMaps[name] ?? name;
1870
+ return version ? `${display} ${version}` : display;
1871
+ }
1872
+ function isNewlyDetected(config, pkgPath, key) {
1873
+ return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
1874
+ }
1875
+ var STACK_FIELDS = [
1876
+ "framework",
1877
+ "styling",
1878
+ "backend",
1879
+ "orm",
1880
+ "linter",
1881
+ "formatter",
1882
+ "testRunner"
1883
+ ];
1884
+ var CONVENTION_KEYS = [
1885
+ "fileNaming",
1886
+ "componentNaming",
1887
+ "hookNaming",
1888
+ "importAlias"
1889
+ ];
1890
+ var STRUCTURE_FIELDS = [
1891
+ { key: "srcDir", label: "source directory" },
1892
+ { key: "pages", label: "pages directory" },
1893
+ { key: "components", label: "components directory" },
1894
+ { key: "hooks", label: "hooks directory" },
1895
+ { key: "utils", label: "utilities directory" },
1896
+ { key: "types", label: "types directory" },
1897
+ { key: "tests", label: "tests directory" },
1898
+ { key: "testPattern", label: "test pattern" }
1899
+ ];
1900
+ function diffPackage(existing, merged, mergedConfig) {
1901
+ const changes = [];
1902
+ const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
1903
+ for (const field of STACK_FIELDS) {
1904
+ const oldVal = existing.stack?.[field];
1905
+ const newVal = merged.stack?.[field];
1906
+ if (!oldVal && newVal) {
1907
+ changes.push({
1908
+ type: "added",
1909
+ description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
1910
+ });
1911
+ } else if (oldVal && newVal && oldVal !== newVal) {
1912
+ changes.push({
1913
+ type: "changed",
1914
+ description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1915
+ });
1850
1916
  }
1851
- if (stack.formatter) {
1852
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
1917
+ }
1918
+ for (const key of CONVENTION_KEYS) {
1919
+ const oldVal = existing.conventions?.[key];
1920
+ const newVal = merged.conventions?.[key];
1921
+ const label = CONVENTION_LABELS3[key] ?? key;
1922
+ if (!oldVal && newVal) {
1923
+ changes.push({
1924
+ type: "added",
1925
+ description: `${pkgPrefix}New convention: ${label} (${newVal})`
1926
+ });
1927
+ } else if (oldVal && newVal && oldVal !== newVal) {
1928
+ const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
1929
+ changes.push({
1930
+ type: "changed",
1931
+ description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
1932
+ });
1853
1933
  }
1854
1934
  }
1855
- if (stack.testRunner) {
1856
- console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
1935
+ for (const { key, label } of STRUCTURE_FIELDS) {
1936
+ const oldVal = existing.structure?.[key];
1937
+ const newVal = merged.structure?.[key];
1938
+ if (!oldVal && newVal) {
1939
+ changes.push({
1940
+ type: "added",
1941
+ description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
1942
+ });
1943
+ }
1857
1944
  }
1858
- console.log("");
1859
- for (const pkg of packages) {
1860
- console.log(formatPackageSummary(pkg));
1945
+ return changes;
1946
+ }
1947
+ function diffConfigs(existing, merged) {
1948
+ const changes = [];
1949
+ const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
1950
+ const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
1951
+ for (const existingPkg of existing.packages) {
1952
+ const mergedPkg = mergedByPath.get(existingPkg.path);
1953
+ if (mergedPkg) {
1954
+ changes.push(...diffPackage(existingPkg, mergedPkg, merged));
1955
+ }
1861
1956
  }
1862
- const packagesWithDirs = packages.filter(
1863
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1864
- );
1865
- if (packagesWithDirs.length > 0) {
1866
- console.log(`
1867
- ${chalk5.bold("Structure:")}`);
1868
- for (const pkg of packagesWithDirs) {
1869
- const groups = groupByRole(pkg.structure.directories);
1870
- if (groups.length === 0) continue;
1871
- console.log(` ${pkg.relativePath}:`);
1872
- for (const group of groups) {
1873
- console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
1874
- }
1957
+ for (const mergedPkg of merged.packages) {
1958
+ if (!existingByPath.has(mergedPkg.path)) {
1959
+ changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
1875
1960
  }
1876
1961
  }
1877
- displayConventions(scanResult);
1878
- displaySummarySection(scanResult);
1879
- console.log("");
1962
+ return changes;
1880
1963
  }
1881
- function formatPackageSummaryPlain(pkg) {
1964
+ function formatStatsDelta(oldStats, newStats) {
1965
+ const fileDelta = newStats.totalFiles - oldStats.totalFiles;
1966
+ const lineDelta = newStats.totalLines - oldStats.totalLines;
1967
+ if (fileDelta === 0 && lineDelta === 0) return void 0;
1882
1968
  const parts = [];
1883
- if (pkg.stack.framework) {
1884
- parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES2));
1969
+ if (fileDelta !== 0) {
1970
+ const sign = fileDelta > 0 ? "+" : "";
1971
+ parts.push(`${sign}${fileDelta.toLocaleString()} files`);
1885
1972
  }
1886
- if (pkg.stack.styling) {
1887
- parts.push(formatItem(pkg.stack.styling, STYLING_NAMES2));
1973
+ if (lineDelta !== 0) {
1974
+ const sign = lineDelta > 0 ? "+" : "";
1975
+ parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
1888
1976
  }
1889
- const files = `${pkg.statistics.totalFiles} files`;
1890
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1891
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1977
+ return `${parts.join(", ")} since last sync`;
1892
1978
  }
1893
- function formatMonorepoResultsText(scanResult) {
1894
- const lines = [];
1895
- const { stack, packages } = scanResult;
1896
- lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1897
- const sharedParts = [formatItem(stack.language)];
1898
- if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1899
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1900
- sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
1901
- } else {
1902
- if (stack.linter) sharedParts.push(formatItem(stack.linter));
1903
- if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1979
+
1980
+ // src/utils/write-generated-files.ts
1981
+ import * as fs9 from "fs";
1982
+ import * as path8 from "path";
1983
+ import { generateContext } from "@viberails/context";
1984
+ var CONTEXT_DIR = ".viberails";
1985
+ var CONTEXT_FILE = "context.md";
1986
+ var SCAN_RESULT_FILE = "scan-result.json";
1987
+ function writeGeneratedFiles(projectRoot, config, scanResult) {
1988
+ const contextDir = path8.join(projectRoot, CONTEXT_DIR);
1989
+ try {
1990
+ if (!fs9.existsSync(contextDir)) {
1991
+ fs9.mkdirSync(contextDir, { recursive: true });
1992
+ }
1993
+ const context = generateContext(config);
1994
+ fs9.writeFileSync(path8.join(contextDir, CONTEXT_FILE), context);
1995
+ fs9.writeFileSync(
1996
+ path8.join(contextDir, SCAN_RESULT_FILE),
1997
+ `${JSON.stringify(scanResult, null, 2)}
1998
+ `
1999
+ );
2000
+ } catch (err) {
2001
+ const message = err instanceof Error ? err.message : String(err);
2002
+ throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
1904
2003
  }
1905
- if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1906
- lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1907
- lines.push("");
1908
- for (const pkg of packages) {
1909
- lines.push(formatPackageSummaryPlain(pkg));
2004
+ }
2005
+
2006
+ // src/commands/config.ts
2007
+ var CONFIG_FILE3 = "viberails.config.json";
2008
+ async function configCommand(options, cwd) {
2009
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
2010
+ if (!projectRoot) {
2011
+ throw new Error("No package.json found. Make sure you are inside a JS/TS project.");
1910
2012
  }
1911
- const packagesWithDirs = packages.filter(
1912
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1913
- );
1914
- if (packagesWithDirs.length > 0) {
1915
- lines.push("");
1916
- lines.push("Structure:");
1917
- for (const pkg of packagesWithDirs) {
1918
- const groups = groupByRole(pkg.structure.directories);
1919
- if (groups.length === 0) continue;
1920
- lines.push(` ${pkg.relativePath}:`);
1921
- for (const group of groups) {
1922
- lines.push(` \u2713 ${formatRoleGroup(group)}`);
2013
+ const configPath = path9.join(projectRoot, CONFIG_FILE3);
2014
+ if (!fs10.existsSync(configPath)) {
2015
+ console.log(`${chalk5.yellow("!")} No config found. Run ${chalk5.cyan("viberails init")} first.`);
2016
+ return;
2017
+ }
2018
+ clack6.intro("viberails config");
2019
+ const config = await loadConfig3(configPath);
2020
+ let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2021
+ clack6.note(formatRulesText(config).join("\n"), "Current rules");
2022
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2023
+ const overrides = await promptRuleMenu({
2024
+ maxFileLines: config.rules.maxFileLines,
2025
+ testCoverage: config.rules.testCoverage,
2026
+ enforceMissingTests: config.rules.enforceMissingTests,
2027
+ enforceNaming: config.rules.enforceNaming,
2028
+ fileNamingValue: rootPkg.conventions?.fileNaming,
2029
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
2030
+ coverageCommand: config.defaults?.coverage?.command,
2031
+ packageOverrides: config.packages
2032
+ });
2033
+ applyRuleOverrides(config, overrides);
2034
+ if (options.rescan && config.packages.length > 1) {
2035
+ const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
2036
+ if (shouldInfer) {
2037
+ const bs = clack6.spinner();
2038
+ bs.start("Building import graph...");
2039
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2040
+ const packages = resolveWorkspacePackages(projectRoot, config.packages);
2041
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
2042
+ const inferred = inferBoundaries(graph);
2043
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
2044
+ if (denyCount > 0) {
2045
+ config.boundaries = inferred;
2046
+ config.rules.enforceBoundaries = true;
2047
+ bs.stop(`Inferred ${denyCount} boundary rules`);
2048
+ } else {
2049
+ bs.stop("No boundary rules inferred");
1923
2050
  }
1924
2051
  }
1925
2052
  }
1926
- lines.push(...formatConventionsText(scanResult));
1927
- const pkgCount = packages.length > 1 ? packages.length : void 0;
1928
- lines.push("");
1929
- lines.push(formatSummary(scanResult.statistics, pkgCount));
1930
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
1931
- if (ext) {
1932
- lines.push(ext);
2053
+ const shouldWrite = await confirm3("Save updated configuration?");
2054
+ if (!shouldWrite) {
2055
+ clack6.outro("No changes written.");
2056
+ return;
2057
+ }
2058
+ const compacted = compactConfig2(config);
2059
+ fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2060
+ `);
2061
+ if (!scanResult) {
2062
+ const s = clack6.spinner();
2063
+ s.start("Scanning for context generation...");
2064
+ scanResult = await scan(projectRoot);
2065
+ s.stop("Scan complete");
2066
+ }
2067
+ writeGeneratedFiles(projectRoot, config, scanResult);
2068
+ clack6.log.success(
2069
+ `Updated:
2070
+ ${CONFIG_FILE3}
2071
+ .viberails/context.md
2072
+ .viberails/scan-result.json`
2073
+ );
2074
+ clack6.outro("Done! Run viberails check to verify.");
2075
+ }
2076
+ async function rescanAndMerge(projectRoot, config) {
2077
+ const s = clack6.spinner();
2078
+ s.start("Re-scanning project...");
2079
+ const scanResult = await scan(projectRoot);
2080
+ const merged = mergeConfig(config, scanResult);
2081
+ s.stop("Scan complete");
2082
+ const changes = diffConfigs(config, merged);
2083
+ if (changes.length > 0) {
2084
+ const changeLines = changes.map((c) => {
2085
+ const icon = c.type === "removed" ? "-" : "+";
2086
+ return `${icon} ${c.description}`;
2087
+ }).join("\n");
2088
+ clack6.note(changeLines, "Changes detected");
2089
+ } else {
2090
+ clack6.log.info("No new changes detected from scan.");
2091
+ }
2092
+ Object.assign(config, merged);
2093
+ return scanResult;
2094
+ }
2095
+
2096
+ // src/commands/fix.ts
2097
+ import * as fs13 from "fs";
2098
+ import * as path13 from "path";
2099
+ import { loadConfig as loadConfig4 } from "@viberails/config";
2100
+ import chalk7 from "chalk";
2101
+
2102
+ // src/commands/fix-helpers.ts
2103
+ import { execSync as execSync2 } from "child_process";
2104
+ import chalk6 from "chalk";
2105
+ function printPlan(renames, stubs) {
2106
+ if (renames.length > 0) {
2107
+ console.log(chalk6.bold("\nFile renames:"));
2108
+ for (const r of renames) {
2109
+ console.log(` ${chalk6.red(r.oldPath)} \u2192 ${chalk6.green(r.newPath)}`);
2110
+ }
2111
+ }
2112
+ if (stubs.length > 0) {
2113
+ console.log(chalk6.bold("\nTest stubs to create:"));
2114
+ for (const s of stubs) {
2115
+ console.log(` ${chalk6.green("+")} ${s.path}`);
2116
+ }
2117
+ }
2118
+ }
2119
+ function checkGitDirty(projectRoot) {
2120
+ try {
2121
+ const output = execSync2("git status --porcelain", {
2122
+ cwd: projectRoot,
2123
+ encoding: "utf-8",
2124
+ stdio: ["ignore", "pipe", "ignore"]
2125
+ });
2126
+ return output.trim().length > 0;
2127
+ } catch {
2128
+ return false;
2129
+ }
2130
+ }
2131
+ function getConventionValue(convention) {
2132
+ if (typeof convention === "string") return convention;
2133
+ return void 0;
2134
+ }
2135
+
2136
+ // src/commands/fix-imports.ts
2137
+ import * as path10 from "path";
2138
+ function stripExtension(filePath) {
2139
+ return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2140
+ }
2141
+ function computeNewSpecifier(oldSpecifier, newBare) {
2142
+ const hasJsExt = oldSpecifier.endsWith(".js");
2143
+ const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
2144
+ const dir = base.lastIndexOf("/");
2145
+ const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
2146
+ const newSpec = prefix + newBare;
2147
+ return hasJsExt ? `${newSpec}.js` : newSpec;
2148
+ }
2149
+ async function updateImportsAfterRenames(renames, projectRoot) {
2150
+ if (renames.length === 0) return [];
2151
+ const { Project, SyntaxKind } = await import("ts-morph");
2152
+ const renameMap = /* @__PURE__ */ new Map();
2153
+ for (const r of renames) {
2154
+ const oldStripped = stripExtension(r.oldAbsPath);
2155
+ const newFilename = path10.basename(r.newPath);
2156
+ const newName = newFilename.slice(0, newFilename.indexOf("."));
2157
+ renameMap.set(oldStripped, { newBare: newName });
2158
+ }
2159
+ const project = new Project({
2160
+ tsConfigFilePath: void 0,
2161
+ skipAddingFilesFromTsConfig: true
2162
+ });
2163
+ project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
2164
+ const updates = [];
2165
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
2166
+ for (const sourceFile of project.getSourceFiles()) {
2167
+ const filePath = sourceFile.getFilePath();
2168
+ const segments = filePath.split(path10.sep);
2169
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
2170
+ const fileDir = path10.dirname(filePath);
2171
+ for (const decl of sourceFile.getImportDeclarations()) {
2172
+ const specifier = decl.getModuleSpecifierValue();
2173
+ if (!specifier.startsWith(".")) continue;
2174
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2175
+ if (!match) continue;
2176
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2177
+ updates.push({
2178
+ file: filePath,
2179
+ oldSpecifier: specifier,
2180
+ newSpecifier: newSpec,
2181
+ line: decl.getStartLineNumber()
2182
+ });
2183
+ decl.setModuleSpecifier(newSpec);
2184
+ }
2185
+ for (const decl of sourceFile.getExportDeclarations()) {
2186
+ const specifier = decl.getModuleSpecifierValue();
2187
+ if (!specifier || !specifier.startsWith(".")) continue;
2188
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2189
+ if (!match) continue;
2190
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2191
+ updates.push({
2192
+ file: filePath,
2193
+ oldSpecifier: specifier,
2194
+ newSpecifier: newSpec,
2195
+ line: decl.getStartLineNumber()
2196
+ });
2197
+ decl.setModuleSpecifier(newSpec);
2198
+ }
2199
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2200
+ if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
2201
+ const args = call.getArguments();
2202
+ if (args.length === 0) continue;
2203
+ const arg = args[0];
2204
+ if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
2205
+ const specifier = arg.getText().slice(1, -1);
2206
+ if (!specifier.startsWith(".")) continue;
2207
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2208
+ if (!match) continue;
2209
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2210
+ updates.push({
2211
+ file: filePath,
2212
+ oldSpecifier: specifier,
2213
+ newSpecifier: newSpec,
2214
+ line: call.getStartLineNumber()
2215
+ });
2216
+ const quote = arg.getText()[0];
2217
+ arg.replaceWithText(`${quote}${newSpec}${quote}`);
2218
+ }
2219
+ }
2220
+ if (updates.length > 0) {
2221
+ await project.save();
1933
2222
  }
1934
- return lines.join("\n");
1935
- }
1936
-
1937
- // src/display.ts
1938
- function formatItem(item, nameMap) {
1939
- const name = nameMap?.[item.name] ?? item.name;
1940
- return item.version ? `${name} ${item.version}` : name;
2223
+ return updates;
1941
2224
  }
1942
- function confidenceLabel(convention) {
1943
- const pct = Math.round(convention.consistency);
1944
- if (convention.confidence === "high") {
1945
- return `${pct}% \u2014 high confidence, will enforce`;
2225
+ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
2226
+ const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
2227
+ const resolved = path10.resolve(fromDir, cleanSpec);
2228
+ for (const ext of extensions) {
2229
+ const candidate = resolved + ext;
2230
+ const stripped = stripExtension(candidate);
2231
+ const match = renameMap.get(stripped);
2232
+ if (match) return match;
1946
2233
  }
1947
- return `${pct}% \u2014 medium confidence, suggested only`;
2234
+ return void 0;
1948
2235
  }
1949
- function displayConventions(scanResult) {
1950
- const conventionEntries = Object.entries(scanResult.conventions);
1951
- if (conventionEntries.length === 0) return;
1952
- console.log(`
1953
- ${chalk6.bold("Conventions:")}`);
1954
- for (const [key, convention] of conventionEntries) {
1955
- if (convention.confidence === "low") continue;
1956
- const label = CONVENTION_LABELS2[key] ?? key;
1957
- if (scanResult.packages.length > 1) {
1958
- const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1959
- const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1960
- if (allSame || pkgValues.length <= 1) {
1961
- const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1962
- const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1963
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1964
- } else {
1965
- console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
1966
- for (const pv of pkgValues) {
1967
- const pct = Math.round(pv.convention.consistency);
1968
- console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
2236
+
2237
+ // src/commands/fix-naming.ts
2238
+ import * as fs11 from "fs";
2239
+ import * as path11 from "path";
2240
+
2241
+ // src/commands/convert-name.ts
2242
+ function splitIntoWords(name) {
2243
+ const parts = name.split(/[-_]/);
2244
+ const words = [];
2245
+ for (const part of parts) {
2246
+ if (part === "") continue;
2247
+ let current = "";
2248
+ for (let i = 0; i < part.length; i++) {
2249
+ const ch = part[i];
2250
+ const isUpper = ch >= "A" && ch <= "Z";
2251
+ if (isUpper && current.length > 0) {
2252
+ const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
2253
+ const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
2254
+ if (!prevIsUpper || nextIsLower) {
2255
+ words.push(current.toLowerCase());
2256
+ current = "";
1969
2257
  }
1970
2258
  }
1971
- } else {
1972
- const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
1973
- const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
1974
- console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
2259
+ current += ch;
1975
2260
  }
2261
+ if (current) words.push(current.toLowerCase());
1976
2262
  }
2263
+ return words;
1977
2264
  }
1978
- function displaySummarySection(scanResult) {
1979
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1980
- console.log(`
1981
- ${chalk6.bold("Summary:")}`);
1982
- console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1983
- const ext = formatExtensions(scanResult.statistics.filesByExtension);
1984
- if (ext) {
1985
- console.log(` ${ext}`);
2265
+ function convertName(bare, target) {
2266
+ const words = splitIntoWords(bare);
2267
+ if (words.length === 0) return bare;
2268
+ switch (target) {
2269
+ case "kebab-case":
2270
+ return words.join("-");
2271
+ case "camelCase":
2272
+ return words[0] + words.slice(1).map(capitalize).join("");
2273
+ case "PascalCase":
2274
+ return words.map(capitalize).join("");
2275
+ case "snake_case":
2276
+ return words.join("_");
2277
+ default:
2278
+ return bare;
1986
2279
  }
1987
2280
  }
1988
- function displayScanResults(scanResult) {
1989
- if (scanResult.packages.length > 1) {
1990
- displayMonorepoResults(scanResult);
1991
- return;
1992
- }
1993
- const { stack } = scanResult;
1994
- console.log(`
1995
- ${chalk6.bold("Detected:")}`);
1996
- if (stack.framework) {
1997
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
2281
+ function capitalize(word) {
2282
+ if (word.length === 0) return word;
2283
+ return word[0].toUpperCase() + word.slice(1);
2284
+ }
2285
+
2286
+ // src/commands/fix-naming.ts
2287
+ function computeRename(relPath, targetConvention, projectRoot) {
2288
+ const filename = path11.basename(relPath);
2289
+ const dir = path11.dirname(relPath);
2290
+ const dotIndex = filename.indexOf(".");
2291
+ if (dotIndex === -1) return null;
2292
+ const bare = filename.slice(0, dotIndex);
2293
+ const suffix = filename.slice(dotIndex);
2294
+ const newBare = convertName(bare, targetConvention);
2295
+ if (newBare === bare) return null;
2296
+ const newFilename = newBare + suffix;
2297
+ const newRelPath = path11.join(dir, newFilename);
2298
+ const oldAbsPath = path11.join(projectRoot, relPath);
2299
+ const newAbsPath = path11.join(projectRoot, newRelPath);
2300
+ if (fs11.existsSync(newAbsPath)) return null;
2301
+ return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
2302
+ }
2303
+ function executeRename(rename) {
2304
+ if (fs11.existsSync(rename.newAbsPath)) return false;
2305
+ fs11.renameSync(rename.oldAbsPath, rename.newAbsPath);
2306
+ return true;
2307
+ }
2308
+ function deduplicateRenames(renames) {
2309
+ const seen = /* @__PURE__ */ new Set();
2310
+ const result = [];
2311
+ for (const r of renames) {
2312
+ if (seen.has(r.newAbsPath)) continue;
2313
+ seen.add(r.newAbsPath);
2314
+ result.push(r);
1998
2315
  }
1999
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
2000
- if (stack.styling) {
2001
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES3)}`);
2316
+ return result;
2317
+ }
2318
+
2319
+ // src/commands/fix-tests.ts
2320
+ import * as fs12 from "fs";
2321
+ import * as path12 from "path";
2322
+ function generateTestStub(sourceRelPath, config, projectRoot) {
2323
+ const pkg = resolvePackageForFile(sourceRelPath, config);
2324
+ const testPattern = pkg?.structure?.testPattern;
2325
+ if (!testPattern) return null;
2326
+ const basename8 = path12.basename(sourceRelPath);
2327
+ const ext = path12.extname(basename8);
2328
+ if (!ext) return null;
2329
+ const stem = basename8.slice(0, -ext.length);
2330
+ const testSuffix = testPattern.replace("*", "");
2331
+ const testFilename = `${stem}${testSuffix}`;
2332
+ const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
2333
+ const testAbsPath = path12.join(dir, testFilename);
2334
+ if (fs12.existsSync(testAbsPath)) return null;
2335
+ return {
2336
+ path: path12.relative(projectRoot, testAbsPath),
2337
+ absPath: testAbsPath,
2338
+ moduleName: stem
2339
+ };
2340
+ }
2341
+ function writeTestStub(stub, config) {
2342
+ const pkg = resolvePackageForFile(stub.path, config);
2343
+ const testRunner = pkg?.stack?.testRunner ?? "";
2344
+ const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
2345
+ const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
2346
+ const content = `${importLine}describe('${stub.moduleName}', () => {
2347
+ it.todo('add tests');
2348
+ });
2349
+ `;
2350
+ fs12.mkdirSync(path12.dirname(stub.absPath), { recursive: true });
2351
+ fs12.writeFileSync(stub.absPath, content);
2352
+ }
2353
+
2354
+ // src/commands/fix.ts
2355
+ var CONFIG_FILE4 = "viberails.config.json";
2356
+ async function fixCommand(options, cwd) {
2357
+ const startDir = cwd ?? process.cwd();
2358
+ const projectRoot = findProjectRoot(startDir);
2359
+ if (!projectRoot) {
2360
+ console.error(`${chalk7.red("Error:")} No package.json found. Are you in a JS/TS project?`);
2361
+ return 1;
2002
2362
  }
2003
- if (stack.backend) {
2004
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
2363
+ const configPath = path13.join(projectRoot, CONFIG_FILE4);
2364
+ if (!fs13.existsSync(configPath)) {
2365
+ console.error(
2366
+ `${chalk7.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
2367
+ );
2368
+ return 1;
2005
2369
  }
2006
- if (stack.orm) {
2007
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES2)}`);
2370
+ const config = await loadConfig4(configPath);
2371
+ if (!options.dryRun) {
2372
+ const isDirty = checkGitDirty(projectRoot);
2373
+ if (isDirty) {
2374
+ console.log(
2375
+ chalk7.yellow("Warning: You have uncommitted changes. Consider committing first.")
2376
+ );
2377
+ }
2008
2378
  }
2009
- if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
2010
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
2011
- } else {
2012
- if (stack.linter) {
2013
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
2379
+ const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
2380
+ const shouldFixTests = !options.rule || options.rule.includes("missing-test");
2381
+ const allFiles = getAllSourceFiles(projectRoot, config);
2382
+ const renames = [];
2383
+ if (shouldFixNaming) {
2384
+ for (const file of allFiles) {
2385
+ const resolved = resolveConfigForFile(file, config);
2386
+ if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
2387
+ const violation = checkNaming(file, resolved.conventions);
2388
+ if (!violation) continue;
2389
+ const convention = getConventionValue(resolved.conventions.fileNaming);
2390
+ if (!convention) continue;
2391
+ const rename = computeRename(file, convention, projectRoot);
2392
+ if (rename) renames.push(rename);
2014
2393
  }
2015
- if (stack.formatter) {
2016
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
2394
+ }
2395
+ const dedupedRenames = deduplicateRenames(renames);
2396
+ const testStubs = [];
2397
+ if (shouldFixTests) {
2398
+ const testViolations = checkMissingTests(projectRoot, config, "warn");
2399
+ for (const v of testViolations) {
2400
+ const stub = generateTestStub(v.file, config, projectRoot);
2401
+ if (stub) testStubs.push(stub);
2017
2402
  }
2018
2403
  }
2019
- if (stack.testRunner) {
2020
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
2404
+ if (dedupedRenames.length === 0 && testStubs.length === 0) {
2405
+ console.log(`${chalk7.green("\u2713")} No fixable violations found.`);
2406
+ return 0;
2021
2407
  }
2022
- if (stack.packageManager) {
2023
- console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
2408
+ printPlan(dedupedRenames, testStubs);
2409
+ if (options.dryRun) {
2410
+ console.log(chalk7.dim("\nDry run \u2014 no changes applied."));
2411
+ return 0;
2024
2412
  }
2025
- if (stack.libraries.length > 0) {
2026
- for (const lib of stack.libraries) {
2027
- console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES2)}`);
2413
+ if (!options.yes) {
2414
+ const confirmed = await confirmDangerous("Apply these fixes?");
2415
+ if (!confirmed) {
2416
+ console.log("Aborted.");
2417
+ return 0;
2028
2418
  }
2029
2419
  }
2030
- const groups = groupByRole(scanResult.structure.directories);
2031
- if (groups.length > 0) {
2032
- console.log(`
2033
- ${chalk6.bold("Structure:")}`);
2034
- for (const group of groups) {
2035
- console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
2420
+ let renameCount = 0;
2421
+ for (const rename of dedupedRenames) {
2422
+ if (executeRename(rename)) {
2423
+ renameCount++;
2036
2424
  }
2037
2425
  }
2038
- displayConventions(scanResult);
2039
- displaySummarySection(scanResult);
2040
- console.log("");
2041
- }
2042
- function displayRulesPreview(config) {
2043
- const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2044
- console.log(
2045
- `${chalk6.bold("Rules:")} ${chalk6.dim("(warns on violation; use --enforce in CI to block)")}`
2046
- );
2047
- console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
2048
- if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
2426
+ let importUpdateCount = 0;
2427
+ if (renameCount > 0) {
2428
+ const appliedRenames = dedupedRenames.filter((r) => fs13.existsSync(r.newAbsPath));
2429
+ const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
2430
+ importUpdateCount = updates.length;
2431
+ }
2432
+ let stubCount = 0;
2433
+ for (const stub of testStubs) {
2434
+ if (!fs13.existsSync(stub.absPath)) {
2435
+ writeTestStub(stub, config);
2436
+ stubCount++;
2437
+ }
2438
+ }
2439
+ console.log("");
2440
+ if (renameCount > 0) {
2441
+ console.log(`${chalk7.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
2442
+ }
2443
+ if (importUpdateCount > 0) {
2049
2444
  console.log(
2050
- ` ${chalk6.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
2445
+ `${chalk7.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
2051
2446
  );
2052
- } else if (config.rules.testCoverage > 0) {
2053
- console.log(` ${chalk6.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
2054
- } else {
2055
- console.log(` ${chalk6.dim("\u2022")} Test coverage target: disabled`);
2056
2447
  }
2057
- if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
2058
- console.log(` ${chalk6.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
2059
- } else {
2060
- console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
2448
+ if (stubCount > 0) {
2449
+ console.log(`${chalk7.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
2061
2450
  }
2062
- console.log(
2063
- ` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
2064
- );
2065
- console.log("");
2451
+ return 0;
2066
2452
  }
2067
2453
 
2454
+ // src/commands/init.ts
2455
+ import * as fs18 from "fs";
2456
+ import * as path18 from "path";
2457
+ import * as clack8 from "@clack/prompts";
2458
+ import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
2459
+ import { scan as scan2 } from "@viberails/scanner";
2460
+ import chalk11 from "chalk";
2461
+
2068
2462
  // src/utils/check-prerequisites.ts
2069
- import { spawnSync as spawnSync2 } from "child_process";
2070
- import * as fs12 from "fs";
2071
- import * as path12 from "path";
2072
- import * as clack6 from "@clack/prompts";
2073
- import chalk7 from "chalk";
2463
+ import { spawnSync as spawnSync3 } from "child_process";
2464
+ import * as fs14 from "fs";
2465
+ import * as path14 from "path";
2466
+ import * as clack7 from "@clack/prompts";
2467
+ import chalk8 from "chalk";
2074
2468
  function checkCoveragePrereqs(projectRoot, scanResult) {
2075
2469
  const testRunner = scanResult.stack.testRunner;
2076
2470
  if (!testRunner) return [];
@@ -2095,9 +2489,9 @@ function checkCoveragePrereqs(projectRoot, scanResult) {
2095
2489
  function displayMissingPrereqs(prereqs) {
2096
2490
  const missing = prereqs.filter((p) => !p.installed);
2097
2491
  for (const m of missing) {
2098
- console.log(` ${chalk7.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
2492
+ console.log(` ${chalk8.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
2099
2493
  if (m.installCommand) {
2100
- console.log(` Install: ${chalk7.cyan(m.installCommand)}`);
2494
+ console.log(` Install: ${chalk8.cyan(m.installCommand)}`);
2101
2495
  }
2102
2496
  }
2103
2497
  }
@@ -2107,11 +2501,11 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2107
2501
  const prereqLines = prereqs.map(
2108
2502
  (p) => `${p.installed ? "\u2713" : "\u2717"} ${p.label}${p.installed ? "" : ` \u2014 ${p.reason}`}`
2109
2503
  ).join("\n");
2110
- clack6.note(prereqLines, "Coverage prerequisites");
2504
+ clack7.note(prereqLines, "Coverage prerequisites");
2111
2505
  let disableCoverage = false;
2112
2506
  for (const m of missing) {
2113
2507
  if (!m.installCommand) continue;
2114
- const choice = await clack6.select({
2508
+ const choice = await clack7.select({
2115
2509
  message: `${m.label} is not installed. It is required for coverage percentage checks.`,
2116
2510
  options: [
2117
2511
  {
@@ -2133,9 +2527,9 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2133
2527
  });
2134
2528
  assertNotCancelled(choice);
2135
2529
  if (choice === "install") {
2136
- const is = clack6.spinner();
2530
+ const is = clack7.spinner();
2137
2531
  is.start(`Installing ${m.label}...`);
2138
- const result = spawnSync2(m.installCommand, {
2532
+ const result = spawnSync3(m.installCommand, {
2139
2533
  cwd: projectRoot,
2140
2534
  shell: true,
2141
2535
  encoding: "utf-8",
@@ -2145,16 +2539,16 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2145
2539
  is.stop(`Installed ${m.label}`);
2146
2540
  } else {
2147
2541
  is.stop(`Failed to install ${m.label}`);
2148
- clack6.log.warn(
2542
+ clack7.log.warn(
2149
2543
  `Install manually: ${m.installCommand}
2150
2544
  Coverage percentage checks will not work until the dependency is installed.`
2151
2545
  );
2152
2546
  }
2153
2547
  } else if (choice === "disable") {
2154
2548
  disableCoverage = true;
2155
- clack6.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2549
+ clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2156
2550
  } else {
2157
- clack6.log.info(
2551
+ clack7.log.info(
2158
2552
  `Coverage percentage checks will fail until ${m.label} is installed.
2159
2553
  Install later: ${m.installCommand}`
2160
2554
  );
@@ -2164,8 +2558,8 @@ Install later: ${m.installCommand}`
2164
2558
  }
2165
2559
  function hasDependency(projectRoot, name) {
2166
2560
  try {
2167
- const pkgPath = path12.join(projectRoot, "package.json");
2168
- const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2561
+ const pkgPath = path14.join(projectRoot, "package.json");
2562
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2169
2563
  return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2170
2564
  } catch {
2171
2565
  return false;
@@ -2187,84 +2581,58 @@ function filterHighConfidence(conventions, meta) {
2187
2581
  }
2188
2582
 
2189
2583
  // src/utils/update-gitignore.ts
2190
- import * as fs13 from "fs";
2191
- import * as path13 from "path";
2584
+ import * as fs15 from "fs";
2585
+ import * as path15 from "path";
2192
2586
  function updateGitignore(projectRoot) {
2193
- const gitignorePath = path13.join(projectRoot, ".gitignore");
2587
+ const gitignorePath = path15.join(projectRoot, ".gitignore");
2194
2588
  let content = "";
2195
- if (fs13.existsSync(gitignorePath)) {
2196
- content = fs13.readFileSync(gitignorePath, "utf-8");
2589
+ if (fs15.existsSync(gitignorePath)) {
2590
+ content = fs15.readFileSync(gitignorePath, "utf-8");
2197
2591
  }
2198
2592
  if (!content.includes(".viberails/scan-result.json")) {
2199
2593
  const block = "\n# viberails\n.viberails/scan-result.json\n";
2200
2594
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
2201
2595
  `;
2202
- fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
2203
- }
2204
- }
2205
-
2206
- // src/utils/write-generated-files.ts
2207
- import * as fs14 from "fs";
2208
- import * as path14 from "path";
2209
- import { generateContext } from "@viberails/context";
2210
- var CONTEXT_DIR = ".viberails";
2211
- var CONTEXT_FILE = "context.md";
2212
- var SCAN_RESULT_FILE = "scan-result.json";
2213
- function writeGeneratedFiles(projectRoot, config, scanResult) {
2214
- const contextDir = path14.join(projectRoot, CONTEXT_DIR);
2215
- try {
2216
- if (!fs14.existsSync(contextDir)) {
2217
- fs14.mkdirSync(contextDir, { recursive: true });
2218
- }
2219
- const context = generateContext(config);
2220
- fs14.writeFileSync(path14.join(contextDir, CONTEXT_FILE), context);
2221
- fs14.writeFileSync(
2222
- path14.join(contextDir, SCAN_RESULT_FILE),
2223
- `${JSON.stringify(scanResult, null, 2)}
2224
- `
2225
- );
2226
- } catch (err) {
2227
- const message = err instanceof Error ? err.message : String(err);
2228
- throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
2596
+ fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
2229
2597
  }
2230
2598
  }
2231
2599
 
2232
2600
  // src/commands/init-hooks.ts
2233
- import * as fs15 from "fs";
2234
- import * as path15 from "path";
2235
- import chalk8 from "chalk";
2601
+ import * as fs16 from "fs";
2602
+ import * as path16 from "path";
2603
+ import chalk9 from "chalk";
2236
2604
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2237
2605
  function setupPreCommitHook(projectRoot) {
2238
- const lefthookPath = path15.join(projectRoot, "lefthook.yml");
2239
- if (fs15.existsSync(lefthookPath)) {
2606
+ const lefthookPath = path16.join(projectRoot, "lefthook.yml");
2607
+ if (fs16.existsSync(lefthookPath)) {
2240
2608
  addLefthookPreCommit(lefthookPath);
2241
- console.log(` ${chalk8.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2609
+ console.log(` ${chalk9.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2242
2610
  return "lefthook.yml";
2243
2611
  }
2244
- const huskyDir = path15.join(projectRoot, ".husky");
2245
- if (fs15.existsSync(huskyDir)) {
2612
+ const huskyDir = path16.join(projectRoot, ".husky");
2613
+ if (fs16.existsSync(huskyDir)) {
2246
2614
  writeHuskyPreCommit(huskyDir);
2247
- console.log(` ${chalk8.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2615
+ console.log(` ${chalk9.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2248
2616
  return ".husky/pre-commit";
2249
2617
  }
2250
- const gitDir = path15.join(projectRoot, ".git");
2251
- if (fs15.existsSync(gitDir)) {
2252
- const hooksDir = path15.join(gitDir, "hooks");
2253
- if (!fs15.existsSync(hooksDir)) {
2254
- fs15.mkdirSync(hooksDir, { recursive: true });
2618
+ const gitDir = path16.join(projectRoot, ".git");
2619
+ if (fs16.existsSync(gitDir)) {
2620
+ const hooksDir = path16.join(gitDir, "hooks");
2621
+ if (!fs16.existsSync(hooksDir)) {
2622
+ fs16.mkdirSync(hooksDir, { recursive: true });
2255
2623
  }
2256
2624
  writeGitHookPreCommit(hooksDir);
2257
- console.log(` ${chalk8.green("\u2713")} .git/hooks/pre-commit`);
2625
+ console.log(` ${chalk9.green("\u2713")} .git/hooks/pre-commit`);
2258
2626
  return ".git/hooks/pre-commit";
2259
2627
  }
2260
2628
  return void 0;
2261
2629
  }
2262
2630
  function writeGitHookPreCommit(hooksDir) {
2263
- const hookPath = path15.join(hooksDir, "pre-commit");
2264
- if (fs15.existsSync(hookPath)) {
2265
- const existing = fs15.readFileSync(hookPath, "utf-8");
2631
+ const hookPath = path16.join(hooksDir, "pre-commit");
2632
+ if (fs16.existsSync(hookPath)) {
2633
+ const existing = fs16.readFileSync(hookPath, "utf-8");
2266
2634
  if (existing.includes("viberails")) return;
2267
- fs15.writeFileSync(
2635
+ fs16.writeFileSync(
2268
2636
  hookPath,
2269
2637
  `${existing.trimEnd()}
2270
2638
 
@@ -2281,10 +2649,10 @@ npx viberails check --staged
2281
2649
  "npx viberails check --staged",
2282
2650
  ""
2283
2651
  ].join("\n");
2284
- fs15.writeFileSync(hookPath, script, { mode: 493 });
2652
+ fs16.writeFileSync(hookPath, script, { mode: 493 });
2285
2653
  }
2286
2654
  function addLefthookPreCommit(lefthookPath) {
2287
- const content = fs15.readFileSync(lefthookPath, "utf-8");
2655
+ const content = fs16.readFileSync(lefthookPath, "utf-8");
2288
2656
  if (content.includes("viberails")) return;
2289
2657
  const doc = parseYaml(content) ?? {};
2290
2658
  if (!doc["pre-commit"]) {
@@ -2296,29 +2664,29 @@ function addLefthookPreCommit(lefthookPath) {
2296
2664
  doc["pre-commit"].commands.viberails = {
2297
2665
  run: "npx viberails check --staged"
2298
2666
  };
2299
- fs15.writeFileSync(lefthookPath, stringifyYaml(doc));
2667
+ fs16.writeFileSync(lefthookPath, stringifyYaml(doc));
2300
2668
  }
2301
2669
  function detectHookManager(projectRoot) {
2302
- if (fs15.existsSync(path15.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2303
- if (fs15.existsSync(path15.join(projectRoot, ".husky"))) return "Husky";
2304
- if (fs15.existsSync(path15.join(projectRoot, ".git"))) return "git hook";
2670
+ if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2671
+ if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
2672
+ if (fs16.existsSync(path16.join(projectRoot, ".git"))) return "git hook";
2305
2673
  return void 0;
2306
2674
  }
2307
2675
  function setupClaudeCodeHook(projectRoot) {
2308
- const claudeDir = path15.join(projectRoot, ".claude");
2309
- if (!fs15.existsSync(claudeDir)) {
2310
- fs15.mkdirSync(claudeDir, { recursive: true });
2676
+ const claudeDir = path16.join(projectRoot, ".claude");
2677
+ if (!fs16.existsSync(claudeDir)) {
2678
+ fs16.mkdirSync(claudeDir, { recursive: true });
2311
2679
  }
2312
- const settingsPath = path15.join(claudeDir, "settings.json");
2680
+ const settingsPath = path16.join(claudeDir, "settings.json");
2313
2681
  let settings = {};
2314
- if (fs15.existsSync(settingsPath)) {
2682
+ if (fs16.existsSync(settingsPath)) {
2315
2683
  try {
2316
- settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
2684
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
2317
2685
  } catch {
2318
2686
  console.warn(
2319
- ` ${chalk8.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2687
+ ` ${chalk9.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2320
2688
  );
2321
- console.warn(` Fix the JSON manually, then re-run ${chalk8.cyan("viberails init --force")}`);
2689
+ console.warn(` Fix the JSON manually, then re-run ${chalk9.cyan("viberails init --force")}`);
2322
2690
  return;
2323
2691
  }
2324
2692
  }
@@ -2339,30 +2707,30 @@ function setupClaudeCodeHook(projectRoot) {
2339
2707
  }
2340
2708
  ];
2341
2709
  settings.hooks = hooks;
2342
- fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2710
+ fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2343
2711
  `);
2344
- console.log(` ${chalk8.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2712
+ console.log(` ${chalk9.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2345
2713
  }
2346
2714
  function setupClaudeMdReference(projectRoot) {
2347
- const claudeMdPath = path15.join(projectRoot, "CLAUDE.md");
2715
+ const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
2348
2716
  let content = "";
2349
- if (fs15.existsSync(claudeMdPath)) {
2350
- content = fs15.readFileSync(claudeMdPath, "utf-8");
2717
+ if (fs16.existsSync(claudeMdPath)) {
2718
+ content = fs16.readFileSync(claudeMdPath, "utf-8");
2351
2719
  }
2352
2720
  if (content.includes("@.viberails/context.md")) return;
2353
2721
  const ref = "\n@.viberails/context.md\n";
2354
2722
  const prefix = content.length === 0 ? "" : content.trimEnd();
2355
- fs15.writeFileSync(claudeMdPath, prefix + ref);
2356
- console.log(` ${chalk8.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2723
+ fs16.writeFileSync(claudeMdPath, prefix + ref);
2724
+ console.log(` ${chalk9.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2357
2725
  }
2358
2726
  function setupGithubAction(projectRoot, packageManager) {
2359
- const workflowDir = path15.join(projectRoot, ".github", "workflows");
2360
- const workflowPath = path15.join(workflowDir, "viberails.yml");
2361
- if (fs15.existsSync(workflowPath)) {
2362
- const existing = fs15.readFileSync(workflowPath, "utf-8");
2727
+ const workflowDir = path16.join(projectRoot, ".github", "workflows");
2728
+ const workflowPath = path16.join(workflowDir, "viberails.yml");
2729
+ if (fs16.existsSync(workflowPath)) {
2730
+ const existing = fs16.readFileSync(workflowPath, "utf-8");
2363
2731
  if (existing.includes("viberails")) return void 0;
2364
2732
  }
2365
- fs15.mkdirSync(workflowDir, { recursive: true });
2733
+ fs16.mkdirSync(workflowDir, { recursive: true });
2366
2734
  const pm = packageManager || "npm";
2367
2735
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
2368
2736
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -2396,71 +2764,71 @@ function setupGithubAction(projectRoot, packageManager) {
2396
2764
  ""
2397
2765
  );
2398
2766
  const content = lines.filter((l) => l !== void 0).join("\n");
2399
- fs15.writeFileSync(workflowPath, content);
2767
+ fs16.writeFileSync(workflowPath, content);
2400
2768
  return ".github/workflows/viberails.yml";
2401
2769
  }
2402
2770
  function writeHuskyPreCommit(huskyDir) {
2403
- const hookPath = path15.join(huskyDir, "pre-commit");
2404
- if (fs15.existsSync(hookPath)) {
2405
- const existing = fs15.readFileSync(hookPath, "utf-8");
2771
+ const hookPath = path16.join(huskyDir, "pre-commit");
2772
+ if (fs16.existsSync(hookPath)) {
2773
+ const existing = fs16.readFileSync(hookPath, "utf-8");
2406
2774
  if (!existing.includes("viberails")) {
2407
- fs15.writeFileSync(hookPath, `${existing.trimEnd()}
2775
+ fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2408
2776
  npx viberails check --staged
2409
2777
  `);
2410
2778
  }
2411
2779
  return;
2412
2780
  }
2413
- fs15.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
2781
+ fs16.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
2414
2782
  }
2415
2783
 
2416
2784
  // src/commands/init-hooks-extra.ts
2417
- import * as fs16 from "fs";
2418
- import * as path16 from "path";
2419
- import chalk9 from "chalk";
2785
+ import * as fs17 from "fs";
2786
+ import * as path17 from "path";
2787
+ import chalk10 from "chalk";
2420
2788
  import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
2421
2789
  function addPreCommitStep(projectRoot, name, command, marker) {
2422
- const lefthookPath = path16.join(projectRoot, "lefthook.yml");
2423
- if (fs16.existsSync(lefthookPath)) {
2424
- const content = fs16.readFileSync(lefthookPath, "utf-8");
2790
+ const lefthookPath = path17.join(projectRoot, "lefthook.yml");
2791
+ if (fs17.existsSync(lefthookPath)) {
2792
+ const content = fs17.readFileSync(lefthookPath, "utf-8");
2425
2793
  if (content.includes(marker)) return void 0;
2426
2794
  const doc = parseYaml2(content) ?? {};
2427
2795
  if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
2428
2796
  if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
2429
2797
  doc["pre-commit"].commands[name] = { run: command };
2430
- fs16.writeFileSync(lefthookPath, stringifyYaml2(doc));
2798
+ fs17.writeFileSync(lefthookPath, stringifyYaml2(doc));
2431
2799
  return "lefthook.yml";
2432
2800
  }
2433
- const huskyDir = path16.join(projectRoot, ".husky");
2434
- if (fs16.existsSync(huskyDir)) {
2435
- const hookPath = path16.join(huskyDir, "pre-commit");
2436
- if (fs16.existsSync(hookPath)) {
2437
- const existing = fs16.readFileSync(hookPath, "utf-8");
2801
+ const huskyDir = path17.join(projectRoot, ".husky");
2802
+ if (fs17.existsSync(huskyDir)) {
2803
+ const hookPath = path17.join(huskyDir, "pre-commit");
2804
+ if (fs17.existsSync(hookPath)) {
2805
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2438
2806
  if (existing.includes(marker)) return void 0;
2439
- fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2807
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2440
2808
  ${command}
2441
2809
  `);
2442
2810
  } else {
2443
- fs16.writeFileSync(hookPath, `#!/bin/sh
2811
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2444
2812
  ${command}
2445
2813
  `, { mode: 493 });
2446
2814
  }
2447
2815
  return ".husky/pre-commit";
2448
2816
  }
2449
- const gitDir = path16.join(projectRoot, ".git");
2450
- if (fs16.existsSync(gitDir)) {
2451
- const hooksDir = path16.join(gitDir, "hooks");
2452
- if (!fs16.existsSync(hooksDir)) fs16.mkdirSync(hooksDir, { recursive: true });
2453
- const hookPath = path16.join(hooksDir, "pre-commit");
2454
- if (fs16.existsSync(hookPath)) {
2455
- const existing = fs16.readFileSync(hookPath, "utf-8");
2817
+ const gitDir = path17.join(projectRoot, ".git");
2818
+ if (fs17.existsSync(gitDir)) {
2819
+ const hooksDir = path17.join(gitDir, "hooks");
2820
+ if (!fs17.existsSync(hooksDir)) fs17.mkdirSync(hooksDir, { recursive: true });
2821
+ const hookPath = path17.join(hooksDir, "pre-commit");
2822
+ if (fs17.existsSync(hookPath)) {
2823
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2456
2824
  if (existing.includes(marker)) return void 0;
2457
- fs16.writeFileSync(hookPath, `${existing.trimEnd()}
2825
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2458
2826
 
2459
2827
  # ${name}
2460
2828
  ${command}
2461
2829
  `);
2462
2830
  } else {
2463
- fs16.writeFileSync(hookPath, `#!/bin/sh
2831
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2464
2832
  # Generated by viberails
2465
2833
 
2466
2834
  # ${name}
@@ -2476,7 +2844,7 @@ ${command}
2476
2844
  function setupTypecheckHook(projectRoot) {
2477
2845
  const target = addPreCommitStep(projectRoot, "typecheck", "npx tsc --noEmit", "tsc");
2478
2846
  if (target) {
2479
- console.log(` ${chalk9.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
2847
+ console.log(` ${chalk10.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
2480
2848
  }
2481
2849
  return target;
2482
2850
  }
@@ -2485,7 +2853,7 @@ function setupLintHook(projectRoot, linter) {
2485
2853
  const linterName = linter === "biome" ? "Biome" : "ESLint";
2486
2854
  const target = addPreCommitStep(projectRoot, "lint", command, linter);
2487
2855
  if (target) {
2488
- console.log(` ${chalk9.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2856
+ console.log(` ${chalk10.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2489
2857
  }
2490
2858
  return target;
2491
2859
  }
@@ -2519,7 +2887,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
2519
2887
  }
2520
2888
 
2521
2889
  // src/commands/init.ts
2522
- var CONFIG_FILE4 = "viberails.config.json";
2890
+ var CONFIG_FILE5 = "viberails.config.json";
2523
2891
  function getExemptedPackages(config) {
2524
2892
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
2525
2893
  }
@@ -2530,11 +2898,11 @@ async function initCommand(options, cwd) {
2530
2898
  "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
2531
2899
  );
2532
2900
  }
2533
- const configPath = path17.join(projectRoot, CONFIG_FILE4);
2534
- if (fs17.existsSync(configPath) && !options.force) {
2901
+ const configPath = path18.join(projectRoot, CONFIG_FILE5);
2902
+ if (fs18.existsSync(configPath) && !options.force) {
2535
2903
  console.log(
2536
- `${chalk10.yellow("!")} viberails is already initialized.
2537
- Run ${chalk10.cyan("viberails sync")} to update, or ${chalk10.cyan("viberails init --force")} to start fresh.`
2904
+ `${chalk11.yellow("!")} viberails is already initialized.
2905
+ Run ${chalk11.cyan("viberails config")} to edit rules, ${chalk11.cyan("viberails sync")} to update, or ${chalk11.cyan("viberails init --force")} to start fresh.`
2538
2906
  );
2539
2907
  return;
2540
2908
  }
@@ -2542,8 +2910,8 @@ async function initCommand(options, cwd) {
2542
2910
  await initInteractive(projectRoot, configPath, options);
2543
2911
  }
2544
2912
  async function initNonInteractive(projectRoot, configPath) {
2545
- console.log(chalk10.dim("Scanning project..."));
2546
- const scanResult = await scan(projectRoot);
2913
+ console.log(chalk11.dim("Scanning project..."));
2914
+ const scanResult = await scan2(projectRoot);
2547
2915
  const config = generateConfig(scanResult);
2548
2916
  for (const pkg of config.packages) {
2549
2917
  const pkgMeta = config._meta?.packages?.[pkg.path]?.conventions;
@@ -2555,11 +2923,11 @@ async function initNonInteractive(projectRoot, configPath) {
2555
2923
  const exempted = getExemptedPackages(config);
2556
2924
  if (exempted.length > 0) {
2557
2925
  console.log(
2558
- ` ${chalk10.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk10.dim("(types-only)")}`
2926
+ ` ${chalk11.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk11.dim("(types-only)")}`
2559
2927
  );
2560
2928
  }
2561
2929
  if (config.packages.length > 1) {
2562
- console.log(chalk10.dim("Building import graph..."));
2930
+ console.log(chalk11.dim("Building import graph..."));
2563
2931
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2564
2932
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
2565
2933
  const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
@@ -2571,50 +2939,50 @@ async function initNonInteractive(projectRoot, configPath) {
2571
2939
  console.log(` Inferred ${denyCount} boundary rules`);
2572
2940
  }
2573
2941
  }
2574
- const compacted = compactConfig2(config);
2575
- fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2942
+ const compacted = compactConfig3(config);
2943
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2576
2944
  `);
2577
2945
  writeGeneratedFiles(projectRoot, config, scanResult);
2578
2946
  updateGitignore(projectRoot);
2579
2947
  setupClaudeCodeHook(projectRoot);
2580
2948
  setupClaudeMdReference(projectRoot);
2581
- const preCommitTarget = setupPreCommitHook(projectRoot);
2582
2949
  const rootPkg = config.packages[0];
2583
2950
  const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
2584
2951
  const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
2585
- const typecheckTarget = rootPkg?.stack?.language === "typescript" ? setupTypecheckHook(projectRoot) : void 0;
2952
+ const hookManager = detectHookManager(projectRoot);
2953
+ const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
2954
+ const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
2586
2955
  const linter = rootPkg?.stack?.linter?.split("@")[0];
2587
- const lintTarget = linter ? setupLintHook(projectRoot, linter) : void 0;
2588
- const ok = chalk10.green("\u2713");
2956
+ const ok = chalk11.green("\u2713");
2589
2957
  const created = [
2590
- `${ok} ${path17.basename(configPath)}`,
2958
+ `${ok} ${path18.basename(configPath)}`,
2591
2959
  `${ok} .viberails/context.md`,
2592
2960
  `${ok} .viberails/scan-result.json`,
2593
2961
  `${ok} .claude/settings.json \u2014 added viberails hook`,
2594
2962
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
2595
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk10.yellow("!")} pre-commit hook skipped`,
2596
- typecheckTarget ? `${ok} ${typecheckTarget} \u2014 added typecheck` : "",
2597
- lintTarget ? `${ok} ${lintTarget} \u2014 added lint check` : "",
2963
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk11.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
2598
2964
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
2599
2965
  ].filter(Boolean);
2966
+ if (hasHookManager && rootPkg?.stack?.language === "typescript") setupTypecheckHook(projectRoot);
2967
+ if (hasHookManager && linter) setupLintHook(projectRoot, linter);
2600
2968
  console.log(`
2601
2969
  Created:
2602
2970
  ${created.map((f) => ` ${f}`).join("\n")}`);
2603
2971
  }
2604
2972
  async function initInteractive(projectRoot, configPath, options) {
2605
- clack7.intro("viberails");
2606
- if (fs17.existsSync(configPath) && options.force) {
2973
+ clack8.intro("viberails");
2974
+ if (fs18.existsSync(configPath) && options.force) {
2607
2975
  const replace = await confirmDangerous(
2608
- `${path17.basename(configPath)} already exists and will be replaced. Continue?`
2976
+ `${path18.basename(configPath)} already exists and will be replaced. Continue?`
2609
2977
  );
2610
2978
  if (!replace) {
2611
- clack7.outro("Aborted. No files were written.");
2979
+ clack8.outro("Aborted. No files were written.");
2612
2980
  return;
2613
2981
  }
2614
2982
  }
2615
- const s = clack7.spinner();
2983
+ const s = clack8.spinner();
2616
2984
  s.start("Scanning project...");
2617
- const scanResult = await scan(projectRoot);
2985
+ const scanResult = await scan2(projectRoot);
2618
2986
  const config = generateConfig(scanResult);
2619
2987
  s.stop("Scan complete");
2620
2988
  const prereqResult = await promptMissingPrereqs(
@@ -2625,16 +2993,13 @@ async function initInteractive(projectRoot, configPath, options) {
2625
2993
  config.rules.testCoverage = 0;
2626
2994
  }
2627
2995
  if (scanResult.statistics.totalFiles === 0) {
2628
- clack7.log.warn(
2996
+ clack8.log.warn(
2629
2997
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
2630
2998
  );
2631
2999
  }
2632
- clack7.note(formatScanResultsText(scanResult), "Scan results");
2633
- const rulesLines = formatRulesText(config);
3000
+ clack8.note(formatScanResultsText(scanResult), "Scan results");
2634
3001
  const exemptedPkgs = getExemptedPackages(config);
2635
- if (exemptedPkgs.length > 0)
2636
- rulesLines.push(`Auto-exempted from coverage: ${exemptedPkgs.join(", ")} (types-only)`);
2637
- clack7.note(rulesLines.join("\n"), "Rules");
3002
+ displayInitSummary(config, exemptedPkgs);
2638
3003
  const decision = await promptInitDecision();
2639
3004
  if (decision === "customize") {
2640
3005
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
@@ -2648,41 +3013,16 @@ async function initInteractive(projectRoot, configPath, options) {
2648
3013
  coverageCommand: config.defaults?.coverage?.command,
2649
3014
  packageOverrides: config.packages
2650
3015
  });
2651
- if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
2652
- config.rules.maxFileLines = overrides.maxFileLines;
2653
- config.rules.testCoverage = overrides.testCoverage;
2654
- config.rules.enforceMissingTests = overrides.enforceMissingTests;
2655
- config.rules.enforceNaming = overrides.enforceNaming;
2656
- for (const pkg of config.packages) {
2657
- pkg.coverage = pkg.coverage ?? {};
2658
- if (pkg.coverage.summaryPath === void 0) {
2659
- pkg.coverage.summaryPath = overrides.coverageSummaryPath;
2660
- }
2661
- if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
2662
- pkg.coverage.command = overrides.coverageCommand;
2663
- }
2664
- }
2665
- if (overrides.fileNamingValue) {
2666
- const oldNaming = rootPkg.conventions?.fileNaming;
2667
- rootPkg.conventions = rootPkg.conventions ?? {};
2668
- rootPkg.conventions.fileNaming = overrides.fileNamingValue;
2669
- if (oldNaming && oldNaming !== overrides.fileNamingValue) {
2670
- for (const pkg of config.packages) {
2671
- if (pkg.conventions?.fileNaming === oldNaming) {
2672
- pkg.conventions.fileNaming = overrides.fileNamingValue;
2673
- }
2674
- }
2675
- }
2676
- }
3016
+ applyRuleOverrides(config, overrides);
2677
3017
  }
2678
3018
  if (config.packages.length > 1) {
2679
- clack7.note(
3019
+ clack8.note(
2680
3020
  "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
2681
3021
  "Boundaries"
2682
3022
  );
2683
3023
  const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
2684
3024
  if (shouldInfer) {
2685
- const bs = clack7.spinner();
3025
+ const bs = clack8.spinner();
2686
3026
  bs.start("Building import graph...");
2687
3027
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2688
3028
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -2694,7 +3034,7 @@ async function initInteractive(projectRoot, configPath, options) {
2694
3034
  config.rules.enforceBoundaries = true;
2695
3035
  bs.stop(`Inferred ${denyCount} boundary rules`);
2696
3036
  const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
2697
- clack7.note(boundaryLines, "Boundary rules");
3037
+ clack8.note(boundaryLines, "Boundary rules");
2698
3038
  } else {
2699
3039
  bs.stop("No boundary rules inferred");
2700
3040
  }
@@ -2702,22 +3042,23 @@ async function initInteractive(projectRoot, configPath, options) {
2702
3042
  }
2703
3043
  const hookManager = detectHookManager(projectRoot);
2704
3044
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
2705
- const integrations = await promptIntegrations(hookManager, {
3045
+ const integrations = await promptIntegrations(projectRoot, hookManager, {
2706
3046
  isTypeScript: rootPkgStack?.language === "typescript",
2707
- linter: rootPkgStack?.linter?.split("@")[0]
3047
+ linter: rootPkgStack?.linter?.split("@")[0],
3048
+ packageManager: rootPkgStack?.packageManager
2708
3049
  });
2709
3050
  const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
2710
3051
  if (!shouldWrite) {
2711
- clack7.outro("Aborted. No files were written.");
3052
+ clack8.outro("Aborted. No files were written.");
2712
3053
  return;
2713
3054
  }
2714
- const compacted = compactConfig2(config);
2715
- fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3055
+ const compacted = compactConfig3(config);
3056
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2716
3057
  `);
2717
3058
  writeGeneratedFiles(projectRoot, config, scanResult);
2718
3059
  updateGitignore(projectRoot);
2719
3060
  const createdFiles = [
2720
- path17.basename(configPath),
3061
+ path18.basename(configPath),
2721
3062
  ".viberails/context.md",
2722
3063
  ".viberails/scan-result.json",
2723
3064
  ...setupSelectedIntegrations(projectRoot, integrations, {
@@ -2725,155 +3066,27 @@ async function initInteractive(projectRoot, configPath, options) {
2725
3066
  packageManager: rootPkgStack?.packageManager
2726
3067
  })
2727
3068
  ];
2728
- clack7.log.success(`Created:
3069
+ clack8.log.success(`Created:
2729
3070
  ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
2730
- clack7.outro(
3071
+ clack8.outro(
2731
3072
  `Done! Next: review viberails.config.json, then run viberails check
2732
- ${chalk10.dim("Tip: use")} ${chalk10.cyan("viberails check --enforce")} ${chalk10.dim("in CI to block PRs on violations.")}`
3073
+ ${chalk11.dim("Tip: use")} ${chalk11.cyan("viberails check --enforce")} ${chalk11.dim("in CI to block PRs on violations.")}`
2733
3074
  );
2734
3075
  }
2735
3076
 
2736
3077
  // src/commands/sync.ts
2737
- import * as fs18 from "fs";
2738
- import * as path18 from "path";
2739
- import { compactConfig as compactConfig3, loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
2740
- import { scan as scan2 } from "@viberails/scanner";
2741
- import chalk11 from "chalk";
2742
-
2743
- // src/utils/diff-configs.ts
2744
- import { CONVENTION_LABELS as CONVENTION_LABELS3, FRAMEWORK_NAMES as FRAMEWORK_NAMES4, ORM_NAMES as ORM_NAMES3, STYLING_NAMES as STYLING_NAMES4 } from "@viberails/types";
2745
- function parseStackString(s) {
2746
- const atIdx = s.indexOf("@");
2747
- if (atIdx > 0) {
2748
- return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
2749
- }
2750
- return { name: s };
2751
- }
2752
- function displayStackName(s) {
2753
- const { name, version } = parseStackString(s);
2754
- const allMaps = {
2755
- ...FRAMEWORK_NAMES4,
2756
- ...STYLING_NAMES4,
2757
- ...ORM_NAMES3
2758
- };
2759
- const display = allMaps[name] ?? name;
2760
- return version ? `${display} ${version}` : display;
2761
- }
2762
- function isNewlyDetected(config, pkgPath, key) {
2763
- return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
2764
- }
2765
- var STACK_FIELDS = [
2766
- "framework",
2767
- "styling",
2768
- "backend",
2769
- "orm",
2770
- "linter",
2771
- "formatter",
2772
- "testRunner"
2773
- ];
2774
- var CONVENTION_KEYS = [
2775
- "fileNaming",
2776
- "componentNaming",
2777
- "hookNaming",
2778
- "importAlias"
2779
- ];
2780
- var STRUCTURE_FIELDS = [
2781
- { key: "srcDir", label: "source directory" },
2782
- { key: "pages", label: "pages directory" },
2783
- { key: "components", label: "components directory" },
2784
- { key: "hooks", label: "hooks directory" },
2785
- { key: "utils", label: "utilities directory" },
2786
- { key: "types", label: "types directory" },
2787
- { key: "tests", label: "tests directory" },
2788
- { key: "testPattern", label: "test pattern" }
2789
- ];
2790
- function diffPackage(existing, merged, mergedConfig) {
2791
- const changes = [];
2792
- const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
2793
- for (const field of STACK_FIELDS) {
2794
- const oldVal = existing.stack?.[field];
2795
- const newVal = merged.stack?.[field];
2796
- if (!oldVal && newVal) {
2797
- changes.push({
2798
- type: "added",
2799
- description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
2800
- });
2801
- } else if (oldVal && newVal && oldVal !== newVal) {
2802
- changes.push({
2803
- type: "changed",
2804
- description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
2805
- });
2806
- }
2807
- }
2808
- for (const key of CONVENTION_KEYS) {
2809
- const oldVal = existing.conventions?.[key];
2810
- const newVal = merged.conventions?.[key];
2811
- const label = CONVENTION_LABELS3[key] ?? key;
2812
- if (!oldVal && newVal) {
2813
- changes.push({
2814
- type: "added",
2815
- description: `${pkgPrefix}New convention: ${label} (${newVal})`
2816
- });
2817
- } else if (oldVal && newVal && oldVal !== newVal) {
2818
- const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
2819
- changes.push({
2820
- type: "changed",
2821
- description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
2822
- });
2823
- }
2824
- }
2825
- for (const { key, label } of STRUCTURE_FIELDS) {
2826
- const oldVal = existing.structure?.[key];
2827
- const newVal = merged.structure?.[key];
2828
- if (!oldVal && newVal) {
2829
- changes.push({
2830
- type: "added",
2831
- description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
2832
- });
2833
- }
2834
- }
2835
- return changes;
2836
- }
2837
- function diffConfigs(existing, merged) {
2838
- const changes = [];
2839
- const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
2840
- const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
2841
- for (const existingPkg of existing.packages) {
2842
- const mergedPkg = mergedByPath.get(existingPkg.path);
2843
- if (mergedPkg) {
2844
- changes.push(...diffPackage(existingPkg, mergedPkg, merged));
2845
- }
2846
- }
2847
- for (const mergedPkg of merged.packages) {
2848
- if (!existingByPath.has(mergedPkg.path)) {
2849
- changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
2850
- }
2851
- }
2852
- return changes;
2853
- }
2854
- function formatStatsDelta(oldStats, newStats) {
2855
- const fileDelta = newStats.totalFiles - oldStats.totalFiles;
2856
- const lineDelta = newStats.totalLines - oldStats.totalLines;
2857
- if (fileDelta === 0 && lineDelta === 0) return void 0;
2858
- const parts = [];
2859
- if (fileDelta !== 0) {
2860
- const sign = fileDelta > 0 ? "+" : "";
2861
- parts.push(`${sign}${fileDelta.toLocaleString()} files`);
2862
- }
2863
- if (lineDelta !== 0) {
2864
- const sign = lineDelta > 0 ? "+" : "";
2865
- parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
2866
- }
2867
- return `${parts.join(", ")} since last sync`;
2868
- }
2869
-
2870
- // src/commands/sync.ts
2871
- var CONFIG_FILE5 = "viberails.config.json";
3078
+ import * as fs19 from "fs";
3079
+ import * as path19 from "path";
3080
+ import * as clack9 from "@clack/prompts";
3081
+ import { compactConfig as compactConfig4, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
3082
+ import { scan as scan3 } from "@viberails/scanner";
3083
+ import chalk12 from "chalk";
3084
+ var CONFIG_FILE6 = "viberails.config.json";
2872
3085
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
2873
3086
  function loadPreviousStats(projectRoot) {
2874
- const scanResultPath = path18.join(projectRoot, SCAN_RESULT_FILE2);
3087
+ const scanResultPath = path19.join(projectRoot, SCAN_RESULT_FILE2);
2875
3088
  try {
2876
- const raw = fs18.readFileSync(scanResultPath, "utf-8");
3089
+ const raw = fs19.readFileSync(scanResultPath, "utf-8");
2877
3090
  const parsed = JSON.parse(raw);
2878
3091
  if (parsed?.statistics?.totalFiles !== void 0) {
2879
3092
  return parsed.statistics;
@@ -2882,7 +3095,7 @@ function loadPreviousStats(projectRoot) {
2882
3095
  }
2883
3096
  return void 0;
2884
3097
  }
2885
- async function syncCommand(cwd) {
3098
+ async function syncCommand(options, cwd) {
2886
3099
  const startDir = cwd ?? process.cwd();
2887
3100
  const projectRoot = findProjectRoot(startDir);
2888
3101
  if (!projectRoot) {
@@ -2890,15 +3103,15 @@ async function syncCommand(cwd) {
2890
3103
  "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"
2891
3104
  );
2892
3105
  }
2893
- const configPath = path18.join(projectRoot, CONFIG_FILE5);
2894
- const existing = await loadConfig4(configPath);
3106
+ const configPath = path19.join(projectRoot, CONFIG_FILE6);
3107
+ const existing = await loadConfig5(configPath);
2895
3108
  const previousStats = loadPreviousStats(projectRoot);
2896
- console.log(chalk11.dim("Scanning project..."));
2897
- const scanResult = await scan2(projectRoot);
2898
- const merged = mergeConfig(existing, scanResult);
2899
- const compacted = compactConfig3(merged);
3109
+ console.log(chalk12.dim("Scanning project..."));
3110
+ const scanResult = await scan3(projectRoot);
3111
+ const merged = mergeConfig2(existing, scanResult);
3112
+ const compacted = compactConfig4(merged);
2900
3113
  const compactedJson = JSON.stringify(compacted, null, 2);
2901
- const rawDisk = fs18.readFileSync(configPath, "utf-8").trim();
3114
+ const rawDisk = fs19.readFileSync(configPath, "utf-8").trim();
2902
3115
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
2903
3116
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
2904
3117
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -2906,31 +3119,69 @@ async function syncCommand(cwd) {
2906
3119
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
2907
3120
  if (changes.length > 0 || statsDelta) {
2908
3121
  console.log(`
2909
- ${chalk11.bold("Changes:")}`);
3122
+ ${chalk12.bold("Changes:")}`);
2910
3123
  for (const change of changes) {
2911
- const icon = change.type === "removed" ? chalk11.red("-") : chalk11.green("+");
3124
+ const icon = change.type === "removed" ? chalk12.red("-") : chalk12.green("+");
2912
3125
  console.log(` ${icon} ${change.description}`);
2913
3126
  }
2914
3127
  if (statsDelta) {
2915
- console.log(` ${chalk11.dim(statsDelta)}`);
3128
+ console.log(` ${chalk12.dim(statsDelta)}`);
2916
3129
  }
2917
3130
  }
2918
- fs18.writeFileSync(configPath, `${compactedJson}
3131
+ if (options?.interactive) {
3132
+ clack9.intro("viberails sync (interactive)");
3133
+ clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
3134
+ const decision = await clack9.select({
3135
+ message: "How would you like to proceed?",
3136
+ options: [
3137
+ { value: "accept", label: "Accept changes" },
3138
+ { value: "customize", label: "Customize rules" },
3139
+ { value: "cancel", label: "Cancel (no changes written)" }
3140
+ ]
3141
+ });
3142
+ assertNotCancelled(decision);
3143
+ if (decision === "cancel") {
3144
+ clack9.outro("Sync cancelled. No files were written.");
3145
+ return;
3146
+ }
3147
+ if (decision === "customize") {
3148
+ const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
3149
+ const overrides = await promptRuleMenu({
3150
+ maxFileLines: merged.rules.maxFileLines,
3151
+ testCoverage: merged.rules.testCoverage,
3152
+ enforceMissingTests: merged.rules.enforceMissingTests,
3153
+ enforceNaming: merged.rules.enforceNaming,
3154
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3155
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3156
+ coverageCommand: merged.defaults?.coverage?.command,
3157
+ packageOverrides: merged.packages
3158
+ });
3159
+ applyRuleOverrides(merged, overrides);
3160
+ const recompacted = compactConfig4(merged);
3161
+ fs19.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3162
+ `);
3163
+ writeGeneratedFiles(projectRoot, merged, scanResult);
3164
+ clack9.log.success("Updated config with your customizations.");
3165
+ clack9.outro("Done! Run viberails check to verify.");
3166
+ return;
3167
+ }
3168
+ }
3169
+ fs19.writeFileSync(configPath, `${compactedJson}
2919
3170
  `);
2920
3171
  writeGeneratedFiles(projectRoot, merged, scanResult);
2921
3172
  console.log(`
2922
- ${chalk11.bold("Synced:")}`);
3173
+ ${chalk12.bold("Synced:")}`);
2923
3174
  if (configChanged) {
2924
- console.log(` ${chalk11.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
3175
+ console.log(` ${chalk12.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
2925
3176
  } else {
2926
- console.log(` ${chalk11.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
3177
+ console.log(` ${chalk12.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
2927
3178
  }
2928
- console.log(` ${chalk11.green("\u2713")} .viberails/context.md \u2014 regenerated`);
2929
- console.log(` ${chalk11.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3179
+ console.log(` ${chalk12.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3180
+ console.log(` ${chalk12.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
2930
3181
  }
2931
3182
 
2932
3183
  // src/index.ts
2933
- var VERSION = "0.5.0";
3184
+ var VERSION = "0.5.2";
2934
3185
  var program = new Command();
2935
3186
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
2936
3187
  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) => {
@@ -2938,16 +3189,25 @@ program.command("init", { isDefault: true }).description("Scan your project and
2938
3189
  await initCommand(options);
2939
3190
  } catch (err) {
2940
3191
  const message = err instanceof Error ? err.message : String(err);
2941
- console.error(`${chalk12.red("Error:")} ${message}`);
3192
+ console.error(`${chalk13.red("Error:")} ${message}`);
3193
+ process.exit(1);
3194
+ }
3195
+ });
3196
+ program.command("sync").description("Re-scan and update generated files").option("-i, --interactive", "Review changes before writing").action(async (options) => {
3197
+ try {
3198
+ await syncCommand(options);
3199
+ } catch (err) {
3200
+ const message = err instanceof Error ? err.message : String(err);
3201
+ console.error(`${chalk13.red("Error:")} ${message}`);
2942
3202
  process.exit(1);
2943
3203
  }
2944
3204
  });
2945
- program.command("sync").description("Re-scan and update generated files").action(async () => {
3205
+ program.command("config").description("Interactively edit existing config rules").option("--rescan", "Re-scan project first (picks up new packages, stack changes)").action(async (options) => {
2946
3206
  try {
2947
- await syncCommand();
3207
+ await configCommand(options);
2948
3208
  } catch (err) {
2949
3209
  const message = err instanceof Error ? err.message : String(err);
2950
- console.error(`${chalk12.red("Error:")} ${message}`);
3210
+ console.error(`${chalk13.red("Error:")} ${message}`);
2951
3211
  process.exit(1);
2952
3212
  }
2953
3213
  });
@@ -2968,7 +3228,7 @@ program.command("check").description("Check files against enforced rules").optio
2968
3228
  process.exit(exitCode);
2969
3229
  } catch (err) {
2970
3230
  const message = err instanceof Error ? err.message : String(err);
2971
- console.error(`${chalk12.red("Error:")} ${message}`);
3231
+ console.error(`${chalk13.red("Error:")} ${message}`);
2972
3232
  process.exit(1);
2973
3233
  }
2974
3234
  }
@@ -2979,7 +3239,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
2979
3239
  process.exit(exitCode);
2980
3240
  } catch (err) {
2981
3241
  const message = err instanceof Error ? err.message : String(err);
2982
- console.error(`${chalk12.red("Error:")} ${message}`);
3242
+ console.error(`${chalk13.red("Error:")} ${message}`);
2983
3243
  process.exit(1);
2984
3244
  }
2985
3245
  });
@@ -2988,7 +3248,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
2988
3248
  await boundariesCommand(options);
2989
3249
  } catch (err) {
2990
3250
  const message = err instanceof Error ? err.message : String(err);
2991
- console.error(`${chalk12.red("Error:")} ${message}`);
3251
+ console.error(`${chalk13.red("Error:")} ${message}`);
2992
3252
  process.exit(1);
2993
3253
  }
2994
3254
  });