viberails 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,7 +34,7 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk15 = __toESM(require("chalk"), 1);
37
+ var import_chalk16 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -84,6 +84,9 @@ var SENTINEL_CUSTOM = "__custom__";
84
84
  var SENTINEL_NONE = "__none__";
85
85
  var SENTINEL_INHERIT = "__inherit__";
86
86
  var SENTINEL_SKIP = "__skip__";
87
+ var HINT_NOT_SET = "not set";
88
+ var HINT_NO_OVERRIDES = "no overrides";
89
+ var HINT_AUTO_DETECT = "auto-detect";
87
90
 
88
91
  // src/utils/prompt-submenus.ts
89
92
  var clack = __toESM(require("@clack/prompts"), 1);
@@ -115,8 +118,7 @@ async function promptFileLimitsMenu(state) {
115
118
  { value: "back", label: "Back" }
116
119
  ]
117
120
  });
118
- assertNotCancelled(choice);
119
- if (choice === "back") return;
121
+ if (isCancelled(choice) || choice === "back") return;
120
122
  if (choice === "maxFileLines") {
121
123
  const result = await clack.text({
122
124
  message: "Maximum lines per source file?",
@@ -127,7 +129,7 @@ async function promptFileLimitsMenu(state) {
127
129
  if (Number.isNaN(n) || n < 1) return "Enter a positive number";
128
130
  }
129
131
  });
130
- assertNotCancelled(result);
132
+ if (isCancelled(result)) continue;
131
133
  state.maxFileLines = Number.parseInt(result, 10);
132
134
  }
133
135
  if (choice === "maxTestFileLines") {
@@ -140,7 +142,7 @@ async function promptFileLimitsMenu(state) {
140
142
  if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
141
143
  }
142
144
  });
143
- assertNotCancelled(result);
145
+ if (isCancelled(result)) continue;
144
146
  state.maxTestFileLines = Number.parseInt(result, 10);
145
147
  }
146
148
  }
@@ -158,42 +160,41 @@ async function promptNamingMenu(state) {
158
160
  options.push({
159
161
  value: "fileNaming",
160
162
  label: "File naming convention",
161
- hint: state.fileNamingValue ?? "(not set)"
163
+ hint: state.fileNamingValue ?? HINT_NOT_SET
162
164
  });
163
165
  }
164
166
  options.push(
165
167
  {
166
168
  value: "componentNaming",
167
169
  label: "Component naming",
168
- hint: state.componentNaming ?? "(not set)"
170
+ hint: state.componentNaming ?? HINT_NOT_SET
169
171
  },
170
172
  {
171
173
  value: "hookNaming",
172
174
  label: "Hook naming",
173
- hint: state.hookNaming ?? "(not set)"
175
+ hint: state.hookNaming ?? HINT_NOT_SET
174
176
  },
175
177
  {
176
178
  value: "importAlias",
177
179
  label: "Import alias",
178
- hint: state.importAlias ?? "(not set)"
180
+ hint: state.importAlias ?? HINT_NOT_SET
179
181
  },
180
182
  { value: "back", label: "Back" }
181
183
  );
182
184
  const choice = await clack.select({ message: "Naming & conventions", options });
183
- assertNotCancelled(choice);
184
- if (choice === "back") return;
185
+ if (isCancelled(choice) || choice === "back") return;
185
186
  if (choice === "enforceNaming") {
186
187
  const result = await clack.confirm({
187
188
  message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
188
189
  initialValue: state.enforceNaming
189
190
  });
190
- assertNotCancelled(result);
191
+ if (isCancelled(result)) continue;
191
192
  if (result && !state.fileNamingValue) {
192
193
  const selected = await clack.select({
193
194
  message: "Which file naming convention should be enforced?",
194
195
  options: [...FILE_NAMING_OPTIONS]
195
196
  });
196
- assertNotCancelled(selected);
197
+ if (isCancelled(selected)) continue;
197
198
  state.fileNamingValue = selected;
198
199
  }
199
200
  state.enforceNaming = result;
@@ -204,7 +205,7 @@ async function promptNamingMenu(state) {
204
205
  options: [...FILE_NAMING_OPTIONS],
205
206
  initialValue: state.fileNamingValue
206
207
  });
207
- assertNotCancelled(selected);
208
+ if (isCancelled(selected)) continue;
208
209
  state.fileNamingValue = selected;
209
210
  }
210
211
  if (choice === "componentNaming") {
@@ -216,7 +217,7 @@ async function promptNamingMenu(state) {
216
217
  ],
217
218
  initialValue: state.componentNaming ?? SENTINEL_CLEAR
218
219
  });
219
- assertNotCancelled(selected);
220
+ if (isCancelled(selected)) continue;
220
221
  state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
221
222
  }
222
223
  if (choice === "hookNaming") {
@@ -228,7 +229,7 @@ async function promptNamingMenu(state) {
228
229
  ],
229
230
  initialValue: state.hookNaming ?? SENTINEL_CLEAR
230
231
  });
231
- assertNotCancelled(selected);
232
+ if (isCancelled(selected)) continue;
232
233
  state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
233
234
  }
234
235
  if (choice === "importAlias") {
@@ -242,7 +243,7 @@ async function promptNamingMenu(state) {
242
243
  ],
243
244
  initialValue: state.importAlias ?? SENTINEL_CLEAR
244
245
  });
245
- assertNotCancelled(selected);
246
+ if (isCancelled(selected)) continue;
246
247
  if (selected === SENTINEL_CLEAR) {
247
248
  state.importAlias = void 0;
248
249
  } else if (selected === SENTINEL_CUSTOM) {
@@ -256,7 +257,7 @@ async function promptNamingMenu(state) {
256
257
  return "Must match pattern like @/*, ~/*, or #src/*";
257
258
  }
258
259
  });
259
- assertNotCancelled(result);
260
+ if (isCancelled(result)) continue;
260
261
  state.importAlias = result.trim();
261
262
  } else {
262
263
  state.importAlias = selected;
@@ -294,14 +295,13 @@ async function promptTestingMenu(state) {
294
295
  }
295
296
  options.push({ value: "back", label: "Back" });
296
297
  const choice = await clack.select({ message: "Testing & coverage", options });
297
- assertNotCancelled(choice);
298
- if (choice === "back") return;
298
+ if (isCancelled(choice) || choice === "back") return;
299
299
  if (choice === "enforceMissingTests") {
300
300
  const result = await clack.confirm({
301
301
  message: "Require every source file to have a corresponding test file?",
302
302
  initialValue: state.enforceMissingTests
303
303
  });
304
- assertNotCancelled(result);
304
+ if (isCancelled(result)) continue;
305
305
  state.enforceMissingTests = result;
306
306
  }
307
307
  if (choice === "testCoverage") {
@@ -314,7 +314,7 @@ async function promptTestingMenu(state) {
314
314
  if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
315
315
  }
316
316
  });
317
- assertNotCancelled(result);
317
+ if (isCancelled(result)) continue;
318
318
  state.testCoverage = Number.parseInt(result, 10);
319
319
  }
320
320
  if (choice === "coverageSummaryPath") {
@@ -325,7 +325,7 @@ async function promptTestingMenu(state) {
325
325
  if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
326
326
  }
327
327
  });
328
- assertNotCancelled(result);
328
+ if (isCancelled(result)) continue;
329
329
  state.coverageSummaryPath = result.trim();
330
330
  }
331
331
  if (choice === "coverageCommand") {
@@ -334,7 +334,7 @@ async function promptTestingMenu(state) {
334
334
  initialValue: state.coverageCommand ?? "",
335
335
  placeholder: "(auto-detect from package.json test runner)"
336
336
  });
337
- assertNotCancelled(result);
337
+ if (isCancelled(result)) continue;
338
338
  const trimmed = result.trim();
339
339
  state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
340
340
  }
@@ -378,7 +378,7 @@ function packageOverrideHint(pkg, defaults) {
378
378
  const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
379
379
  if (hasSummaryOverride) tags.push("summary override");
380
380
  if (hasCommandOverride) tags.push("command override");
381
- return tags.length > 0 ? tags.join(", ") : "(no overrides)";
381
+ return tags.length > 0 ? tags.join(", ") : HINT_NO_OVERRIDES;
382
382
  }
383
383
  async function promptPackageOverrides(packages, defaults) {
384
384
  const editablePackages = packages.filter((pkg) => pkg.path !== ".");
@@ -395,8 +395,7 @@ async function promptPackageOverrides(packages, defaults) {
395
395
  { value: SENTINEL_DONE, label: "Done" }
396
396
  ]
397
397
  });
398
- assertNotCancelled(selectedPath);
399
- if (selectedPath === SENTINEL_DONE) break;
398
+ if (isCancelled(selectedPath) || selectedPath === SENTINEL_DONE) break;
400
399
  const target = editablePackages.find((pkg) => pkg.path === selectedPath);
401
400
  if (!target) continue;
402
401
  await promptSinglePackageOverrides(target, defaults);
@@ -410,11 +409,11 @@ async function promptSinglePackageOverrides(target, defaults) {
410
409
  const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
411
410
  const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
412
411
  const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
413
- const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
412
+ const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? HINT_AUTO_DETECT;
414
413
  const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
415
414
  const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
416
- const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
417
- const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
415
+ const namingHint = hasNamingOverride ? String(effectiveNaming) : `inherits: ${effectiveNaming ?? "not set"}`;
416
+ const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `inherits: ${effectiveMaxLines}`;
418
417
  const choice = await clack2.select({
419
418
  message: `Edit overrides for ${target.path}`,
420
419
  options: [
@@ -427,8 +426,7 @@ async function promptSinglePackageOverrides(target, defaults) {
427
426
  { value: "back", label: "Back to package list" }
428
427
  ]
429
428
  });
430
- assertNotCancelled(choice);
431
- if (choice === "back") break;
429
+ if (isCancelled(choice) || choice === "back") break;
432
430
  if (choice === "fileNaming") {
433
431
  const selected = await clack2.select({
434
432
  message: `File naming for ${target.path}`,
@@ -442,7 +440,7 @@ async function promptSinglePackageOverrides(target, defaults) {
442
440
  ],
443
441
  initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
444
442
  });
445
- assertNotCancelled(selected);
443
+ if (isCancelled(selected)) continue;
446
444
  if (selected === SENTINEL_INHERIT) {
447
445
  if (target.conventions) delete target.conventions.fileNaming;
448
446
  } else if (selected === SENTINEL_NONE) {
@@ -457,7 +455,7 @@ async function promptSinglePackageOverrides(target, defaults) {
457
455
  initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
458
456
  placeholder: String(defaults.maxFileLines)
459
457
  });
460
- assertNotCancelled(result);
458
+ if (isCancelled(result)) continue;
461
459
  const value = result.trim();
462
460
  if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
463
461
  if (target.rules) delete target.rules.maxFileLines;
@@ -475,7 +473,7 @@ async function promptSinglePackageOverrides(target, defaults) {
475
473
  if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
476
474
  }
477
475
  });
478
- assertNotCancelled(result);
476
+ if (isCancelled(result)) continue;
479
477
  const nextCoverage = Number.parseInt(result, 10);
480
478
  if (nextCoverage === defaults.testCoverage) {
481
479
  if (target.rules) delete target.rules.testCoverage;
@@ -489,7 +487,7 @@ async function promptSinglePackageOverrides(target, defaults) {
489
487
  initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
490
488
  placeholder: defaults.coverageSummaryPath
491
489
  });
492
- assertNotCancelled(result);
490
+ if (isCancelled(result)) continue;
493
491
  const value = result.trim();
494
492
  if (value.length === 0 || value === defaults.coverageSummaryPath) {
495
493
  if (target.coverage) delete target.coverage.summaryPath;
@@ -503,7 +501,7 @@ async function promptSinglePackageOverrides(target, defaults) {
503
501
  initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
504
502
  placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
505
503
  });
506
- assertNotCancelled(result);
504
+ if (isCancelled(result)) continue;
507
505
  const value = result.trim();
508
506
  const defaultCommand = defaults.coverageCommand ?? "";
509
507
  if (value.length === 0 || value === defaultCommand) {
@@ -674,6 +672,9 @@ function assertNotCancelled(value) {
674
672
  process.exit(0);
675
673
  }
676
674
  }
675
+ function isCancelled(value) {
676
+ return clack5.isCancel(value);
677
+ }
677
678
  async function confirm3(message) {
678
679
  const result = await clack5.confirm({ message, initialValue: true });
679
680
  assertNotCancelled(result);
@@ -1412,9 +1413,9 @@ async function checkCommand(options, cwd) {
1412
1413
  }
1413
1414
  const violations = [];
1414
1415
  const severity = options.enforce ? "error" : "warn";
1415
- const log8 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1416
+ const log9 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1416
1417
  };
1417
- log8(" Checking files...");
1418
+ log9(" Checking files...");
1418
1419
  for (const file of filesToCheck) {
1419
1420
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1420
1421
  const relPath = path7.relative(projectRoot, absPath);
@@ -1447,9 +1448,9 @@ async function checkCommand(options, cwd) {
1447
1448
  }
1448
1449
  }
1449
1450
  }
1450
- log8(" done\n");
1451
+ log9(" done\n");
1451
1452
  if (!options.files) {
1452
- log8(" Checking missing tests...");
1453
+ log9(" Checking missing tests...");
1453
1454
  const testViolations = checkMissingTests(projectRoot, config, severity);
1454
1455
  if (options.staged) {
1455
1456
  const stagedSet = new Set(filesToCheck);
@@ -1462,14 +1463,14 @@ async function checkCommand(options, cwd) {
1462
1463
  } else {
1463
1464
  violations.push(...testViolations);
1464
1465
  }
1465
- log8(" done\n");
1466
+ log9(" done\n");
1466
1467
  }
1467
1468
  if (!options.files && !options.staged && !options.diffBase) {
1468
- log8(" Running test coverage...\n");
1469
+ log9(" Running test coverage...\n");
1469
1470
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1470
1471
  staged: options.staged,
1471
1472
  enforce: options.enforce,
1472
- onProgress: (pkg) => log8(` Coverage: ${pkg}...
1473
+ onProgress: (pkg) => log9(` Coverage: ${pkg}...
1473
1474
  `)
1474
1475
  });
1475
1476
  violations.push(...coverageViolations);
@@ -1494,7 +1495,7 @@ async function checkCommand(options, cwd) {
1494
1495
  severity
1495
1496
  });
1496
1497
  }
1497
- log8(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1498
+ log9(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1498
1499
  `);
1499
1500
  }
1500
1501
  if (options.format === "json") {
@@ -2776,10 +2777,10 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
2776
2777
  // src/commands/init.ts
2777
2778
  var fs21 = __toESM(require("fs"), 1);
2778
2779
  var path21 = __toESM(require("path"), 1);
2779
- var clack12 = __toESM(require("@clack/prompts"), 1);
2780
+ var clack13 = __toESM(require("@clack/prompts"), 1);
2780
2781
  var import_config9 = require("@viberails/config");
2781
2782
  var import_scanner3 = require("@viberails/scanner");
2782
- var import_chalk13 = __toESM(require("chalk"), 1);
2783
+ var import_chalk14 = __toESM(require("chalk"), 1);
2783
2784
 
2784
2785
  // src/utils/check-prerequisites.ts
2785
2786
  var fs14 = __toESM(require("fs"), 1);
@@ -2887,6 +2888,9 @@ async function executeDeferredInstalls(projectRoot, installs) {
2887
2888
  }
2888
2889
 
2889
2890
  // src/utils/prompt-main-menu.ts
2891
+ var clack11 = __toESM(require("@clack/prompts"), 1);
2892
+
2893
+ // src/utils/prompt-main-menu-handlers.ts
2890
2894
  var clack10 = __toESM(require("@clack/prompts"), 1);
2891
2895
 
2892
2896
  // src/utils/prompt-integrations.ts
@@ -2980,140 +2984,7 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
2980
2984
  };
2981
2985
  }
2982
2986
 
2983
- // src/utils/prompt-main-menu-hints.ts
2984
- function fileLimitsHint(config) {
2985
- const max = config.rules.maxFileLines;
2986
- const test = config.rules.maxTestFileLines;
2987
- return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
2988
- }
2989
- function fileNamingHint(config, scanResult) {
2990
- const rootPkg = getRootPackage(config.packages);
2991
- const naming = rootPkg.conventions?.fileNaming;
2992
- if (!config.rules.enforceNaming) return "not enforced";
2993
- if (naming) {
2994
- const detected = scanResult.packages.some(
2995
- (p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
2996
- );
2997
- return detected ? `${naming} (detected)` : naming;
2998
- }
2999
- return "mixed \u2014 will not enforce if skipped";
3000
- }
3001
- function fileNamingStatus(config) {
3002
- if (!config.rules.enforceNaming) return "disabled";
3003
- const rootPkg = getRootPackage(config.packages);
3004
- return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
3005
- }
3006
- function missingTestsHint(config) {
3007
- if (!config.rules.enforceMissingTests) return "not enforced";
3008
- const rootPkg = getRootPackage(config.packages);
3009
- const pattern = rootPkg.structure?.testPattern;
3010
- return pattern ? `enforced (${pattern})` : "enforced";
3011
- }
3012
- function coverageHint(config, hasTestRunner) {
3013
- if (config.rules.testCoverage === 0) return "disabled";
3014
- if (!hasTestRunner)
3015
- return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3016
- const isMonorepo = config.packages.length > 1;
3017
- if (isMonorepo) {
3018
- const withCov = config.packages.filter(
3019
- (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3020
- );
3021
- const exempt = config.packages.length - withCov.length;
3022
- return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3023
- }
3024
- return `${config.rules.testCoverage}%`;
3025
- }
3026
- function advancedNamingHint(config) {
3027
- const rootPkg = getRootPackage(config.packages);
3028
- const parts = [];
3029
- if (rootPkg.conventions?.componentNaming)
3030
- parts.push(`${rootPkg.conventions.componentNaming} components`);
3031
- if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
3032
- if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
3033
- return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
3034
- }
3035
- function integrationsHint(state) {
3036
- if (!state.visited.integrations || !state.integrations)
3037
- return "not configured \u2014 select to set up";
3038
- const items = [];
3039
- if (state.integrations.preCommitHook) items.push("pre-commit");
3040
- if (state.integrations.typecheckHook) items.push("typecheck");
3041
- if (state.integrations.lintHook) items.push("lint");
3042
- if (state.integrations.claudeCodeHook) items.push("Claude");
3043
- if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
3044
- if (state.integrations.githubAction) items.push("CI");
3045
- return items.length > 0 ? items.join(" \xB7 ") : "none selected";
3046
- }
3047
- function packageOverridesHint(config) {
3048
- const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3049
- const editable = config.packages.filter((p) => p.path !== ".");
3050
- const customized = editable.filter(
3051
- (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3052
- ).length;
3053
- return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3054
- }
3055
- function boundariesHint(config, state) {
3056
- if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3057
- const deny = config.boundaries?.deny;
3058
- if (!deny) return "enabled";
3059
- const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3060
- const pkgCount = Object.keys(deny).length;
3061
- return `${ruleCount} rules across ${pkgCount} packages`;
3062
- }
3063
- function statusIcon(status) {
3064
- if (status === "ok") return "\u2713";
3065
- if (status === "needs-input") return "?";
3066
- return "~";
3067
- }
3068
- function buildMainMenuOptions(config, scanResult, state) {
3069
- const namingStatus = fileNamingStatus(config);
3070
- const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
3071
- const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
3072
- const options = [
3073
- {
3074
- value: "fileLimits",
3075
- label: `${statusIcon("ok")} Max file size`,
3076
- hint: fileLimitsHint(config)
3077
- },
3078
- {
3079
- value: "fileNaming",
3080
- label: `${statusIcon(namingStatus)} File naming`,
3081
- hint: fileNamingHint(config, scanResult)
3082
- },
3083
- {
3084
- value: "missingTests",
3085
- label: `${statusIcon(missingTestsStatus)} Missing tests`,
3086
- hint: missingTestsHint(config)
3087
- },
3088
- {
3089
- value: "coverage",
3090
- label: `${statusIcon(coverageStatus)} Coverage`,
3091
- hint: coverageHint(config, state.hasTestRunner)
3092
- },
3093
- { value: "advancedNaming", label: " Advanced naming", hint: advancedNamingHint(config) }
3094
- ];
3095
- if (config.packages.length > 1) {
3096
- const bIcon = state.visited.boundaries && config.rules.enforceBoundaries ? statusIcon("ok") : " ";
3097
- options.push(
3098
- {
3099
- value: "packageOverrides",
3100
- label: " Per-package overrides",
3101
- hint: packageOverridesHint(config)
3102
- },
3103
- { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3104
- );
3105
- }
3106
- const iIcon = state.visited.integrations ? statusIcon("ok") : " ";
3107
- options.push(
3108
- { value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
3109
- { value: "reset", label: " Reset all to defaults" },
3110
- { value: "review", label: " Review scan details" },
3111
- { value: "done", label: " Done \u2014 write config" }
3112
- );
3113
- return options;
3114
- }
3115
-
3116
- // src/utils/prompt-main-menu.ts
2987
+ // src/utils/prompt-main-menu-handlers.ts
3117
2988
  async function handleAdvancedNaming(config) {
3118
2989
  const rootPkg = getRootPackage(config.packages);
3119
2990
  const state = {
@@ -3141,51 +3012,6 @@ async function handleAdvancedNaming(config) {
3141
3012
  rootPkg.conventions.hookNaming = state.hookNaming || void 0;
3142
3013
  rootPkg.conventions.importAlias = state.importAlias || void 0;
3143
3014
  }
3144
- async function promptMainMenu(config, scanResult, opts) {
3145
- const originalConfig = structuredClone(config);
3146
- const state = {
3147
- visited: { integrations: false, boundaries: false },
3148
- deferredInstalls: [],
3149
- hasTestRunner: opts.hasTestRunner,
3150
- hookManager: opts.hookManager
3151
- };
3152
- while (true) {
3153
- const options = buildMainMenuOptions(config, scanResult, state);
3154
- const choice = await clack10.select({ message: "Configure viberails", options });
3155
- assertNotCancelled(choice);
3156
- if (choice === "done") {
3157
- if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
3158
- config.rules.enforceNaming = false;
3159
- }
3160
- break;
3161
- }
3162
- if (choice === "fileLimits") {
3163
- const s = {
3164
- maxFileLines: config.rules.maxFileLines,
3165
- maxTestFileLines: config.rules.maxTestFileLines
3166
- };
3167
- await promptFileLimitsMenu(s);
3168
- config.rules.maxFileLines = s.maxFileLines;
3169
- config.rules.maxTestFileLines = s.maxTestFileLines;
3170
- }
3171
- if (choice === "fileNaming") await handleFileNaming(config, scanResult);
3172
- if (choice === "missingTests") await handleMissingTests(config);
3173
- if (choice === "coverage") await handleCoverage(config, state, opts);
3174
- if (choice === "advancedNaming") await handleAdvancedNaming(config);
3175
- if (choice === "packageOverrides") await handlePackageOverrides(config);
3176
- if (choice === "boundaries") await handleBoundaries(config, state, opts);
3177
- if (choice === "integrations") await handleIntegrations(state, opts);
3178
- if (choice === "review") clack10.note(formatScanResultsText(scanResult), "Scan details");
3179
- if (choice === "reset") {
3180
- Object.assign(config, structuredClone(originalConfig));
3181
- state.deferredInstalls = [];
3182
- state.visited = { integrations: false, boundaries: false };
3183
- state.integrations = void 0;
3184
- clack10.log.info("Reset all settings to scan-detected defaults.");
3185
- }
3186
- }
3187
- return state;
3188
- }
3189
3015
  async function handleFileNaming(config, scanResult) {
3190
3016
  const isMonorepo = config.packages.length > 1;
3191
3017
  if (isMonorepo) {
@@ -3214,7 +3040,7 @@ async function handleFileNaming(config, scanResult) {
3214
3040
  options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
3215
3041
  initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
3216
3042
  });
3217
- assertNotCancelled(selected);
3043
+ if (isCancelled(selected)) return;
3218
3044
  if (selected === SENTINEL_SKIP) {
3219
3045
  config.rules.enforceNaming = false;
3220
3046
  if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
@@ -3229,12 +3055,15 @@ async function handleMissingTests(config) {
3229
3055
  message: "Require every source file to have a test file?",
3230
3056
  initialValue: config.rules.enforceMissingTests
3231
3057
  });
3232
- assertNotCancelled(result);
3058
+ if (isCancelled(result)) return;
3233
3059
  config.rules.enforceMissingTests = result;
3234
3060
  }
3235
3061
  async function handleCoverage(config, state, opts) {
3236
3062
  if (!opts.hasTestRunner) {
3237
- clack10.log.info("Coverage checks are inactive \u2014 no test runner detected.");
3063
+ clack10.note(
3064
+ "No test runner (vitest, jest, etc.) was detected.\nInstall one, then re-run viberails init to configure coverage.",
3065
+ "Coverage inactive"
3066
+ );
3238
3067
  return;
3239
3068
  }
3240
3069
  const planned = planCoverageInstall(opts.coveragePrereqs);
@@ -3255,7 +3084,7 @@ async function handleCoverage(config, state, opts) {
3255
3084
  }
3256
3085
  ]
3257
3086
  });
3258
- assertNotCancelled(choice);
3087
+ if (isCancelled(choice)) return;
3259
3088
  state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
3260
3089
  if (choice === "install") {
3261
3090
  planned.onFailure = () => {
@@ -3276,7 +3105,7 @@ async function handleCoverage(config, state, opts) {
3276
3105
  if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
3277
3106
  }
3278
3107
  });
3279
- assertNotCancelled(result);
3108
+ if (isCancelled(result)) return;
3280
3109
  config.rules.testCoverage = Number.parseInt(result, 10);
3281
3110
  }
3282
3111
  async function handlePackageOverrides(config) {
@@ -3295,7 +3124,7 @@ async function handleBoundaries(config, state, opts) {
3295
3124
  message: "Infer boundary rules from current import patterns?",
3296
3125
  initialValue: false
3297
3126
  });
3298
- assertNotCancelled(shouldInfer);
3127
+ if (isCancelled(shouldInfer)) return;
3299
3128
  state.visited.boundaries = true;
3300
3129
  if (!shouldInfer) {
3301
3130
  config.rules.enforceBoundaries = false;
@@ -3338,6 +3167,215 @@ async function handleIntegrations(state, opts) {
3338
3167
  }
3339
3168
  }
3340
3169
 
3170
+ // src/utils/prompt-main-menu-hints.ts
3171
+ var import_chalk10 = __toESM(require("chalk"), 1);
3172
+ function fileLimitsHint(config) {
3173
+ const max = config.rules.maxFileLines;
3174
+ const test = config.rules.maxTestFileLines;
3175
+ return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
3176
+ }
3177
+ function fileNamingHint(config, scanResult) {
3178
+ const rootPkg = getRootPackage(config.packages);
3179
+ const naming = rootPkg.conventions?.fileNaming;
3180
+ if (!config.rules.enforceNaming) return "not enforced";
3181
+ if (naming) {
3182
+ const detected = scanResult.packages.some(
3183
+ (p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
3184
+ );
3185
+ return detected ? `${naming} (detected)` : naming;
3186
+ }
3187
+ return "mixed \u2014 will not enforce if skipped";
3188
+ }
3189
+ function fileNamingStatus(config) {
3190
+ if (!config.rules.enforceNaming) return "disabled";
3191
+ const rootPkg = getRootPackage(config.packages);
3192
+ return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
3193
+ }
3194
+ function missingTestsHint(config) {
3195
+ if (!config.rules.enforceMissingTests) return "not enforced";
3196
+ const rootPkg = getRootPackage(config.packages);
3197
+ const pattern = rootPkg.structure?.testPattern;
3198
+ return pattern ? `enforced (${pattern})` : "enforced";
3199
+ }
3200
+ function coverageHint(config, hasTestRunner) {
3201
+ if (config.rules.testCoverage === 0) return "disabled";
3202
+ if (!hasTestRunner)
3203
+ return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3204
+ const isMonorepo = config.packages.length > 1;
3205
+ if (isMonorepo) {
3206
+ const withCov = config.packages.filter(
3207
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3208
+ );
3209
+ const exempt = config.packages.length - withCov.length;
3210
+ return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3211
+ }
3212
+ return `${config.rules.testCoverage}%`;
3213
+ }
3214
+ function advancedNamingHint(config) {
3215
+ const rootPkg = getRootPackage(config.packages);
3216
+ const parts = [];
3217
+ if (rootPkg.conventions?.componentNaming)
3218
+ parts.push(`${rootPkg.conventions.componentNaming} components`);
3219
+ if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
3220
+ if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
3221
+ return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
3222
+ }
3223
+ function integrationsHint(state) {
3224
+ if (!state.visited.integrations || !state.integrations)
3225
+ return "not configured \u2014 select to set up";
3226
+ const items = [];
3227
+ if (state.integrations.preCommitHook) items.push("pre-commit");
3228
+ if (state.integrations.typecheckHook) items.push("typecheck");
3229
+ if (state.integrations.lintHook) items.push("lint");
3230
+ if (state.integrations.claudeCodeHook) items.push("Claude");
3231
+ if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
3232
+ if (state.integrations.githubAction) items.push("CI");
3233
+ return items.length > 0 ? items.join(" \xB7 ") : "none selected";
3234
+ }
3235
+ function packageOverridesHint(config) {
3236
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3237
+ const editable = config.packages.filter((p) => p.path !== ".");
3238
+ const customized = editable.filter(
3239
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3240
+ ).length;
3241
+ return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3242
+ }
3243
+ function boundariesHint(config, state) {
3244
+ if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3245
+ const deny = config.boundaries?.deny;
3246
+ if (!deny) return "enabled";
3247
+ const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3248
+ const pkgCount = Object.keys(deny).length;
3249
+ return `${ruleCount} rules across ${pkgCount} packages`;
3250
+ }
3251
+ function advancedNamingStatus(config) {
3252
+ const rootPkg = getRootPackage(config.packages);
3253
+ const hasAny = !!rootPkg.conventions?.componentNaming || !!rootPkg.conventions?.hookNaming || !!rootPkg.conventions?.importAlias;
3254
+ return hasAny ? "ok" : "unconfigured";
3255
+ }
3256
+ function packageOverridesStatus(config) {
3257
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3258
+ const editable = config.packages.filter((p) => p.path !== ".");
3259
+ const customized = editable.some(
3260
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3261
+ );
3262
+ return customized ? "ok" : "unconfigured";
3263
+ }
3264
+ function statusIcon(status) {
3265
+ if (status === "ok") return import_chalk10.default.green("\u2713");
3266
+ if (status === "needs-input") return import_chalk10.default.yellow("?");
3267
+ if (status === "unconfigured") return import_chalk10.default.dim("-");
3268
+ return import_chalk10.default.yellow("~");
3269
+ }
3270
+ function buildMainMenuOptions(config, scanResult, state) {
3271
+ const namingStatus = fileNamingStatus(config);
3272
+ const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
3273
+ const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
3274
+ const options = [
3275
+ {
3276
+ value: "fileLimits",
3277
+ label: `${statusIcon("ok")} Max file size`,
3278
+ hint: fileLimitsHint(config)
3279
+ },
3280
+ {
3281
+ value: "fileNaming",
3282
+ label: `${statusIcon(namingStatus)} Default file naming`,
3283
+ hint: fileNamingHint(config, scanResult)
3284
+ },
3285
+ {
3286
+ value: "missingTests",
3287
+ label: `${statusIcon(missingTestsStatus)} Missing tests`,
3288
+ hint: missingTestsHint(config)
3289
+ },
3290
+ {
3291
+ value: "coverage",
3292
+ label: `${statusIcon(coverageStatus)} Coverage`,
3293
+ hint: coverageHint(config, state.hasTestRunner)
3294
+ },
3295
+ {
3296
+ value: "advancedNaming",
3297
+ label: `${statusIcon(advancedNamingStatus(config))} Advanced naming`,
3298
+ hint: advancedNamingHint(config)
3299
+ }
3300
+ ];
3301
+ if (config.packages.length > 1) {
3302
+ const bIcon = statusIcon(
3303
+ state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
3304
+ );
3305
+ const poIcon = statusIcon(packageOverridesStatus(config));
3306
+ options.push(
3307
+ {
3308
+ value: "packageOverrides",
3309
+ label: `${poIcon} Per-package overrides`,
3310
+ hint: packageOverridesHint(config)
3311
+ },
3312
+ { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3313
+ );
3314
+ }
3315
+ const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
3316
+ options.push(
3317
+ { value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
3318
+ { value: "reset", label: " Reset all to defaults" },
3319
+ { value: "review", label: " Review scan details" },
3320
+ { value: "done", label: " Done \u2014 write config" }
3321
+ );
3322
+ return options;
3323
+ }
3324
+
3325
+ // src/utils/prompt-main-menu.ts
3326
+ async function promptMainMenu(config, scanResult, opts) {
3327
+ const originalConfig = structuredClone(config);
3328
+ const state = {
3329
+ visited: { integrations: false, boundaries: false },
3330
+ deferredInstalls: [],
3331
+ hasTestRunner: opts.hasTestRunner,
3332
+ hookManager: opts.hookManager
3333
+ };
3334
+ while (true) {
3335
+ const options = buildMainMenuOptions(config, scanResult, state);
3336
+ const choice = await clack11.select({ message: "Configure viberails", options });
3337
+ assertNotCancelled(choice);
3338
+ if (choice === "done") {
3339
+ if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
3340
+ config.rules.enforceNaming = false;
3341
+ }
3342
+ break;
3343
+ }
3344
+ if (choice === "fileLimits") {
3345
+ const s = {
3346
+ maxFileLines: config.rules.maxFileLines,
3347
+ maxTestFileLines: config.rules.maxTestFileLines
3348
+ };
3349
+ await promptFileLimitsMenu(s);
3350
+ config.rules.maxFileLines = s.maxFileLines;
3351
+ config.rules.maxTestFileLines = s.maxTestFileLines;
3352
+ }
3353
+ if (choice === "fileNaming") await handleFileNaming(config, scanResult);
3354
+ if (choice === "missingTests") await handleMissingTests(config);
3355
+ if (choice === "coverage") await handleCoverage(config, state, opts);
3356
+ if (choice === "advancedNaming") await handleAdvancedNaming(config);
3357
+ if (choice === "packageOverrides") await handlePackageOverrides(config);
3358
+ if (choice === "boundaries") await handleBoundaries(config, state, opts);
3359
+ if (choice === "integrations") await handleIntegrations(state, opts);
3360
+ if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
3361
+ if (choice === "reset") {
3362
+ const confirmed = await clack11.confirm({
3363
+ message: "Reset all settings to scan-detected defaults?",
3364
+ initialValue: false
3365
+ });
3366
+ assertNotCancelled(confirmed);
3367
+ if (confirmed) {
3368
+ Object.assign(config, structuredClone(originalConfig));
3369
+ state.deferredInstalls = [];
3370
+ state.visited = { integrations: false, boundaries: false };
3371
+ state.integrations = void 0;
3372
+ clack11.log.info("Reset all settings to scan-detected defaults.");
3373
+ }
3374
+ }
3375
+ }
3376
+ return state;
3377
+ }
3378
+
3341
3379
  // src/utils/update-gitignore.ts
3342
3380
  var fs16 = __toESM(require("fs"), 1);
3343
3381
  var path16 = __toESM(require("path"), 1);
@@ -3358,7 +3396,7 @@ function updateGitignore(projectRoot) {
3358
3396
  // src/commands/init-hooks.ts
3359
3397
  var fs18 = __toESM(require("fs"), 1);
3360
3398
  var path18 = __toESM(require("path"), 1);
3361
- var import_chalk10 = __toESM(require("chalk"), 1);
3399
+ var import_chalk11 = __toESM(require("chalk"), 1);
3362
3400
  var import_yaml = require("yaml");
3363
3401
 
3364
3402
  // src/commands/resolve-typecheck.ts
@@ -3403,13 +3441,13 @@ function setupPreCommitHook(projectRoot) {
3403
3441
  const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3404
3442
  if (fs18.existsSync(lefthookPath)) {
3405
3443
  addLefthookPreCommit(lefthookPath);
3406
- console.log(` ${import_chalk10.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3444
+ console.log(` ${import_chalk11.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3407
3445
  return "lefthook.yml";
3408
3446
  }
3409
3447
  const huskyDir = path18.join(projectRoot, ".husky");
3410
3448
  if (fs18.existsSync(huskyDir)) {
3411
3449
  writeHuskyPreCommit(huskyDir);
3412
- console.log(` ${import_chalk10.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3450
+ console.log(` ${import_chalk11.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3413
3451
  return ".husky/pre-commit";
3414
3452
  }
3415
3453
  const gitDir = path18.join(projectRoot, ".git");
@@ -3419,7 +3457,7 @@ function setupPreCommitHook(projectRoot) {
3419
3457
  fs18.mkdirSync(hooksDir, { recursive: true });
3420
3458
  }
3421
3459
  writeGitHookPreCommit(hooksDir);
3422
- console.log(` ${import_chalk10.default.green("\u2713")} .git/hooks/pre-commit`);
3460
+ console.log(` ${import_chalk11.default.green("\u2713")} .git/hooks/pre-commit`);
3423
3461
  return ".git/hooks/pre-commit";
3424
3462
  }
3425
3463
  return void 0;
@@ -3480,9 +3518,9 @@ function setupClaudeCodeHook(projectRoot) {
3480
3518
  settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
3481
3519
  } catch {
3482
3520
  console.warn(
3483
- ` ${import_chalk10.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3521
+ ` ${import_chalk11.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3484
3522
  );
3485
- console.warn(` Fix the JSON manually, then re-run ${import_chalk10.default.cyan("viberails init --force")}`);
3523
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk11.default.cyan("viberails init --force")}`);
3486
3524
  return;
3487
3525
  }
3488
3526
  }
@@ -3505,7 +3543,7 @@ function setupClaudeCodeHook(projectRoot) {
3505
3543
  settings.hooks = hooks;
3506
3544
  fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3507
3545
  `);
3508
- console.log(` ${import_chalk10.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3546
+ console.log(` ${import_chalk11.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3509
3547
  }
3510
3548
  function setupClaudeMdReference(projectRoot) {
3511
3549
  const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
@@ -3517,7 +3555,7 @@ function setupClaudeMdReference(projectRoot) {
3517
3555
  const ref = "\n@.viberails/context.md\n";
3518
3556
  const prefix = content.length === 0 ? "" : content.trimEnd();
3519
3557
  fs18.writeFileSync(claudeMdPath, prefix + ref);
3520
- console.log(` ${import_chalk10.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3558
+ console.log(` ${import_chalk11.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3521
3559
  }
3522
3560
  function setupGithubAction(projectRoot, packageManager, options) {
3523
3561
  const workflowDir = path18.join(projectRoot, ".github", "workflows");
@@ -3603,7 +3641,7 @@ ${cmd}
3603
3641
  // src/commands/init-hooks-extra.ts
3604
3642
  var fs19 = __toESM(require("fs"), 1);
3605
3643
  var path19 = __toESM(require("path"), 1);
3606
- var import_chalk11 = __toESM(require("chalk"), 1);
3644
+ var import_chalk12 = __toESM(require("chalk"), 1);
3607
3645
  var import_yaml2 = require("yaml");
3608
3646
  function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
3609
3647
  const lefthookPath = path19.join(projectRoot, "lefthook.yml");
@@ -3663,12 +3701,12 @@ ${command}
3663
3701
  function setupTypecheckHook(projectRoot, packageManager) {
3664
3702
  const resolved = resolveTypecheckCommand(projectRoot, packageManager);
3665
3703
  if (!resolved.command) {
3666
- console.log(` ${import_chalk11.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3704
+ console.log(` ${import_chalk12.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3667
3705
  return void 0;
3668
3706
  }
3669
3707
  const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
3670
3708
  if (target) {
3671
- console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3709
+ console.log(` ${import_chalk12.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3672
3710
  }
3673
3711
  return target;
3674
3712
  }
@@ -3689,7 +3727,7 @@ function setupLintHook(projectRoot, linter) {
3689
3727
  }
3690
3728
  const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
3691
3729
  if (target) {
3692
- console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3730
+ console.log(` ${import_chalk12.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3693
3731
  }
3694
3732
  return target;
3695
3733
  }
@@ -3698,7 +3736,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3698
3736
  if (integrations.preCommitHook) {
3699
3737
  const t = setupPreCommitHook(projectRoot);
3700
3738
  if (t && opts.lefthookExpected && !t.includes("lefthook")) {
3701
- console.log(` ${import_chalk11.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3739
+ console.log(` ${import_chalk12.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3702
3740
  }
3703
3741
  created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
3704
3742
  }
@@ -3731,10 +3769,10 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3731
3769
  // src/commands/init-non-interactive.ts
3732
3770
  var fs20 = __toESM(require("fs"), 1);
3733
3771
  var path20 = __toESM(require("path"), 1);
3734
- var clack11 = __toESM(require("@clack/prompts"), 1);
3772
+ var clack12 = __toESM(require("@clack/prompts"), 1);
3735
3773
  var import_config8 = require("@viberails/config");
3736
3774
  var import_scanner2 = require("@viberails/scanner");
3737
- var import_chalk12 = __toESM(require("chalk"), 1);
3775
+ var import_chalk13 = __toESM(require("chalk"), 1);
3738
3776
 
3739
3777
  // src/utils/filter-confidence.ts
3740
3778
  function filterHighConfidence(conventions, meta) {
@@ -3755,7 +3793,7 @@ function getExemptedPackages(config) {
3755
3793
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3756
3794
  }
3757
3795
  async function initNonInteractive(projectRoot, configPath) {
3758
- const s = clack11.spinner();
3796
+ const s = clack12.spinner();
3759
3797
  s.start("Scanning project...");
3760
3798
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
3761
3799
  const config = (0, import_config8.generateConfig)(scanResult);
@@ -3770,11 +3808,11 @@ async function initNonInteractive(projectRoot, configPath) {
3770
3808
  const exempted = getExemptedPackages(config);
3771
3809
  if (exempted.length > 0) {
3772
3810
  console.log(
3773
- ` ${import_chalk12.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk12.default.dim("(types-only)")}`
3811
+ ` ${import_chalk13.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk13.default.dim("(types-only)")}`
3774
3812
  );
3775
3813
  }
3776
3814
  if (config.packages.length > 1) {
3777
- const bs = clack11.spinner();
3815
+ const bs = clack12.spinner();
3778
3816
  bs.start("Building import graph...");
3779
3817
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3780
3818
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3807,14 +3845,14 @@ async function initNonInteractive(projectRoot, configPath) {
3807
3845
  const hookManager = detectHookManager(projectRoot);
3808
3846
  const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
3809
3847
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3810
- const ok = import_chalk12.default.green("\u2713");
3848
+ const ok = import_chalk13.default.green("\u2713");
3811
3849
  const created = [
3812
3850
  `${ok} ${path20.basename(configPath)}`,
3813
3851
  `${ok} .viberails/context.md`,
3814
3852
  `${ok} .viberails/scan-result.json`,
3815
3853
  `${ok} .claude/settings.json \u2014 added viberails hook`,
3816
3854
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
3817
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk12.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3855
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk13.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3818
3856
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
3819
3857
  ].filter(Boolean);
3820
3858
  if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
@@ -3839,8 +3877,8 @@ async function initCommand(options, cwd) {
3839
3877
  return initInteractive(projectRoot, configPath, options);
3840
3878
  }
3841
3879
  console.log(
3842
- `${import_chalk13.default.yellow("!")} viberails is already initialized.
3843
- Run ${import_chalk13.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk13.default.cyan("viberails sync")} to update generated files, or ${import_chalk13.default.cyan("viberails init --force")} to replace it.`
3880
+ `${import_chalk14.default.yellow("!")} viberails is already initialized.
3881
+ Run ${import_chalk14.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk14.default.cyan("viberails sync")} to update generated files, or ${import_chalk14.default.cyan("viberails init --force")} to replace it.`
3844
3882
  );
3845
3883
  return;
3846
3884
  }
@@ -3848,11 +3886,11 @@ async function initCommand(options, cwd) {
3848
3886
  await initInteractive(projectRoot, configPath, options);
3849
3887
  }
3850
3888
  async function initInteractive(projectRoot, configPath, options) {
3851
- clack12.intro("viberails");
3889
+ clack13.intro("viberails");
3852
3890
  if (fs21.existsSync(configPath) && !options.force) {
3853
3891
  const action = await promptExistingConfigAction(path21.basename(configPath));
3854
3892
  if (action === "cancel") {
3855
- clack12.outro("Aborted. No files were written.");
3893
+ clack13.outro("Aborted. No files were written.");
3856
3894
  return;
3857
3895
  }
3858
3896
  if (action === "edit") {
@@ -3866,23 +3904,23 @@ async function initInteractive(projectRoot, configPath, options) {
3866
3904
  `${path21.basename(configPath)} already exists and will be replaced. Continue?`
3867
3905
  );
3868
3906
  if (!replace) {
3869
- clack12.outro("Aborted. No files were written.");
3907
+ clack13.outro("Aborted. No files were written.");
3870
3908
  return;
3871
3909
  }
3872
3910
  }
3873
- const s = clack12.spinner();
3911
+ const s = clack13.spinner();
3874
3912
  s.start("Scanning project...");
3875
3913
  const scanResult = await (0, import_scanner3.scan)(projectRoot);
3876
3914
  const config = (0, import_config9.generateConfig)(scanResult);
3877
3915
  s.stop("Scan complete");
3878
3916
  if (scanResult.statistics.totalFiles === 0) {
3879
- clack12.log.warn(
3917
+ clack13.log.warn(
3880
3918
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3881
3919
  );
3882
3920
  }
3883
3921
  const hasTestRunner = !!scanResult.stack.testRunner;
3884
3922
  if (!hasTestRunner) {
3885
- clack12.log.info(
3923
+ clack13.log.info(
3886
3924
  "No test runner detected. Coverage checks are inactive until a test runner is installed.\nInstall a test runner (e.g. vitest) and re-run viberails init."
3887
3925
  );
3888
3926
  }
@@ -3903,13 +3941,13 @@ async function initInteractive(projectRoot, configPath, options) {
3903
3941
  });
3904
3942
  const shouldWrite = await confirm3("Apply this setup?");
3905
3943
  if (!shouldWrite) {
3906
- clack12.outro("Aborted. No files were written.");
3944
+ clack13.outro("Aborted. No files were written.");
3907
3945
  return;
3908
3946
  }
3909
3947
  if (state.deferredInstalls.length > 0) {
3910
3948
  await executeDeferredInstalls(projectRoot, state.deferredInstalls);
3911
3949
  }
3912
- const ws = clack12.spinner();
3950
+ const ws = clack13.spinner();
3913
3951
  ws.start("Writing configuration...");
3914
3952
  const compacted = (0, import_config9.compactConfig)(config);
3915
3953
  fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
@@ -3917,10 +3955,10 @@ async function initInteractive(projectRoot, configPath, options) {
3917
3955
  writeGeneratedFiles(projectRoot, config, scanResult);
3918
3956
  updateGitignore(projectRoot);
3919
3957
  ws.stop("Configuration written");
3920
- const ok = import_chalk13.default.green("\u2713");
3921
- clack12.log.step(`${ok} ${path21.basename(configPath)}`);
3922
- clack12.log.step(`${ok} .viberails/context.md`);
3923
- clack12.log.step(`${ok} .viberails/scan-result.json`);
3958
+ const ok = import_chalk14.default.green("\u2713");
3959
+ clack13.log.step(`${ok} ${path21.basename(configPath)}`);
3960
+ clack13.log.step(`${ok} .viberails/context.md`);
3961
+ clack13.log.step(`${ok} .viberails/scan-result.json`);
3924
3962
  if (state.visited.integrations && state.integrations) {
3925
3963
  const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
3926
3964
  setupSelectedIntegrations(projectRoot, state.integrations, {
@@ -3929,19 +3967,19 @@ async function initInteractive(projectRoot, configPath, options) {
3929
3967
  lefthookExpected
3930
3968
  });
3931
3969
  }
3932
- clack12.outro(
3970
+ clack13.outro(
3933
3971
  `Done! Next: review viberails.config.json, then run viberails check
3934
- ${import_chalk13.default.dim("Tip: use")} ${import_chalk13.default.cyan("viberails check --enforce")} ${import_chalk13.default.dim("in CI to block PRs on violations.")}`
3972
+ ${import_chalk14.default.dim("Tip: use")} ${import_chalk14.default.cyan("viberails check --enforce")} ${import_chalk14.default.dim("in CI to block PRs on violations.")}`
3935
3973
  );
3936
3974
  }
3937
3975
 
3938
3976
  // src/commands/sync.ts
3939
3977
  var fs22 = __toESM(require("fs"), 1);
3940
3978
  var path22 = __toESM(require("path"), 1);
3941
- var clack13 = __toESM(require("@clack/prompts"), 1);
3979
+ var clack14 = __toESM(require("@clack/prompts"), 1);
3942
3980
  var import_config11 = require("@viberails/config");
3943
3981
  var import_scanner4 = require("@viberails/scanner");
3944
- var import_chalk14 = __toESM(require("chalk"), 1);
3982
+ var import_chalk15 = __toESM(require("chalk"), 1);
3945
3983
  var CONFIG_FILE6 = "viberails.config.json";
3946
3984
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
3947
3985
  function loadPreviousStats(projectRoot) {
@@ -3967,7 +4005,7 @@ async function syncCommand(options, cwd) {
3967
4005
  const configPath = path22.join(projectRoot, CONFIG_FILE6);
3968
4006
  const existing = await (0, import_config11.loadConfig)(configPath);
3969
4007
  const previousStats = loadPreviousStats(projectRoot);
3970
- const s = clack13.spinner();
4008
+ const s = clack14.spinner();
3971
4009
  s.start("Scanning project...");
3972
4010
  const scanResult = await (0, import_scanner4.scan)(projectRoot);
3973
4011
  s.stop("Scan complete");
@@ -3982,19 +4020,19 @@ async function syncCommand(options, cwd) {
3982
4020
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
3983
4021
  if (changes.length > 0 || statsDelta) {
3984
4022
  console.log(`
3985
- ${import_chalk14.default.bold("Changes:")}`);
4023
+ ${import_chalk15.default.bold("Changes:")}`);
3986
4024
  for (const change of changes) {
3987
- const icon = change.type === "removed" ? import_chalk14.default.red("-") : import_chalk14.default.green("+");
4025
+ const icon = change.type === "removed" ? import_chalk15.default.red("-") : import_chalk15.default.green("+");
3988
4026
  console.log(` ${icon} ${change.description}`);
3989
4027
  }
3990
4028
  if (statsDelta) {
3991
- console.log(` ${import_chalk14.default.dim(statsDelta)}`);
4029
+ console.log(` ${import_chalk15.default.dim(statsDelta)}`);
3992
4030
  }
3993
4031
  }
3994
4032
  if (options?.interactive) {
3995
- clack13.intro("viberails sync (interactive)");
3996
- clack13.note(formatRulesText(merged).join("\n"), "Rules after sync");
3997
- const decision = await clack13.select({
4033
+ clack14.intro("viberails sync (interactive)");
4034
+ clack14.note(formatRulesText(merged).join("\n"), "Rules after sync");
4035
+ const decision = await clack14.select({
3998
4036
  message: "How would you like to proceed?",
3999
4037
  options: [
4000
4038
  { value: "accept", label: "Accept changes" },
@@ -4004,7 +4042,7 @@ ${import_chalk14.default.bold("Changes:")}`);
4004
4042
  });
4005
4043
  assertNotCancelled(decision);
4006
4044
  if (decision === "cancel") {
4007
- clack13.outro("Sync cancelled. No files were written.");
4045
+ clack14.outro("Sync cancelled. No files were written.");
4008
4046
  return;
4009
4047
  }
4010
4048
  if (decision === "customize") {
@@ -4028,8 +4066,8 @@ ${import_chalk14.default.bold("Changes:")}`);
4028
4066
  fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4029
4067
  `);
4030
4068
  writeGeneratedFiles(projectRoot, merged, scanResult);
4031
- clack13.log.success("Updated config with your customizations.");
4032
- clack13.outro("Done! Run viberails check to verify.");
4069
+ clack14.log.success("Updated config with your customizations.");
4070
+ clack14.outro("Done! Run viberails check to verify.");
4033
4071
  return;
4034
4072
  }
4035
4073
  }
@@ -4037,18 +4075,18 @@ ${import_chalk14.default.bold("Changes:")}`);
4037
4075
  `);
4038
4076
  writeGeneratedFiles(projectRoot, merged, scanResult);
4039
4077
  console.log(`
4040
- ${import_chalk14.default.bold("Synced:")}`);
4078
+ ${import_chalk15.default.bold("Synced:")}`);
4041
4079
  if (configChanged) {
4042
- console.log(` ${import_chalk14.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4080
+ console.log(` ${import_chalk15.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4043
4081
  } else {
4044
- console.log(` ${import_chalk14.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4082
+ console.log(` ${import_chalk15.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4045
4083
  }
4046
- console.log(` ${import_chalk14.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4047
- console.log(` ${import_chalk14.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
4084
+ console.log(` ${import_chalk15.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4085
+ console.log(` ${import_chalk15.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
4048
4086
  }
4049
4087
 
4050
4088
  // src/index.ts
4051
- var VERSION = "0.6.6";
4089
+ var VERSION = "0.6.7";
4052
4090
  var program = new import_commander.Command();
4053
4091
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
4054
4092
  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) => {
@@ -4056,7 +4094,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
4056
4094
  await initCommand(options);
4057
4095
  } catch (err) {
4058
4096
  const message = err instanceof Error ? err.message : String(err);
4059
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4097
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4060
4098
  process.exit(1);
4061
4099
  }
4062
4100
  });
@@ -4065,7 +4103,7 @@ program.command("sync").description("Re-scan and update generated files").option
4065
4103
  await syncCommand(options);
4066
4104
  } catch (err) {
4067
4105
  const message = err instanceof Error ? err.message : String(err);
4068
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4106
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4069
4107
  process.exit(1);
4070
4108
  }
4071
4109
  });
@@ -4074,7 +4112,7 @@ program.command("config").description("Interactively edit existing config rules"
4074
4112
  await configCommand(options);
4075
4113
  } catch (err) {
4076
4114
  const message = err instanceof Error ? err.message : String(err);
4077
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4115
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4078
4116
  process.exit(1);
4079
4117
  }
4080
4118
  });
@@ -4095,7 +4133,7 @@ program.command("check").description("Check files against enforced rules").optio
4095
4133
  process.exit(exitCode);
4096
4134
  } catch (err) {
4097
4135
  const message = err instanceof Error ? err.message : String(err);
4098
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4136
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4099
4137
  process.exit(1);
4100
4138
  }
4101
4139
  }
@@ -4106,7 +4144,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
4106
4144
  process.exit(exitCode);
4107
4145
  } catch (err) {
4108
4146
  const message = err instanceof Error ? err.message : String(err);
4109
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4147
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4110
4148
  process.exit(1);
4111
4149
  }
4112
4150
  });
@@ -4115,7 +4153,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
4115
4153
  await boundariesCommand(options);
4116
4154
  } catch (err) {
4117
4155
  const message = err instanceof Error ? err.message : String(err);
4118
- console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4156
+ console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4119
4157
  process.exit(1);
4120
4158
  }
4121
4159
  });