viberails 0.6.5 → 0.6.6

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
@@ -6,9 +6,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
9
  var __export = (target, all) => {
13
10
  for (var name in all)
14
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -31,178 +28,82 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
28
  ));
32
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
30
 
34
- // src/utils/spawn-async.ts
35
- function spawnAsync(command, cwd) {
36
- return new Promise((resolve4) => {
37
- const child = (0, import_node_child_process.spawn)(command, { cwd, shell: true, stdio: "pipe" });
38
- let stdout = "";
39
- let stderr = "";
40
- child.stdout.on("data", (d) => {
41
- stdout += d.toString();
42
- });
43
- child.stderr.on("data", (d) => {
44
- stderr += d.toString();
45
- });
46
- child.on("close", (status) => {
47
- resolve4({ status, stdout, stderr });
48
- });
49
- child.on("error", () => {
50
- resolve4({ status: 1, stdout, stderr });
51
- });
52
- });
53
- }
54
- var import_node_child_process;
55
- var init_spawn_async = __esm({
56
- "src/utils/spawn-async.ts"() {
57
- "use strict";
58
- import_node_child_process = require("child_process");
59
- }
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ VERSION: () => VERSION
60
35
  });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_chalk15 = __toESM(require("chalk"), 1);
38
+ var import_commander = require("commander");
61
39
 
62
- // src/utils/prompt-integrations.ts
63
- async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
64
- const choice = await clack.select({
65
- message: "No shared git hook manager detected. Install Lefthook?",
66
- options: [
67
- {
68
- value: "install",
69
- label: "Yes, install Lefthook",
70
- hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
71
- },
72
- {
73
- value: "skip",
74
- label: "No, skip",
75
- hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
76
- }
77
- ]
78
- });
79
- assertNotCancelled(choice);
80
- if (choice !== "install") return void 0;
81
- const pm = packageManager || "npm";
82
- const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
83
- const s = clack.spinner();
84
- s.start("Installing Lefthook...");
85
- const result = await spawnAsync(installCmd, projectRoot);
86
- if (result.status === 0) {
87
- const fs22 = await import("fs");
88
- const path22 = await import("path");
89
- const lefthookPath = path22.join(projectRoot, "lefthook.yml");
90
- if (!fs22.existsSync(lefthookPath)) {
91
- fs22.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
92
- }
93
- s.stop("Installed Lefthook");
94
- return "Lefthook";
95
- }
96
- s.stop("Failed to install Lefthook");
97
- clack.log.warn(`Install manually: ${installCmd}`);
98
- return void 0;
99
- }
100
- async function promptIntegrations(projectRoot, hookManager, tools) {
101
- let resolvedHookManager = hookManager;
102
- if (!resolvedHookManager) {
103
- resolvedHookManager = await promptHookManagerInstall(
104
- projectRoot,
105
- tools?.packageManager ?? "npm",
106
- tools?.isWorkspace
107
- );
108
- }
109
- const isBareHook = !resolvedHookManager;
110
- const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
111
- const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
112
- const options = [
113
- {
114
- value: "preCommit",
115
- label: hookLabel,
116
- hint: hookHint
40
+ // src/commands/boundaries.ts
41
+ var fs3 = __toESM(require("fs"), 1);
42
+ var path3 = __toESM(require("path"), 1);
43
+ var import_config = require("@viberails/config");
44
+ var import_chalk = __toESM(require("chalk"), 1);
45
+
46
+ // src/utils/find-project-root.ts
47
+ var fs = __toESM(require("fs"), 1);
48
+ var path = __toESM(require("path"), 1);
49
+ function findProjectRoot(startDir) {
50
+ let dir = path.resolve(startDir);
51
+ while (true) {
52
+ if (fs.existsSync(path.join(dir, "package.json"))) {
53
+ return dir;
117
54
  }
118
- ];
119
- if (tools?.isTypeScript) {
120
- options.push({
121
- value: "typecheck",
122
- label: "Typecheck (tsc --noEmit)",
123
- hint: "pre-commit hook + CI check"
124
- });
125
- }
126
- if (tools?.linter) {
127
- const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
128
- options.push({
129
- value: "lint",
130
- label: `Lint check (${linterName})`,
131
- hint: "pre-commit hook + CI check"
132
- });
133
- }
134
- options.push(
135
- {
136
- value: "claude",
137
- label: "Claude Code hook",
138
- hint: "checks files when Claude edits them"
139
- },
140
- {
141
- value: "claudeMd",
142
- label: "CLAUDE.md reference",
143
- hint: "appends @.viberails/context.md so Claude loads rules automatically"
144
- },
145
- {
146
- value: "githubAction",
147
- label: "GitHub Actions workflow",
148
- hint: "blocks PRs that fail viberails check"
55
+ const parent = path.dirname(dir);
56
+ if (parent === dir) {
57
+ return null;
149
58
  }
150
- );
151
- const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
152
- const result = await clack.multiselect({
153
- message: "Optional integrations",
154
- options,
155
- initialValues,
156
- required: false
157
- });
158
- assertNotCancelled(result);
159
- return {
160
- preCommitHook: result.includes("preCommit"),
161
- claudeCodeHook: result.includes("claude"),
162
- claudeMdRef: result.includes("claudeMd"),
163
- githubAction: result.includes("githubAction"),
164
- typecheckHook: result.includes("typecheck"),
165
- lintHook: result.includes("lint")
166
- };
167
- }
168
- var clack;
169
- var init_prompt_integrations = __esm({
170
- "src/utils/prompt-integrations.ts"() {
171
- "use strict";
172
- clack = __toESM(require("@clack/prompts"), 1);
173
- init_prompt();
174
- init_spawn_async();
59
+ dir = parent;
175
60
  }
176
- });
61
+ }
62
+
63
+ // src/utils/prompt.ts
64
+ var clack5 = __toESM(require("@clack/prompts"), 1);
65
+
66
+ // src/utils/prompt-rules.ts
67
+ var clack4 = __toESM(require("@clack/prompts"), 1);
177
68
 
178
69
  // src/utils/get-root-package.ts
179
70
  function getRootPackage(packages) {
180
71
  return packages.find((pkg) => pkg.path === ".") ?? packages[0];
181
72
  }
182
- var init_get_root_package = __esm({
183
- "src/utils/get-root-package.ts"() {
184
- "use strict";
185
- }
186
- });
73
+
74
+ // src/utils/prompt-menu-handlers.ts
75
+ var clack3 = __toESM(require("@clack/prompts"), 1);
76
+
77
+ // src/utils/prompt-package-overrides.ts
78
+ var clack2 = __toESM(require("@clack/prompts"), 1);
187
79
 
188
80
  // src/utils/prompt-constants.ts
189
- var SENTINEL_DONE, SENTINEL_CLEAR, SENTINEL_CUSTOM, SENTINEL_NONE, SENTINEL_INHERIT, SENTINEL_SKIP;
190
- var init_prompt_constants = __esm({
191
- "src/utils/prompt-constants.ts"() {
192
- "use strict";
193
- SENTINEL_DONE = "__done__";
194
- SENTINEL_CLEAR = "__clear__";
195
- SENTINEL_CUSTOM = "__custom__";
196
- SENTINEL_NONE = "__none__";
197
- SENTINEL_INHERIT = "__inherit__";
198
- SENTINEL_SKIP = "__skip__";
199
- }
200
- });
81
+ var SENTINEL_DONE = "__done__";
82
+ var SENTINEL_CLEAR = "__clear__";
83
+ var SENTINEL_CUSTOM = "__custom__";
84
+ var SENTINEL_NONE = "__none__";
85
+ var SENTINEL_INHERIT = "__inherit__";
86
+ var SENTINEL_SKIP = "__skip__";
201
87
 
202
88
  // src/utils/prompt-submenus.ts
89
+ var clack = __toESM(require("@clack/prompts"), 1);
90
+ var FILE_NAMING_OPTIONS = [
91
+ { value: "kebab-case", label: "kebab-case" },
92
+ { value: "camelCase", label: "camelCase" },
93
+ { value: "PascalCase", label: "PascalCase" },
94
+ { value: "snake_case", label: "snake_case" }
95
+ ];
96
+ var COMPONENT_NAMING_OPTIONS = [
97
+ { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
98
+ { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
99
+ ];
100
+ var HOOK_NAMING_OPTIONS = [
101
+ { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
102
+ { value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
103
+ ];
203
104
  async function promptFileLimitsMenu(state) {
204
105
  while (true) {
205
- const choice = await clack2.select({
106
+ const choice = await clack.select({
206
107
  message: "File limits",
207
108
  options: [
208
109
  { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
@@ -217,7 +118,7 @@ async function promptFileLimitsMenu(state) {
217
118
  assertNotCancelled(choice);
218
119
  if (choice === "back") return;
219
120
  if (choice === "maxFileLines") {
220
- const result = await clack2.text({
121
+ const result = await clack.text({
221
122
  message: "Maximum lines per source file?",
222
123
  initialValue: String(state.maxFileLines),
223
124
  validate: (v) => {
@@ -230,7 +131,7 @@ async function promptFileLimitsMenu(state) {
230
131
  state.maxFileLines = Number.parseInt(result, 10);
231
132
  }
232
133
  if (choice === "maxTestFileLines") {
233
- const result = await clack2.text({
134
+ const result = await clack.text({
234
135
  message: "Maximum lines per test file (0 to disable)?",
235
136
  initialValue: String(state.maxTestFileLines),
236
137
  validate: (v) => {
@@ -278,17 +179,17 @@ async function promptNamingMenu(state) {
278
179
  },
279
180
  { value: "back", label: "Back" }
280
181
  );
281
- const choice = await clack2.select({ message: "Naming & conventions", options });
182
+ const choice = await clack.select({ message: "Naming & conventions", options });
282
183
  assertNotCancelled(choice);
283
184
  if (choice === "back") return;
284
185
  if (choice === "enforceNaming") {
285
- const result = await clack2.confirm({
186
+ const result = await clack.confirm({
286
187
  message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
287
188
  initialValue: state.enforceNaming
288
189
  });
289
190
  assertNotCancelled(result);
290
191
  if (result && !state.fileNamingValue) {
291
- const selected = await clack2.select({
192
+ const selected = await clack.select({
292
193
  message: "Which file naming convention should be enforced?",
293
194
  options: [...FILE_NAMING_OPTIONS]
294
195
  });
@@ -298,7 +199,7 @@ async function promptNamingMenu(state) {
298
199
  state.enforceNaming = result;
299
200
  }
300
201
  if (choice === "fileNaming") {
301
- const selected = await clack2.select({
202
+ const selected = await clack.select({
302
203
  message: "Which file naming convention should be enforced?",
303
204
  options: [...FILE_NAMING_OPTIONS],
304
205
  initialValue: state.fileNamingValue
@@ -307,7 +208,7 @@ async function promptNamingMenu(state) {
307
208
  state.fileNamingValue = selected;
308
209
  }
309
210
  if (choice === "componentNaming") {
310
- const selected = await clack2.select({
211
+ const selected = await clack.select({
311
212
  message: "Component naming convention",
312
213
  options: [
313
214
  ...COMPONENT_NAMING_OPTIONS,
@@ -319,7 +220,7 @@ async function promptNamingMenu(state) {
319
220
  state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
320
221
  }
321
222
  if (choice === "hookNaming") {
322
- const selected = await clack2.select({
223
+ const selected = await clack.select({
323
224
  message: "Hook naming convention",
324
225
  options: [
325
226
  ...HOOK_NAMING_OPTIONS,
@@ -331,7 +232,7 @@ async function promptNamingMenu(state) {
331
232
  state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
332
233
  }
333
234
  if (choice === "importAlias") {
334
- const selected = await clack2.select({
235
+ const selected = await clack.select({
335
236
  message: "Import alias pattern",
336
237
  options: [
337
238
  { value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
@@ -345,7 +246,7 @@ async function promptNamingMenu(state) {
345
246
  if (selected === SENTINEL_CLEAR) {
346
247
  state.importAlias = void 0;
347
248
  } else if (selected === SENTINEL_CUSTOM) {
348
- const result = await clack2.text({
249
+ const result = await clack.text({
349
250
  message: "Custom import alias (e.g. #/*)?",
350
251
  initialValue: state.importAlias ?? "",
351
252
  placeholder: "e.g. #/*",
@@ -392,11 +293,11 @@ async function promptTestingMenu(state) {
392
293
  );
393
294
  }
394
295
  options.push({ value: "back", label: "Back" });
395
- const choice = await clack2.select({ message: "Testing & coverage", options });
296
+ const choice = await clack.select({ message: "Testing & coverage", options });
396
297
  assertNotCancelled(choice);
397
298
  if (choice === "back") return;
398
299
  if (choice === "enforceMissingTests") {
399
- const result = await clack2.confirm({
300
+ const result = await clack.confirm({
400
301
  message: "Require every source file to have a corresponding test file?",
401
302
  initialValue: state.enforceMissingTests
402
303
  });
@@ -404,7 +305,7 @@ async function promptTestingMenu(state) {
404
305
  state.enforceMissingTests = result;
405
306
  }
406
307
  if (choice === "testCoverage") {
407
- const result = await clack2.text({
308
+ const result = await clack.text({
408
309
  message: "Test coverage target (0 disables coverage checks)?",
409
310
  initialValue: String(state.testCoverage),
410
311
  validate: (v) => {
@@ -417,7 +318,7 @@ async function promptTestingMenu(state) {
417
318
  state.testCoverage = Number.parseInt(result, 10);
418
319
  }
419
320
  if (choice === "coverageSummaryPath") {
420
- const result = await clack2.text({
321
+ const result = await clack.text({
421
322
  message: "Coverage summary path (relative to package root)?",
422
323
  initialValue: state.coverageSummaryPath,
423
324
  validate: (v) => {
@@ -428,7 +329,7 @@ async function promptTestingMenu(state) {
428
329
  state.coverageSummaryPath = result.trim();
429
330
  }
430
331
  if (choice === "coverageCommand") {
431
- const result = await clack2.text({
332
+ const result = await clack.text({
432
333
  message: "Coverage command (blank to auto-detect from package.json)?",
433
334
  initialValue: state.coverageCommand ?? "",
434
335
  placeholder: "(auto-detect from package.json test runner)"
@@ -439,29 +340,6 @@ async function promptTestingMenu(state) {
439
340
  }
440
341
  }
441
342
  }
442
- var clack2, FILE_NAMING_OPTIONS, COMPONENT_NAMING_OPTIONS, HOOK_NAMING_OPTIONS;
443
- var init_prompt_submenus = __esm({
444
- "src/utils/prompt-submenus.ts"() {
445
- "use strict";
446
- clack2 = __toESM(require("@clack/prompts"), 1);
447
- init_prompt();
448
- init_prompt_constants();
449
- FILE_NAMING_OPTIONS = [
450
- { value: "kebab-case", label: "kebab-case" },
451
- { value: "camelCase", label: "camelCase" },
452
- { value: "PascalCase", label: "PascalCase" },
453
- { value: "snake_case", label: "snake_case" }
454
- ];
455
- COMPONENT_NAMING_OPTIONS = [
456
- { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
457
- { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
458
- ];
459
- HOOK_NAMING_OPTIONS = [
460
- { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
461
- { value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
462
- ];
463
- }
464
- });
465
343
 
466
344
  // src/utils/prompt-package-overrides.ts
467
345
  function normalizePackageOverrides(packages) {
@@ -506,7 +384,7 @@ async function promptPackageOverrides(packages, defaults) {
506
384
  const editablePackages = packages.filter((pkg) => pkg.path !== ".");
507
385
  if (editablePackages.length === 0) return packages;
508
386
  while (true) {
509
- const selectedPath = await clack3.select({
387
+ const selectedPath = await clack2.select({
510
388
  message: "Select package to edit overrides",
511
389
  options: [
512
390
  ...editablePackages.map((pkg) => ({
@@ -537,7 +415,7 @@ async function promptSinglePackageOverrides(target, defaults) {
537
415
  const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
538
416
  const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
539
417
  const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
540
- const choice = await clack3.select({
418
+ const choice = await clack2.select({
541
419
  message: `Edit overrides for ${target.path}`,
542
420
  options: [
543
421
  { value: "fileNaming", label: "File naming", hint: namingHint },
@@ -552,7 +430,7 @@ async function promptSinglePackageOverrides(target, defaults) {
552
430
  assertNotCancelled(choice);
553
431
  if (choice === "back") break;
554
432
  if (choice === "fileNaming") {
555
- const selected = await clack3.select({
433
+ const selected = await clack2.select({
556
434
  message: `File naming for ${target.path}`,
557
435
  options: [
558
436
  ...FILE_NAMING_OPTIONS,
@@ -574,7 +452,7 @@ async function promptSinglePackageOverrides(target, defaults) {
574
452
  }
575
453
  }
576
454
  if (choice === "maxFileLines") {
577
- const result = await clack3.text({
455
+ const result = await clack2.text({
578
456
  message: `Max file lines for ${target.path} (blank to inherit default)?`,
579
457
  initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
580
458
  placeholder: String(defaults.maxFileLines)
@@ -588,7 +466,7 @@ async function promptSinglePackageOverrides(target, defaults) {
588
466
  }
589
467
  }
590
468
  if (choice === "testCoverage") {
591
- const result = await clack3.text({
469
+ const result = await clack2.text({
592
470
  message: "Package testCoverage (0 to exempt package)?",
593
471
  initialValue: String(effectiveCoverage),
594
472
  validate: (v) => {
@@ -606,7 +484,7 @@ async function promptSinglePackageOverrides(target, defaults) {
606
484
  }
607
485
  }
608
486
  if (choice === "summaryPath") {
609
- const result = await clack3.text({
487
+ const result = await clack2.text({
610
488
  message: "Path to coverage summary file (blank to inherit default)?",
611
489
  initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
612
490
  placeholder: defaults.coverageSummaryPath
@@ -620,7 +498,7 @@ async function promptSinglePackageOverrides(target, defaults) {
620
498
  }
621
499
  }
622
500
  if (choice === "command") {
623
- const result = await clack3.text({
501
+ const result = await clack2.text({
624
502
  message: "Coverage command (blank to auto-detect)?",
625
503
  initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
626
504
  placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
@@ -644,16 +522,6 @@ async function promptSinglePackageOverrides(target, defaults) {
644
522
  }
645
523
  }
646
524
  }
647
- var clack3;
648
- var init_prompt_package_overrides = __esm({
649
- "src/utils/prompt-package-overrides.ts"() {
650
- "use strict";
651
- clack3 = __toESM(require("@clack/prompts"), 1);
652
- init_prompt();
653
- init_prompt_constants();
654
- init_prompt_submenus();
655
- }
656
- });
657
525
 
658
526
  // src/utils/prompt-menu-handlers.ts
659
527
  function getPackageDiffs(pkg, root) {
@@ -695,11 +563,11 @@ function getPackageDiffs(pkg, root) {
695
563
  return diffs;
696
564
  }
697
565
  function buildMenuOptions(state, packageCount) {
698
- const fileLimitsHint = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
566
+ const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
699
567
  const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
700
568
  const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
701
569
  const options = [
702
- { value: "fileLimits", label: "File limits", hint: fileLimitsHint },
570
+ { value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
703
571
  { value: "naming", label: "Naming & conventions", hint: namingHint },
704
572
  { value: "testing", label: "Testing & coverage", hint: testingHint }
705
573
  ];
@@ -733,7 +601,7 @@ async function handleMenuChoice(choice, state, defaults, root) {
733
601
  state.coverageSummaryPath = defaults.coverageSummaryPath;
734
602
  state.coverageCommand = defaults.coverageCommand;
735
603
  state.packageOverrides = clonePackages(defaults.packageOverrides);
736
- clack4.log.info("Reset all rules to detected defaults.");
604
+ clack3.log.info("Reset all rules to detected defaults.");
737
605
  return;
738
606
  }
739
607
  if (choice === "fileLimits") {
@@ -761,21 +629,12 @@ async function handleMenuChoice(choice, state, defaults, root) {
761
629
  const lines = packageDiffs.map((entry) => `${entry.pkg.path}
762
630
  ${entry.diffs.join(", ")}`);
763
631
  if (lines.length > 0) {
764
- clack4.note(lines.join("\n\n"), "Existing package differences");
632
+ clack3.note(lines.join("\n\n"), "Existing package differences");
765
633
  }
766
634
  }
767
635
  return;
768
636
  }
769
637
  }
770
- var clack4;
771
- var init_prompt_menu_handlers = __esm({
772
- "src/utils/prompt-menu-handlers.ts"() {
773
- "use strict";
774
- clack4 = __toESM(require("@clack/prompts"), 1);
775
- init_prompt_package_overrides();
776
- init_prompt_submenus();
777
- }
778
- });
779
638
 
780
639
  // src/utils/prompt-rules.ts
781
640
  async function promptRuleMenu(defaults) {
@@ -787,7 +646,7 @@ async function promptRuleMenu(defaults) {
787
646
  const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
788
647
  while (true) {
789
648
  const options = buildMenuOptions(state, packageCount);
790
- const choice = await clack5.select({ message: "Customize rules", options });
649
+ const choice = await clack4.select({ message: "Customize rules", options });
791
650
  assertNotCancelled(choice);
792
651
  if (choice === "done") break;
793
652
  await handleMenuChoice(choice, state, defaults, root);
@@ -807,36 +666,26 @@ async function promptRuleMenu(defaults) {
807
666
  packageOverrides: state.packageOverrides
808
667
  };
809
668
  }
810
- var clack5;
811
- var init_prompt_rules = __esm({
812
- "src/utils/prompt-rules.ts"() {
813
- "use strict";
814
- clack5 = __toESM(require("@clack/prompts"), 1);
815
- init_get_root_package();
816
- init_prompt();
817
- init_prompt_menu_handlers();
818
- }
819
- });
820
669
 
821
670
  // src/utils/prompt.ts
822
671
  function assertNotCancelled(value) {
823
- if (clack6.isCancel(value)) {
824
- clack6.cancel("Setup cancelled.");
672
+ if (clack5.isCancel(value)) {
673
+ clack5.cancel("Setup cancelled.");
825
674
  process.exit(0);
826
675
  }
827
676
  }
828
677
  async function confirm3(message) {
829
- const result = await clack6.confirm({ message, initialValue: true });
678
+ const result = await clack5.confirm({ message, initialValue: true });
830
679
  assertNotCancelled(result);
831
680
  return result;
832
681
  }
833
682
  async function confirmDangerous(message) {
834
- const result = await clack6.confirm({ message, initialValue: false });
683
+ const result = await clack5.confirm({ message, initialValue: false });
835
684
  assertNotCancelled(result);
836
685
  return result;
837
686
  }
838
687
  async function promptExistingConfigAction(configFile) {
839
- const result = await clack6.select({
688
+ const result = await clack5.select({
840
689
  message: `${configFile} already exists. What do you want to do?`,
841
690
  options: [
842
691
  {
@@ -859,134 +708,6 @@ async function promptExistingConfigAction(configFile) {
859
708
  assertNotCancelled(result);
860
709
  return result;
861
710
  }
862
- async function promptInitDecision() {
863
- const result = await clack6.select({
864
- message: "How do you want to proceed?",
865
- options: [
866
- {
867
- value: "accept",
868
- label: "Accept defaults",
869
- hint: "writes the config with these defaults; use --enforce in CI to block"
870
- },
871
- {
872
- value: "customize",
873
- label: "Customize rules",
874
- hint: "edit limits, naming, test coverage, and package overrides"
875
- },
876
- {
877
- value: "review",
878
- label: "Review detected details",
879
- hint: "show the full scan report with package and structure details"
880
- }
881
- ]
882
- });
883
- assertNotCancelled(result);
884
- return result;
885
- }
886
- var clack6;
887
- var init_prompt = __esm({
888
- "src/utils/prompt.ts"() {
889
- "use strict";
890
- clack6 = __toESM(require("@clack/prompts"), 1);
891
- init_prompt_integrations();
892
- init_prompt_rules();
893
- }
894
- });
895
-
896
- // src/utils/prompt-naming-default.ts
897
- var prompt_naming_default_exports = {};
898
- __export(prompt_naming_default_exports, {
899
- resolveNamingDefault: () => resolveNamingDefault
900
- });
901
- async function resolveNamingDefault(config, scanResult) {
902
- const rootPkg = getRootPackage(config.packages);
903
- if (!config.rules.enforceNaming || rootPkg?.conventions?.fileNaming) return false;
904
- const isMonorepo = config.packages.length > 1;
905
- const pkgNamingData = isMonorepo ? scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
906
- path: p.relativePath,
907
- naming: p.conventions.fileNaming
908
- })) : [];
909
- const chosen = await promptNamingDefault(pkgNamingData, isMonorepo);
910
- if (chosen === SENTINEL_SKIP) {
911
- config.rules.enforceNaming = false;
912
- } else if (rootPkg) {
913
- rootPkg.conventions = rootPkg.conventions ?? {};
914
- rootPkg.conventions.fileNaming = chosen;
915
- }
916
- return true;
917
- }
918
- async function promptNamingDefault(pkgNamingData, isMonorepo) {
919
- if (isMonorepo && pkgNamingData.length > 0) {
920
- const lines = pkgNamingData.map(
921
- (p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
922
- );
923
- clack10.note(lines.join("\n"), "Per-package file naming detected");
924
- }
925
- const message = isMonorepo ? "Which convention should be the default? You can override per-package later." : "Which file naming convention should be used?";
926
- const options = FILE_NAMING_OPTIONS.map((opt) => {
927
- if (isMonorepo && pkgNamingData.length > 0) {
928
- const count = pkgNamingData.filter((p) => p.naming.value === opt.value).length;
929
- return {
930
- value: opt.value,
931
- label: opt.label,
932
- hint: count > 0 ? `${count} package${count > 1 ? "s" : ""}` : void 0
933
- };
934
- }
935
- return { value: opt.value, label: opt.label };
936
- });
937
- const selected = await clack10.select({
938
- message,
939
- options: [...options, { value: SENTINEL_SKIP, label: "Don't enforce naming" }]
940
- });
941
- assertNotCancelled(selected);
942
- return selected;
943
- }
944
- var clack10;
945
- var init_prompt_naming_default = __esm({
946
- "src/utils/prompt-naming-default.ts"() {
947
- "use strict";
948
- clack10 = __toESM(require("@clack/prompts"), 1);
949
- init_get_root_package();
950
- init_prompt();
951
- init_prompt_constants();
952
- init_prompt_submenus();
953
- }
954
- });
955
-
956
- // src/index.ts
957
- var index_exports = {};
958
- __export(index_exports, {
959
- VERSION: () => VERSION
960
- });
961
- module.exports = __toCommonJS(index_exports);
962
- var import_chalk16 = __toESM(require("chalk"), 1);
963
- var import_commander = require("commander");
964
-
965
- // src/commands/boundaries.ts
966
- var fs3 = __toESM(require("fs"), 1);
967
- var path3 = __toESM(require("path"), 1);
968
- var import_config = require("@viberails/config");
969
- var import_chalk = __toESM(require("chalk"), 1);
970
-
971
- // src/utils/find-project-root.ts
972
- var fs = __toESM(require("fs"), 1);
973
- var path = __toESM(require("path"), 1);
974
- function findProjectRoot(startDir) {
975
- let dir = path.resolve(startDir);
976
- while (true) {
977
- if (fs.existsSync(path.join(dir, "package.json"))) {
978
- return dir;
979
- }
980
- const parent = path.dirname(dir);
981
- if (parent === dir) {
982
- return null;
983
- }
984
- dir = parent;
985
- }
986
- }
987
-
988
- // src/commands/boundaries.ts
989
- init_prompt();
990
711
 
991
712
  // src/utils/resolve-workspace-packages.ts
992
713
  var fs2 = __toESM(require("fs"), 1);
@@ -1179,7 +900,7 @@ function resolveIgnoreForFile(relPath, config) {
1179
900
  }
1180
901
 
1181
902
  // src/commands/check-coverage.ts
1182
- var import_node_child_process2 = require("child_process");
903
+ var import_node_child_process = require("child_process");
1183
904
  var fs4 = __toESM(require("fs"), 1);
1184
905
  var path4 = __toESM(require("path"), 1);
1185
906
  var import_config3 = require("@viberails/config");
@@ -1222,7 +943,7 @@ function readCoveragePercentage(summaryPath) {
1222
943
  }
1223
944
  }
1224
945
  function runCoverageCommand(pkgRoot, command) {
1225
- const result = (0, import_node_child_process2.spawnSync)(command, {
946
+ const result = (0, import_node_child_process.spawnSync)(command, {
1226
947
  cwd: pkgRoot,
1227
948
  shell: true,
1228
949
  encoding: "utf-8",
@@ -1317,7 +1038,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
1317
1038
  }
1318
1039
 
1319
1040
  // src/commands/check-files.ts
1320
- var import_node_child_process3 = require("child_process");
1041
+ var import_node_child_process2 = require("child_process");
1321
1042
  var fs5 = __toESM(require("fs"), 1);
1322
1043
  var path5 = __toESM(require("path"), 1);
1323
1044
  var import_config4 = require("@viberails/config");
@@ -1390,7 +1111,7 @@ function checkNaming(relPath, conventions) {
1390
1111
  }
1391
1112
  function getStagedFiles(projectRoot) {
1392
1113
  try {
1393
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1114
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1394
1115
  cwd: projectRoot,
1395
1116
  encoding: "utf-8",
1396
1117
  stdio: ["ignore", "pipe", "ignore"]
@@ -1402,12 +1123,12 @@ function getStagedFiles(projectRoot) {
1402
1123
  }
1403
1124
  function getDiffFiles(projectRoot, base) {
1404
1125
  try {
1405
- const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1126
+ const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1406
1127
  cwd: projectRoot,
1407
1128
  encoding: "utf-8",
1408
1129
  stdio: ["ignore", "pipe", "ignore"]
1409
1130
  });
1410
- const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1131
+ const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1411
1132
  cwd: projectRoot,
1412
1133
  encoding: "utf-8",
1413
1134
  stdio: ["ignore", "pipe", "ignore"]
@@ -1453,7 +1174,7 @@ function deletedTestFileToSourceFile(deletedTestFile, config) {
1453
1174
  }
1454
1175
  function getStagedDeletedTestSourceFiles(projectRoot, config) {
1455
1176
  try {
1456
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=D", {
1177
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=D", {
1457
1178
  cwd: projectRoot,
1458
1179
  encoding: "utf-8",
1459
1180
  stdio: ["ignore", "pipe", "ignore"]
@@ -1465,7 +1186,7 @@ function getStagedDeletedTestSourceFiles(projectRoot, config) {
1465
1186
  }
1466
1187
  function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
1467
1188
  try {
1468
- const output = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1189
+ const output = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1469
1190
  cwd: projectRoot,
1470
1191
  encoding: "utf-8",
1471
1192
  stdio: ["ignore", "pipe", "ignore"]
@@ -1691,9 +1412,9 @@ async function checkCommand(options, cwd) {
1691
1412
  }
1692
1413
  const violations = [];
1693
1414
  const severity = options.enforce ? "error" : "warn";
1694
- const log7 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1415
+ const log8 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1695
1416
  };
1696
- log7(" Checking files...");
1417
+ log8(" Checking files...");
1697
1418
  for (const file of filesToCheck) {
1698
1419
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1699
1420
  const relPath = path7.relative(projectRoot, absPath);
@@ -1726,9 +1447,9 @@ async function checkCommand(options, cwd) {
1726
1447
  }
1727
1448
  }
1728
1449
  }
1729
- log7(" done\n");
1450
+ log8(" done\n");
1730
1451
  if (!options.files) {
1731
- log7(" Checking missing tests...");
1452
+ log8(" Checking missing tests...");
1732
1453
  const testViolations = checkMissingTests(projectRoot, config, severity);
1733
1454
  if (options.staged) {
1734
1455
  const stagedSet = new Set(filesToCheck);
@@ -1741,14 +1462,14 @@ async function checkCommand(options, cwd) {
1741
1462
  } else {
1742
1463
  violations.push(...testViolations);
1743
1464
  }
1744
- log7(" done\n");
1465
+ log8(" done\n");
1745
1466
  }
1746
1467
  if (!options.files && !options.staged && !options.diffBase) {
1747
- log7(" Running test coverage...\n");
1468
+ log8(" Running test coverage...\n");
1748
1469
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1749
1470
  staged: options.staged,
1750
1471
  enforce: options.enforce,
1751
- onProgress: (pkg) => log7(` Coverage: ${pkg}...
1472
+ onProgress: (pkg) => log8(` Coverage: ${pkg}...
1752
1473
  `)
1753
1474
  });
1754
1475
  violations.push(...coverageViolations);
@@ -1773,7 +1494,7 @@ async function checkCommand(options, cwd) {
1773
1494
  severity
1774
1495
  });
1775
1496
  }
1776
- log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1497
+ log8(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1777
1498
  `);
1778
1499
  }
1779
1500
  if (options.format === "json") {
@@ -1849,7 +1570,7 @@ async function hookCheckCommand(cwd) {
1849
1570
  // src/commands/config.ts
1850
1571
  var fs10 = __toESM(require("fs"), 1);
1851
1572
  var path9 = __toESM(require("path"), 1);
1852
- var clack7 = __toESM(require("@clack/prompts"), 1);
1573
+ var clack6 = __toESM(require("@clack/prompts"), 1);
1853
1574
  var import_config6 = require("@viberails/config");
1854
1575
  var import_scanner = require("@viberails/scanner");
1855
1576
  var import_chalk6 = __toESM(require("chalk"), 1);
@@ -2273,7 +1994,6 @@ function formatScanResultsText(scanResult) {
2273
1994
  }
2274
1995
 
2275
1996
  // src/utils/apply-rule-overrides.ts
2276
- init_get_root_package();
2277
1997
  function applyRuleOverrides(config, overrides) {
2278
1998
  if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
2279
1999
  const rootPkg = getRootPackage(config.packages);
@@ -2444,9 +2164,6 @@ function formatStatsDelta(oldStats, newStats) {
2444
2164
  return `${parts.join(", ")} since last sync`;
2445
2165
  }
2446
2166
 
2447
- // src/commands/config.ts
2448
- init_prompt();
2449
-
2450
2167
  // src/utils/write-generated-files.ts
2451
2168
  var fs9 = __toESM(require("fs"), 1);
2452
2169
  var path8 = __toESM(require("path"), 1);
@@ -2486,11 +2203,11 @@ async function configCommand(options, cwd) {
2486
2203
  return;
2487
2204
  }
2488
2205
  if (!options.suppressIntro) {
2489
- clack7.intro("viberails config");
2206
+ clack6.intro("viberails config");
2490
2207
  }
2491
2208
  const config = await (0, import_config6.loadConfig)(configPath);
2492
2209
  let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2493
- clack7.note(formatRulesText(config).join("\n"), "Current rules");
2210
+ clack6.note(formatRulesText(config).join("\n"), "Current rules");
2494
2211
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2495
2212
  const overrides = await promptRuleMenu({
2496
2213
  maxFileLines: config.rules.maxFileLines,
@@ -2510,7 +2227,7 @@ async function configCommand(options, cwd) {
2510
2227
  if (options.rescan && config.packages.length > 1) {
2511
2228
  const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
2512
2229
  if (shouldInfer) {
2513
- const bs = clack7.spinner();
2230
+ const bs = clack6.spinner();
2514
2231
  bs.start("Building import graph...");
2515
2232
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2516
2233
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -2528,29 +2245,29 @@ async function configCommand(options, cwd) {
2528
2245
  }
2529
2246
  const shouldWrite = await confirm3("Save updated configuration?");
2530
2247
  if (!shouldWrite) {
2531
- clack7.outro("No changes written.");
2248
+ clack6.outro("No changes written.");
2532
2249
  return;
2533
2250
  }
2534
2251
  const compacted = (0, import_config6.compactConfig)(config);
2535
2252
  fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2536
2253
  `);
2537
2254
  if (!scanResult) {
2538
- const s = clack7.spinner();
2255
+ const s = clack6.spinner();
2539
2256
  s.start("Scanning for context generation...");
2540
2257
  scanResult = await (0, import_scanner.scan)(projectRoot);
2541
2258
  s.stop("Scan complete");
2542
2259
  }
2543
2260
  writeGeneratedFiles(projectRoot, config, scanResult);
2544
- clack7.log.success(
2261
+ clack6.log.success(
2545
2262
  `Updated:
2546
2263
  ${CONFIG_FILE3}
2547
2264
  .viberails/context.md
2548
2265
  .viberails/scan-result.json`
2549
2266
  );
2550
- clack7.outro("Done! Run viberails check to verify.");
2267
+ clack6.outro("Done! Run viberails check to verify.");
2551
2268
  }
2552
2269
  async function rescanAndMerge(projectRoot, config) {
2553
- const s = clack7.spinner();
2270
+ const s = clack6.spinner();
2554
2271
  s.start("Re-scanning project...");
2555
2272
  const scanResult = await (0, import_scanner.scan)(projectRoot);
2556
2273
  const merged = (0, import_config6.mergeConfig)(config, scanResult);
@@ -2561,9 +2278,9 @@ async function rescanAndMerge(projectRoot, config) {
2561
2278
  const icon = c.type === "removed" ? "-" : "+";
2562
2279
  return `${icon} ${c.description}`;
2563
2280
  }).join("\n");
2564
- clack7.note(changeLines, "Changes detected");
2281
+ clack6.note(changeLines, "Changes detected");
2565
2282
  } else {
2566
- clack7.log.info("No new changes detected from scan.");
2283
+ clack6.log.info("No new changes detected from scan.");
2567
2284
  }
2568
2285
  Object.assign(config, merged);
2569
2286
  return scanResult;
@@ -2574,10 +2291,9 @@ var fs13 = __toESM(require("fs"), 1);
2574
2291
  var path13 = __toESM(require("path"), 1);
2575
2292
  var import_config7 = require("@viberails/config");
2576
2293
  var import_chalk8 = __toESM(require("chalk"), 1);
2577
- init_prompt();
2578
2294
 
2579
2295
  // src/commands/fix-helpers.ts
2580
- var import_node_child_process4 = require("child_process");
2296
+ var import_node_child_process3 = require("child_process");
2581
2297
  var import_chalk7 = __toESM(require("chalk"), 1);
2582
2298
  function printPlan(renames, stubs) {
2583
2299
  if (renames.length > 0) {
@@ -2595,7 +2311,7 @@ function printPlan(renames, stubs) {
2595
2311
  }
2596
2312
  function checkGitDirty(projectRoot) {
2597
2313
  try {
2598
- const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
2314
+ const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
2599
2315
  cwd: projectRoot,
2600
2316
  encoding: "utf-8",
2601
2317
  stdio: ["ignore", "pipe", "ignore"]
@@ -3058,161 +2774,42 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
3058
2774
  }
3059
2775
 
3060
2776
  // src/commands/init.ts
3061
- var fs20 = __toESM(require("fs"), 1);
3062
- var path20 = __toESM(require("path"), 1);
3063
- var clack11 = __toESM(require("@clack/prompts"), 1);
2777
+ var fs21 = __toESM(require("fs"), 1);
2778
+ var path21 = __toESM(require("path"), 1);
2779
+ var clack12 = __toESM(require("@clack/prompts"), 1);
3064
2780
  var import_config9 = require("@viberails/config");
3065
2781
  var import_scanner3 = require("@viberails/scanner");
3066
- var import_chalk14 = __toESM(require("chalk"), 1);
2782
+ var import_chalk13 = __toESM(require("chalk"), 1);
3067
2783
 
3068
- // src/display-init.ts
3069
- var import_types6 = require("@viberails/types");
2784
+ // src/utils/check-prerequisites.ts
2785
+ var fs14 = __toESM(require("fs"), 1);
2786
+ var path14 = __toESM(require("path"), 1);
2787
+ var clack7 = __toESM(require("@clack/prompts"), 1);
3070
2788
  var import_chalk9 = __toESM(require("chalk"), 1);
3071
- var INIT_OVERVIEW_NAMES = {
3072
- typescript: "TypeScript",
3073
- javascript: "JavaScript",
3074
- eslint: "ESLint",
3075
- prettier: "Prettier",
3076
- jest: "Jest",
3077
- vitest: "Vitest",
3078
- biome: "Biome"
3079
- };
3080
- function formatDetectedOverview(scanResult) {
3081
- const { stack } = scanResult;
3082
- const primaryParts = [];
3083
- const secondaryParts = [];
3084
- const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
3085
- if (scanResult.packages.length > 1) {
3086
- primaryParts.push("monorepo");
3087
- primaryParts.push(`${scanResult.packages.length} packages`);
3088
- } else if (stack.framework) {
3089
- primaryParts.push(formatItem(stack.framework, import_types6.FRAMEWORK_NAMES));
3090
- } else {
3091
- primaryParts.push("single package");
3092
- }
3093
- primaryParts.push(formatOverviewItem(stack.language));
3094
- if (stack.styling) {
3095
- primaryParts.push(formatOverviewItem(stack.styling, import_types6.STYLING_NAMES));
3096
- }
3097
- if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
3098
- if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
3099
- if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
3100
- if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
3101
- const primary = primaryParts.map((part) => import_chalk9.default.cyan(part)).join(import_chalk9.default.dim(" \xB7 "));
3102
- const secondary = secondaryParts.join(import_chalk9.default.dim(" \xB7 "));
3103
- return secondary ? `${primary}
3104
- ${import_chalk9.default.dim(secondary)}` : primary;
3105
- }
3106
- function displayInitOverview(scanResult, config, exemptedPackages) {
3107
- const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
3108
- const isMonorepo = config.packages.length > 1;
3109
- const ok = import_chalk9.default.green("\u2713");
3110
- const info = import_chalk9.default.yellow("~");
3111
- console.log("");
3112
- console.log(` ${import_chalk9.default.bold("Ready to initialize:")}`);
3113
- console.log(` ${formatDetectedOverview(scanResult)}`);
3114
- console.log("");
3115
- console.log(` ${import_chalk9.default.bold("Rules to apply:")}`);
3116
- console.log(` ${ok} Max file size: ${import_chalk9.default.cyan(`${config.rules.maxFileLines} lines`)}`);
3117
- const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
3118
- if (config.rules.enforceNaming && fileNaming) {
3119
- console.log(` ${ok} File naming: ${import_chalk9.default.cyan(fileNaming)}`);
3120
- } else {
3121
- console.log(` ${info} File naming: ${import_chalk9.default.dim("not enforced")}`);
3122
- }
3123
- const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
3124
- if (config.rules.enforceMissingTests && testPattern) {
3125
- console.log(` ${ok} Missing tests: ${import_chalk9.default.cyan(`enforced (${testPattern})`)}`);
3126
- } else if (config.rules.enforceMissingTests) {
3127
- console.log(` ${ok} Missing tests: ${import_chalk9.default.cyan("enforced")}`);
3128
- } else {
3129
- console.log(` ${info} Missing tests: ${import_chalk9.default.dim("not enforced")}`);
3130
- }
3131
- if (config.rules.testCoverage > 0) {
3132
- if (isMonorepo) {
3133
- const withCoverage = config.packages.filter(
3134
- (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3135
- );
3136
- console.log(
3137
- ` ${ok} Coverage: ${import_chalk9.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk9.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
3138
- );
3139
- } else {
3140
- console.log(` ${ok} Coverage: ${import_chalk9.default.cyan(`${config.rules.testCoverage}%`)}`);
3141
- }
3142
- } else {
3143
- console.log(` ${info} Coverage: ${import_chalk9.default.dim("disabled")}`);
3144
- }
3145
- if (exemptedPackages.length > 0) {
3146
- console.log(
3147
- ` ${import_chalk9.default.dim(" exempted:")} ${import_chalk9.default.dim(exemptedPackages.join(", "))} ${import_chalk9.default.dim("(types-only)")}`
3148
- );
3149
- }
3150
- console.log("");
3151
- console.log(` ${import_chalk9.default.bold("Also available:")}`);
3152
- if (isMonorepo) {
3153
- console.log(` ${info} Infer boundaries from current imports`);
3154
- }
3155
- console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
3156
- console.log(
3157
- `
3158
- ${import_chalk9.default.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
3159
- );
3160
- console.log("");
3161
- }
3162
- function summarizeSelectedIntegrations(integrations, opts) {
3163
- const lines = [];
3164
- if (opts.hasBoundaries) {
3165
- lines.push("\u2713 Boundary rules: inferred from current imports");
3166
- } else {
3167
- lines.push("~ Boundary rules: not enabled");
3168
- }
3169
- if (opts.hasCoverage) {
3170
- lines.push("\u2713 Coverage checks: enabled");
3171
- } else {
3172
- lines.push("~ Coverage checks: disabled");
3173
- }
3174
- const selectedIntegrations = [
3175
- integrations.preCommitHook ? "pre-commit hook" : void 0,
3176
- integrations.typecheckHook ? "typecheck" : void 0,
3177
- integrations.lintHook ? "lint check" : void 0,
3178
- integrations.claudeCodeHook ? "Claude Code hook" : void 0,
3179
- integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
3180
- integrations.githubAction ? "GitHub Actions workflow" : void 0
3181
- ].filter(Boolean);
3182
- if (selectedIntegrations.length > 0) {
3183
- lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
3184
- } else {
3185
- lines.push("~ Integrations: none selected");
3186
- }
3187
- return lines;
3188
- }
3189
- function displaySetupPlan(config, integrations, opts = {}) {
3190
- const configFile = opts.configFile ?? "viberails.config.json";
3191
- const lines = summarizeSelectedIntegrations(integrations, {
3192
- hasBoundaries: config.rules.enforceBoundaries,
3193
- hasCoverage: config.rules.testCoverage > 0
2789
+
2790
+ // src/utils/spawn-async.ts
2791
+ var import_node_child_process4 = require("child_process");
2792
+ function spawnAsync(command, cwd) {
2793
+ return new Promise((resolve4) => {
2794
+ const child = (0, import_node_child_process4.spawn)(command, { cwd, shell: true, stdio: "pipe" });
2795
+ let stdout = "";
2796
+ let stderr = "";
2797
+ child.stdout.on("data", (d) => {
2798
+ stdout += d.toString();
2799
+ });
2800
+ child.stderr.on("data", (d) => {
2801
+ stderr += d.toString();
2802
+ });
2803
+ child.on("close", (status) => {
2804
+ resolve4({ status, stdout, stderr });
2805
+ });
2806
+ child.on("error", () => {
2807
+ resolve4({ status: 1, stdout, stderr });
2808
+ });
3194
2809
  });
3195
- console.log("");
3196
- console.log(` ${import_chalk9.default.bold("Ready to write:")}`);
3197
- console.log(
3198
- ` ${opts.replacingExistingConfig ? import_chalk9.default.yellow("!") : import_chalk9.default.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? import_chalk9.default.dim(" (replacing existing config)") : ""}`
3199
- );
3200
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
3201
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
3202
- for (const line of lines) {
3203
- const icon = line.startsWith("\u2713") ? import_chalk9.default.green("\u2713") : import_chalk9.default.yellow("~");
3204
- console.log(` ${icon} ${line.slice(2)}`);
3205
- }
3206
- console.log("");
3207
2810
  }
3208
2811
 
3209
2812
  // src/utils/check-prerequisites.ts
3210
- var fs14 = __toESM(require("fs"), 1);
3211
- var path14 = __toESM(require("path"), 1);
3212
- var clack8 = __toESM(require("@clack/prompts"), 1);
3213
- var import_chalk10 = __toESM(require("chalk"), 1);
3214
- init_prompt();
3215
- init_spawn_async();
3216
2813
  function checkCoveragePrereqs(projectRoot, scanResult) {
3217
2814
  const pm = scanResult.stack.packageManager.name;
3218
2815
  const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
@@ -3243,116 +2840,535 @@ function displayMissingPrereqs(prereqs) {
3243
2840
  const missing = prereqs.filter((p) => !p.installed);
3244
2841
  for (const m of missing) {
3245
2842
  const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
3246
- console.log(` ${import_chalk10.default.yellow("!")} ${m.label} not installed${suffix}`);
2843
+ console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
3247
2844
  if (m.installCommand) {
3248
- console.log(` Install: ${import_chalk10.default.cyan(m.installCommand)}`);
2845
+ console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
3249
2846
  }
3250
2847
  }
3251
2848
  }
3252
- async function promptMissingPrereqs(projectRoot, prereqs) {
3253
- const missing = prereqs.filter((p) => !p.installed);
3254
- if (missing.length === 0) return { disableCoverage: false };
3255
- const prereqLines = prereqs.map((p) => {
3256
- if (p.installed) return `\u2713 ${p.label}`;
3257
- const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
3258
- return `\u2717 ${p.label} \u2014 ${detail}`;
3259
- }).join("\n");
3260
- clack8.note(prereqLines, "Coverage support");
3261
- let disableCoverage = false;
3262
- for (const m of missing) {
3263
- if (!m.installCommand) continue;
3264
- const pkgCount = m.affectedPackages?.length;
3265
- const message = pkgCount ? `${m.label} is not installed. Required for coverage in ${pkgCount} packages using vitest.` : `${m.label} is not installed. It is required for coverage percentage checks.`;
3266
- const choice = await clack8.select({
3267
- message,
2849
+ function planCoverageInstall(prereqs) {
2850
+ const missing = prereqs.find((p) => !p.installed && p.installCommand);
2851
+ if (!missing?.installCommand) return void 0;
2852
+ return {
2853
+ label: missing.label,
2854
+ command: missing.installCommand
2855
+ };
2856
+ }
2857
+ function hasDependency(projectRoot, name) {
2858
+ try {
2859
+ const pkgPath = path14.join(projectRoot, "package.json");
2860
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2861
+ return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2862
+ } catch {
2863
+ return false;
2864
+ }
2865
+ }
2866
+
2867
+ // src/utils/deferred-install.ts
2868
+ var clack8 = __toESM(require("@clack/prompts"), 1);
2869
+ async function executeDeferredInstalls(projectRoot, installs) {
2870
+ if (installs.length === 0) return 0;
2871
+ let successCount = 0;
2872
+ for (const install of installs) {
2873
+ const s = clack8.spinner();
2874
+ s.start(`Installing ${install.label}...`);
2875
+ const result = await spawnAsync(install.command, projectRoot);
2876
+ if (result.status === 0) {
2877
+ s.stop(`Installed ${install.label}`);
2878
+ install.onSuccess?.();
2879
+ successCount++;
2880
+ } else {
2881
+ s.stop(`Failed to install ${install.label}`);
2882
+ clack8.log.warn(`Install manually: ${install.command}`);
2883
+ install.onFailure?.();
2884
+ }
2885
+ }
2886
+ return successCount;
2887
+ }
2888
+
2889
+ // src/utils/prompt-main-menu.ts
2890
+ var clack10 = __toESM(require("@clack/prompts"), 1);
2891
+
2892
+ // src/utils/prompt-integrations.ts
2893
+ var fs15 = __toESM(require("fs"), 1);
2894
+ var path15 = __toESM(require("path"), 1);
2895
+ var clack9 = __toESM(require("@clack/prompts"), 1);
2896
+ function buildLefthookInstallCommand(pm, isWorkspace) {
2897
+ if (pm === "yarn") return "yarn add -D lefthook";
2898
+ if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
2899
+ if (pm === "npm") return "npm install -D lefthook";
2900
+ return `${pm} add -D lefthook`;
2901
+ }
2902
+ async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
2903
+ const options = [];
2904
+ const needsLefthook = !hookManager;
2905
+ if (needsLefthook) {
2906
+ const pm = packageManager ?? "npm";
2907
+ options.push({
2908
+ value: "installLefthook",
2909
+ label: "Install Lefthook",
2910
+ hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
2911
+ });
2912
+ }
2913
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
2914
+ const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
2915
+ options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
2916
+ if (tools?.isTypeScript) {
2917
+ options.push({
2918
+ value: "typecheck",
2919
+ label: "Typecheck (tsc --noEmit)",
2920
+ hint: "pre-commit hook + CI check"
2921
+ });
2922
+ }
2923
+ if (tools?.linter) {
2924
+ const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
2925
+ options.push({
2926
+ value: "lint",
2927
+ label: `Lint check (${linterName})`,
2928
+ hint: "pre-commit hook + CI check"
2929
+ });
2930
+ }
2931
+ options.push(
2932
+ {
2933
+ value: "claude",
2934
+ label: "Claude Code hook",
2935
+ hint: "checks files when Claude edits them"
2936
+ },
2937
+ {
2938
+ value: "claudeMd",
2939
+ label: "CLAUDE.md reference",
2940
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
2941
+ },
2942
+ {
2943
+ value: "githubAction",
2944
+ label: "GitHub Actions workflow",
2945
+ hint: "blocks PRs that fail viberails check"
2946
+ }
2947
+ );
2948
+ const initialValues = options.map((o) => o.value);
2949
+ const result = await clack9.multiselect({
2950
+ message: "Integrations",
2951
+ options,
2952
+ initialValues,
2953
+ required: false
2954
+ });
2955
+ assertNotCancelled(result);
2956
+ let lefthookInstall;
2957
+ if (needsLefthook && result.includes("installLefthook")) {
2958
+ const pm = packageManager ?? "npm";
2959
+ lefthookInstall = {
2960
+ label: "Lefthook",
2961
+ command: buildLefthookInstallCommand(pm, isWorkspace),
2962
+ onSuccess: projectRoot ? () => {
2963
+ const ymlPath = path15.join(projectRoot, "lefthook.yml");
2964
+ if (!fs15.existsSync(ymlPath)) {
2965
+ fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
2966
+ }
2967
+ } : void 0
2968
+ };
2969
+ }
2970
+ return {
2971
+ choice: {
2972
+ preCommitHook: result.includes("preCommit"),
2973
+ claudeCodeHook: result.includes("claude"),
2974
+ claudeMdRef: result.includes("claudeMd"),
2975
+ githubAction: result.includes("githubAction"),
2976
+ typecheckHook: result.includes("typecheck"),
2977
+ lintHook: result.includes("lint")
2978
+ },
2979
+ lefthookInstall
2980
+ };
2981
+ }
2982
+
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
3117
+ async function handleAdvancedNaming(config) {
3118
+ const rootPkg = getRootPackage(config.packages);
3119
+ const state = {
3120
+ maxFileLines: config.rules.maxFileLines,
3121
+ maxTestFileLines: config.rules.maxTestFileLines,
3122
+ testCoverage: config.rules.testCoverage,
3123
+ enforceMissingTests: config.rules.enforceMissingTests,
3124
+ enforceNaming: config.rules.enforceNaming,
3125
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3126
+ componentNaming: rootPkg.conventions?.componentNaming,
3127
+ hookNaming: rootPkg.conventions?.hookNaming,
3128
+ importAlias: rootPkg.conventions?.importAlias,
3129
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3130
+ coverageCommand: config.defaults?.coverage?.command
3131
+ };
3132
+ await promptNamingMenu(state);
3133
+ rootPkg.conventions = rootPkg.conventions ?? {};
3134
+ config.rules.enforceNaming = state.enforceNaming;
3135
+ if (state.fileNamingValue) {
3136
+ rootPkg.conventions.fileNaming = state.fileNamingValue;
3137
+ } else {
3138
+ delete rootPkg.conventions.fileNaming;
3139
+ }
3140
+ rootPkg.conventions.componentNaming = state.componentNaming || void 0;
3141
+ rootPkg.conventions.hookNaming = state.hookNaming || void 0;
3142
+ rootPkg.conventions.importAlias = state.importAlias || void 0;
3143
+ }
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
+ async function handleFileNaming(config, scanResult) {
3190
+ const isMonorepo = config.packages.length > 1;
3191
+ if (isMonorepo) {
3192
+ const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
3193
+ path: p.relativePath,
3194
+ naming: p.conventions.fileNaming
3195
+ }));
3196
+ if (pkgData.length > 0) {
3197
+ const lines = pkgData.map(
3198
+ (p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
3199
+ );
3200
+ clack10.note(lines.join("\n"), "Per-package file naming detected");
3201
+ }
3202
+ }
3203
+ const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
3204
+ if (isMonorepo) {
3205
+ const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
3206
+ const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
3207
+ return { value: opt.value, label: opt.label, hint };
3208
+ }
3209
+ return { value: opt.value, label: opt.label };
3210
+ });
3211
+ const rootPkg = getRootPackage(config.packages);
3212
+ const selected = await clack10.select({
3213
+ message: isMonorepo ? "Default file naming convention" : "File naming convention",
3214
+ options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
3215
+ initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
3216
+ });
3217
+ assertNotCancelled(selected);
3218
+ if (selected === SENTINEL_SKIP) {
3219
+ config.rules.enforceNaming = false;
3220
+ if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
3221
+ } else {
3222
+ config.rules.enforceNaming = true;
3223
+ rootPkg.conventions = rootPkg.conventions ?? {};
3224
+ rootPkg.conventions.fileNaming = selected;
3225
+ }
3226
+ }
3227
+ async function handleMissingTests(config) {
3228
+ const result = await clack10.confirm({
3229
+ message: "Require every source file to have a test file?",
3230
+ initialValue: config.rules.enforceMissingTests
3231
+ });
3232
+ assertNotCancelled(result);
3233
+ config.rules.enforceMissingTests = result;
3234
+ }
3235
+ async function handleCoverage(config, state, opts) {
3236
+ if (!opts.hasTestRunner) {
3237
+ clack10.log.info("Coverage checks are inactive \u2014 no test runner detected.");
3238
+ return;
3239
+ }
3240
+ const planned = planCoverageInstall(opts.coveragePrereqs);
3241
+ if (planned) {
3242
+ const choice = await clack10.select({
3243
+ message: `${planned.label} is not installed. Needed for coverage checks.`,
3268
3244
  options: [
3269
3245
  {
3270
3246
  value: "install",
3271
- label: "Install now",
3272
- hint: m.installCommand
3273
- },
3274
- {
3275
- value: "disable",
3276
- label: "Disable coverage checks",
3277
- hint: "missing-test checks still stay active"
3247
+ label: "Install (after final confirmation)",
3248
+ hint: planned.command
3278
3249
  },
3250
+ { value: "disable", label: "Disable coverage checks" },
3279
3251
  {
3280
3252
  value: "skip",
3281
3253
  label: "Skip for now",
3282
- hint: `install later: ${m.installCommand}`
3254
+ hint: `install later: ${planned.command}`
3283
3255
  }
3284
3256
  ]
3285
3257
  });
3286
3258
  assertNotCancelled(choice);
3259
+ state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
3287
3260
  if (choice === "install") {
3288
- const is = clack8.spinner();
3289
- is.start(`Installing ${m.label}...`);
3290
- const result = await spawnAsync(m.installCommand, projectRoot);
3291
- if (result.status === 0) {
3292
- is.stop(`Installed ${m.label}`);
3293
- } else {
3294
- is.stop(`Failed to install ${m.label}`);
3295
- clack8.log.warn(
3296
- `Install manually: ${m.installCommand}
3297
- Coverage percentage checks will not work until the dependency is installed.`
3298
- );
3299
- }
3261
+ planned.onFailure = () => {
3262
+ config.rules.testCoverage = 0;
3263
+ };
3264
+ state.deferredInstalls.push(planned);
3300
3265
  } else if (choice === "disable") {
3301
- disableCoverage = true;
3302
- clack8.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
3303
- } else {
3304
- clack8.log.info(
3305
- `Coverage percentage checks will fail until ${m.label} is installed.
3306
- Install later: ${m.installCommand}`
3307
- );
3266
+ config.rules.testCoverage = 0;
3267
+ return;
3308
3268
  }
3309
3269
  }
3310
- return { disableCoverage };
3270
+ const result = await clack10.text({
3271
+ message: "Test coverage target (0 = disable)?",
3272
+ initialValue: String(config.rules.testCoverage),
3273
+ validate: (v) => {
3274
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
3275
+ const n = Number.parseInt(v, 10);
3276
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
3277
+ }
3278
+ });
3279
+ assertNotCancelled(result);
3280
+ config.rules.testCoverage = Number.parseInt(result, 10);
3311
3281
  }
3312
- function hasDependency(projectRoot, name) {
3282
+ async function handlePackageOverrides(config) {
3283
+ const rootPkg = getRootPackage(config.packages);
3284
+ config.packages = await promptPackageOverrides(config.packages, {
3285
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3286
+ maxFileLines: config.rules.maxFileLines,
3287
+ testCoverage: config.rules.testCoverage,
3288
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3289
+ coverageCommand: config.defaults?.coverage?.command
3290
+ });
3291
+ normalizePackageOverrides(config.packages);
3292
+ }
3293
+ async function handleBoundaries(config, state, opts) {
3294
+ const shouldInfer = await clack10.confirm({
3295
+ message: "Infer boundary rules from current import patterns?",
3296
+ initialValue: false
3297
+ });
3298
+ assertNotCancelled(shouldInfer);
3299
+ state.visited.boundaries = true;
3300
+ if (!shouldInfer) {
3301
+ config.rules.enforceBoundaries = false;
3302
+ return;
3303
+ }
3304
+ const bs = clack10.spinner();
3305
+ bs.start("Building import graph...");
3313
3306
  try {
3314
- const pkgPath = path14.join(projectRoot, "package.json");
3315
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
3316
- return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
3317
- } catch {
3318
- return false;
3307
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3308
+ const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
3309
+ const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
3310
+ const inferred = inferBoundaries(graph);
3311
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
3312
+ if (denyCount > 0) {
3313
+ config.boundaries = inferred;
3314
+ config.rules.enforceBoundaries = true;
3315
+ const pkgCount = Object.keys(inferred.deny).length;
3316
+ bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3317
+ } else {
3318
+ bs.stop("No boundary rules inferred");
3319
+ }
3320
+ } catch (err) {
3321
+ bs.stop("Failed to build import graph");
3322
+ clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
3323
+ }
3324
+ }
3325
+ async function handleIntegrations(state, opts) {
3326
+ const result = await promptIntegrationsDeferred(
3327
+ state.hookManager,
3328
+ opts.tools,
3329
+ opts.tools.packageManager,
3330
+ opts.tools.isWorkspace,
3331
+ opts.projectRoot
3332
+ );
3333
+ state.visited.integrations = true;
3334
+ state.integrations = result.choice;
3335
+ state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
3336
+ if (result.lefthookInstall) {
3337
+ state.deferredInstalls.push(result.lefthookInstall);
3319
3338
  }
3320
3339
  }
3321
-
3322
- // src/commands/init.ts
3323
- init_prompt();
3324
3340
 
3325
3341
  // src/utils/update-gitignore.ts
3326
- var fs15 = __toESM(require("fs"), 1);
3327
- var path15 = __toESM(require("path"), 1);
3342
+ var fs16 = __toESM(require("fs"), 1);
3343
+ var path16 = __toESM(require("path"), 1);
3328
3344
  function updateGitignore(projectRoot) {
3329
- const gitignorePath = path15.join(projectRoot, ".gitignore");
3345
+ const gitignorePath = path16.join(projectRoot, ".gitignore");
3330
3346
  let content = "";
3331
- if (fs15.existsSync(gitignorePath)) {
3332
- content = fs15.readFileSync(gitignorePath, "utf-8");
3347
+ if (fs16.existsSync(gitignorePath)) {
3348
+ content = fs16.readFileSync(gitignorePath, "utf-8");
3333
3349
  }
3334
3350
  if (!content.includes(".viberails/scan-result.json")) {
3335
3351
  const block = "\n# viberails\n.viberails/scan-result.json\n";
3336
3352
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
3337
3353
  `;
3338
- fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
3354
+ fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
3339
3355
  }
3340
3356
  }
3341
3357
 
3342
3358
  // src/commands/init-hooks.ts
3343
- var fs17 = __toESM(require("fs"), 1);
3344
- var path17 = __toESM(require("path"), 1);
3345
- var import_chalk11 = __toESM(require("chalk"), 1);
3359
+ var fs18 = __toESM(require("fs"), 1);
3360
+ var path18 = __toESM(require("path"), 1);
3361
+ var import_chalk10 = __toESM(require("chalk"), 1);
3346
3362
  var import_yaml = require("yaml");
3347
3363
 
3348
3364
  // src/commands/resolve-typecheck.ts
3349
- var fs16 = __toESM(require("fs"), 1);
3350
- var path16 = __toESM(require("path"), 1);
3365
+ var fs17 = __toESM(require("fs"), 1);
3366
+ var path17 = __toESM(require("path"), 1);
3351
3367
  function hasTurboTask(projectRoot, taskName) {
3352
- const turboPath = path16.join(projectRoot, "turbo.json");
3353
- if (!fs16.existsSync(turboPath)) return false;
3368
+ const turboPath = path17.join(projectRoot, "turbo.json");
3369
+ if (!fs17.existsSync(turboPath)) return false;
3354
3370
  try {
3355
- const turbo = JSON.parse(fs16.readFileSync(turboPath, "utf-8"));
3371
+ const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
3356
3372
  const tasks = turbo.tasks ?? turbo.pipeline ?? {};
3357
3373
  return taskName in tasks;
3358
3374
  } catch {
@@ -3363,10 +3379,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3363
3379
  if (hasTurboTask(projectRoot, "typecheck")) {
3364
3380
  return { command: "npx turbo typecheck", label: "turbo typecheck" };
3365
3381
  }
3366
- const pkgJsonPath = path16.join(projectRoot, "package.json");
3367
- if (fs16.existsSync(pkgJsonPath)) {
3382
+ const pkgJsonPath = path17.join(projectRoot, "package.json");
3383
+ if (fs17.existsSync(pkgJsonPath)) {
3368
3384
  try {
3369
- const pkg = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
3385
+ const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
3370
3386
  if (pkg.scripts?.typecheck) {
3371
3387
  const pm = packageManager ?? "npm";
3372
3388
  return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
@@ -3374,7 +3390,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3374
3390
  } catch {
3375
3391
  }
3376
3392
  }
3377
- if (fs16.existsSync(path16.join(projectRoot, "tsconfig.json"))) {
3393
+ if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
3378
3394
  return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
3379
3395
  }
3380
3396
  return {
@@ -3384,36 +3400,36 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3384
3400
 
3385
3401
  // src/commands/init-hooks.ts
3386
3402
  function setupPreCommitHook(projectRoot) {
3387
- const lefthookPath = path17.join(projectRoot, "lefthook.yml");
3388
- if (fs17.existsSync(lefthookPath)) {
3403
+ const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3404
+ if (fs18.existsSync(lefthookPath)) {
3389
3405
  addLefthookPreCommit(lefthookPath);
3390
- console.log(` ${import_chalk11.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3406
+ console.log(` ${import_chalk10.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3391
3407
  return "lefthook.yml";
3392
3408
  }
3393
- const huskyDir = path17.join(projectRoot, ".husky");
3394
- if (fs17.existsSync(huskyDir)) {
3409
+ const huskyDir = path18.join(projectRoot, ".husky");
3410
+ if (fs18.existsSync(huskyDir)) {
3395
3411
  writeHuskyPreCommit(huskyDir);
3396
- console.log(` ${import_chalk11.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3412
+ console.log(` ${import_chalk10.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3397
3413
  return ".husky/pre-commit";
3398
3414
  }
3399
- const gitDir = path17.join(projectRoot, ".git");
3400
- if (fs17.existsSync(gitDir)) {
3401
- const hooksDir = path17.join(gitDir, "hooks");
3402
- if (!fs17.existsSync(hooksDir)) {
3403
- fs17.mkdirSync(hooksDir, { recursive: true });
3415
+ const gitDir = path18.join(projectRoot, ".git");
3416
+ if (fs18.existsSync(gitDir)) {
3417
+ const hooksDir = path18.join(gitDir, "hooks");
3418
+ if (!fs18.existsSync(hooksDir)) {
3419
+ fs18.mkdirSync(hooksDir, { recursive: true });
3404
3420
  }
3405
3421
  writeGitHookPreCommit(hooksDir);
3406
- console.log(` ${import_chalk11.default.green("\u2713")} .git/hooks/pre-commit`);
3422
+ console.log(` ${import_chalk10.default.green("\u2713")} .git/hooks/pre-commit`);
3407
3423
  return ".git/hooks/pre-commit";
3408
3424
  }
3409
3425
  return void 0;
3410
3426
  }
3411
3427
  function writeGitHookPreCommit(hooksDir) {
3412
- const hookPath = path17.join(hooksDir, "pre-commit");
3413
- if (fs17.existsSync(hookPath)) {
3414
- const existing = fs17.readFileSync(hookPath, "utf-8");
3428
+ const hookPath = path18.join(hooksDir, "pre-commit");
3429
+ if (fs18.existsSync(hookPath)) {
3430
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3415
3431
  if (existing.includes("viberails")) return;
3416
- fs17.writeFileSync(
3432
+ fs18.writeFileSync(
3417
3433
  hookPath,
3418
3434
  `${existing.trimEnd()}
3419
3435
 
@@ -3430,10 +3446,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
3430
3446
  "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
3431
3447
  ""
3432
3448
  ].join("\n");
3433
- fs17.writeFileSync(hookPath, script, { mode: 493 });
3449
+ fs18.writeFileSync(hookPath, script, { mode: 493 });
3434
3450
  }
3435
3451
  function addLefthookPreCommit(lefthookPath) {
3436
- const content = fs17.readFileSync(lefthookPath, "utf-8");
3452
+ const content = fs18.readFileSync(lefthookPath, "utf-8");
3437
3453
  if (content.includes("viberails")) return;
3438
3454
  const doc = (0, import_yaml.parse)(content) ?? {};
3439
3455
  if (!doc["pre-commit"]) {
@@ -3445,28 +3461,28 @@ function addLefthookPreCommit(lefthookPath) {
3445
3461
  doc["pre-commit"].commands.viberails = {
3446
3462
  run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
3447
3463
  };
3448
- fs17.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3464
+ fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3449
3465
  }
3450
3466
  function detectHookManager(projectRoot) {
3451
- if (fs17.existsSync(path17.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3452
- if (fs17.existsSync(path17.join(projectRoot, ".husky"))) return "Husky";
3467
+ if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3468
+ if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
3453
3469
  return void 0;
3454
3470
  }
3455
3471
  function setupClaudeCodeHook(projectRoot) {
3456
- const claudeDir = path17.join(projectRoot, ".claude");
3457
- if (!fs17.existsSync(claudeDir)) {
3458
- fs17.mkdirSync(claudeDir, { recursive: true });
3472
+ const claudeDir = path18.join(projectRoot, ".claude");
3473
+ if (!fs18.existsSync(claudeDir)) {
3474
+ fs18.mkdirSync(claudeDir, { recursive: true });
3459
3475
  }
3460
- const settingsPath = path17.join(claudeDir, "settings.json");
3476
+ const settingsPath = path18.join(claudeDir, "settings.json");
3461
3477
  let settings = {};
3462
- if (fs17.existsSync(settingsPath)) {
3478
+ if (fs18.existsSync(settingsPath)) {
3463
3479
  try {
3464
- settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
3480
+ settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
3465
3481
  } catch {
3466
3482
  console.warn(
3467
- ` ${import_chalk11.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3483
+ ` ${import_chalk10.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3468
3484
  );
3469
- console.warn(` Fix the JSON manually, then re-run ${import_chalk11.default.cyan("viberails init --force")}`);
3485
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk10.default.cyan("viberails init --force")}`);
3470
3486
  return;
3471
3487
  }
3472
3488
  }
@@ -3487,30 +3503,30 @@ function setupClaudeCodeHook(projectRoot) {
3487
3503
  }
3488
3504
  ];
3489
3505
  settings.hooks = hooks;
3490
- fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3506
+ fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3491
3507
  `);
3492
- console.log(` ${import_chalk11.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3508
+ console.log(` ${import_chalk10.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3493
3509
  }
3494
3510
  function setupClaudeMdReference(projectRoot) {
3495
- const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
3511
+ const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
3496
3512
  let content = "";
3497
- if (fs17.existsSync(claudeMdPath)) {
3498
- content = fs17.readFileSync(claudeMdPath, "utf-8");
3513
+ if (fs18.existsSync(claudeMdPath)) {
3514
+ content = fs18.readFileSync(claudeMdPath, "utf-8");
3499
3515
  }
3500
3516
  if (content.includes("@.viberails/context.md")) return;
3501
3517
  const ref = "\n@.viberails/context.md\n";
3502
3518
  const prefix = content.length === 0 ? "" : content.trimEnd();
3503
- fs17.writeFileSync(claudeMdPath, prefix + ref);
3504
- console.log(` ${import_chalk11.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3519
+ fs18.writeFileSync(claudeMdPath, prefix + ref);
3520
+ console.log(` ${import_chalk10.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3505
3521
  }
3506
3522
  function setupGithubAction(projectRoot, packageManager, options) {
3507
- const workflowDir = path17.join(projectRoot, ".github", "workflows");
3508
- const workflowPath = path17.join(workflowDir, "viberails.yml");
3509
- if (fs17.existsSync(workflowPath)) {
3510
- const existing = fs17.readFileSync(workflowPath, "utf-8");
3523
+ const workflowDir = path18.join(projectRoot, ".github", "workflows");
3524
+ const workflowPath = path18.join(workflowDir, "viberails.yml");
3525
+ if (fs18.existsSync(workflowPath)) {
3526
+ const existing = fs18.readFileSync(workflowPath, "utf-8");
3511
3527
  if (existing.includes("viberails")) return void 0;
3512
3528
  }
3513
- fs17.mkdirSync(workflowDir, { recursive: true });
3529
+ fs18.mkdirSync(workflowDir, { recursive: true });
3514
3530
  const pm = packageManager || "npm";
3515
3531
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
3516
3532
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -3564,74 +3580,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
3564
3580
  ""
3565
3581
  );
3566
3582
  const content = lines.filter((l) => l !== void 0).join("\n");
3567
- fs17.writeFileSync(workflowPath, content);
3583
+ fs18.writeFileSync(workflowPath, content);
3568
3584
  return ".github/workflows/viberails.yml";
3569
3585
  }
3570
3586
  function writeHuskyPreCommit(huskyDir) {
3571
- const hookPath = path17.join(huskyDir, "pre-commit");
3587
+ const hookPath = path18.join(huskyDir, "pre-commit");
3572
3588
  const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
3573
- if (fs17.existsSync(hookPath)) {
3574
- const existing = fs17.readFileSync(hookPath, "utf-8");
3589
+ if (fs18.existsSync(hookPath)) {
3590
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3575
3591
  if (!existing.includes("viberails")) {
3576
- fs17.writeFileSync(hookPath, `${existing.trimEnd()}
3592
+ fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3577
3593
  ${cmd}
3578
3594
  `);
3579
3595
  }
3580
3596
  return;
3581
3597
  }
3582
- fs17.writeFileSync(hookPath, `#!/bin/sh
3598
+ fs18.writeFileSync(hookPath, `#!/bin/sh
3583
3599
  ${cmd}
3584
3600
  `, { mode: 493 });
3585
3601
  }
3586
3602
 
3587
3603
  // src/commands/init-hooks-extra.ts
3588
- var fs18 = __toESM(require("fs"), 1);
3589
- var path18 = __toESM(require("path"), 1);
3590
- var import_chalk12 = __toESM(require("chalk"), 1);
3604
+ var fs19 = __toESM(require("fs"), 1);
3605
+ var path19 = __toESM(require("path"), 1);
3606
+ var import_chalk11 = __toESM(require("chalk"), 1);
3591
3607
  var import_yaml2 = require("yaml");
3592
3608
  function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
3593
- const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3594
- if (fs18.existsSync(lefthookPath)) {
3595
- const content = fs18.readFileSync(lefthookPath, "utf-8");
3609
+ const lefthookPath = path19.join(projectRoot, "lefthook.yml");
3610
+ if (fs19.existsSync(lefthookPath)) {
3611
+ const content = fs19.readFileSync(lefthookPath, "utf-8");
3596
3612
  if (content.includes(marker)) return void 0;
3597
3613
  const doc = (0, import_yaml2.parse)(content) ?? {};
3598
3614
  if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
3599
3615
  if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
3600
3616
  doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
3601
- fs18.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3617
+ fs19.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3602
3618
  return "lefthook.yml";
3603
3619
  }
3604
- const huskyDir = path18.join(projectRoot, ".husky");
3605
- if (fs18.existsSync(huskyDir)) {
3606
- const hookPath = path18.join(huskyDir, "pre-commit");
3607
- if (fs18.existsSync(hookPath)) {
3608
- const existing = fs18.readFileSync(hookPath, "utf-8");
3620
+ const huskyDir = path19.join(projectRoot, ".husky");
3621
+ if (fs19.existsSync(huskyDir)) {
3622
+ const hookPath = path19.join(huskyDir, "pre-commit");
3623
+ if (fs19.existsSync(hookPath)) {
3624
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3609
3625
  if (existing.includes(marker)) return void 0;
3610
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3626
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3611
3627
  ${command}
3612
3628
  `);
3613
3629
  } else {
3614
- fs18.writeFileSync(hookPath, `#!/bin/sh
3630
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3615
3631
  ${command}
3616
3632
  `, { mode: 493 });
3617
3633
  }
3618
3634
  return ".husky/pre-commit";
3619
3635
  }
3620
- const gitDir = path18.join(projectRoot, ".git");
3621
- if (fs18.existsSync(gitDir)) {
3622
- const hooksDir = path18.join(gitDir, "hooks");
3623
- if (!fs18.existsSync(hooksDir)) fs18.mkdirSync(hooksDir, { recursive: true });
3624
- const hookPath = path18.join(hooksDir, "pre-commit");
3625
- if (fs18.existsSync(hookPath)) {
3626
- const existing = fs18.readFileSync(hookPath, "utf-8");
3636
+ const gitDir = path19.join(projectRoot, ".git");
3637
+ if (fs19.existsSync(gitDir)) {
3638
+ const hooksDir = path19.join(gitDir, "hooks");
3639
+ if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
3640
+ const hookPath = path19.join(hooksDir, "pre-commit");
3641
+ if (fs19.existsSync(hookPath)) {
3642
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3627
3643
  if (existing.includes(marker)) return void 0;
3628
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3644
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3629
3645
 
3630
3646
  # ${name}
3631
3647
  ${command}
3632
3648
  `);
3633
3649
  } else {
3634
- fs18.writeFileSync(hookPath, `#!/bin/sh
3650
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3635
3651
  # Generated by viberails
3636
3652
 
3637
3653
  # ${name}
@@ -3647,17 +3663,17 @@ ${command}
3647
3663
  function setupTypecheckHook(projectRoot, packageManager) {
3648
3664
  const resolved = resolveTypecheckCommand(projectRoot, packageManager);
3649
3665
  if (!resolved.command) {
3650
- console.log(` ${import_chalk12.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3666
+ console.log(` ${import_chalk11.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3651
3667
  return void 0;
3652
3668
  }
3653
3669
  const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
3654
3670
  if (target) {
3655
- console.log(` ${import_chalk12.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3671
+ console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3656
3672
  }
3657
3673
  return target;
3658
3674
  }
3659
3675
  function setupLintHook(projectRoot, linter) {
3660
- const isLefthook = fs18.existsSync(path18.join(projectRoot, "lefthook.yml"));
3676
+ const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
3661
3677
  const linterName = linter === "biome" ? "Biome" : "ESLint";
3662
3678
  let command;
3663
3679
  let lefthookExtra;
@@ -3673,7 +3689,7 @@ function setupLintHook(projectRoot, linter) {
3673
3689
  }
3674
3690
  const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
3675
3691
  if (target) {
3676
- console.log(` ${import_chalk12.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3692
+ console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3677
3693
  }
3678
3694
  return target;
3679
3695
  }
@@ -3681,6 +3697,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3681
3697
  const created = [];
3682
3698
  if (integrations.preCommitHook) {
3683
3699
  const t = setupPreCommitHook(projectRoot);
3700
+ if (t && opts.lefthookExpected && !t.includes("lefthook")) {
3701
+ console.log(` ${import_chalk11.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3702
+ }
3684
3703
  created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
3685
3704
  }
3686
3705
  if (integrations.typecheckHook) {
@@ -3710,12 +3729,12 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3710
3729
  }
3711
3730
 
3712
3731
  // src/commands/init-non-interactive.ts
3713
- var fs19 = __toESM(require("fs"), 1);
3714
- var path19 = __toESM(require("path"), 1);
3715
- var clack9 = __toESM(require("@clack/prompts"), 1);
3732
+ var fs20 = __toESM(require("fs"), 1);
3733
+ var path20 = __toESM(require("path"), 1);
3734
+ var clack11 = __toESM(require("@clack/prompts"), 1);
3716
3735
  var import_config8 = require("@viberails/config");
3717
3736
  var import_scanner2 = require("@viberails/scanner");
3718
- var import_chalk13 = __toESM(require("chalk"), 1);
3737
+ var import_chalk12 = __toESM(require("chalk"), 1);
3719
3738
 
3720
3739
  // src/utils/filter-confidence.ts
3721
3740
  function filterHighConfidence(conventions, meta) {
@@ -3736,7 +3755,7 @@ function getExemptedPackages(config) {
3736
3755
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3737
3756
  }
3738
3757
  async function initNonInteractive(projectRoot, configPath) {
3739
- const s = clack9.spinner();
3758
+ const s = clack11.spinner();
3740
3759
  s.start("Scanning project...");
3741
3760
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
3742
3761
  const config = (0, import_config8.generateConfig)(scanResult);
@@ -3751,11 +3770,11 @@ async function initNonInteractive(projectRoot, configPath) {
3751
3770
  const exempted = getExemptedPackages(config);
3752
3771
  if (exempted.length > 0) {
3753
3772
  console.log(
3754
- ` ${import_chalk13.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk13.default.dim("(types-only)")}`
3773
+ ` ${import_chalk12.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk12.default.dim("(types-only)")}`
3755
3774
  );
3756
3775
  }
3757
3776
  if (config.packages.length > 1) {
3758
- const bs = clack9.spinner();
3777
+ const bs = clack11.spinner();
3759
3778
  bs.start("Building import graph...");
3760
3779
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3761
3780
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3771,7 +3790,7 @@ async function initNonInteractive(projectRoot, configPath) {
3771
3790
  }
3772
3791
  }
3773
3792
  const compacted = (0, import_config8.compactConfig)(config);
3774
- fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3793
+ fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3775
3794
  `);
3776
3795
  writeGeneratedFiles(projectRoot, config, scanResult);
3777
3796
  updateGitignore(projectRoot);
@@ -3788,14 +3807,14 @@ async function initNonInteractive(projectRoot, configPath) {
3788
3807
  const hookManager = detectHookManager(projectRoot);
3789
3808
  const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
3790
3809
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3791
- const ok = import_chalk13.default.green("\u2713");
3810
+ const ok = import_chalk12.default.green("\u2713");
3792
3811
  const created = [
3793
- `${ok} ${path19.basename(configPath)}`,
3812
+ `${ok} ${path20.basename(configPath)}`,
3794
3813
  `${ok} .viberails/context.md`,
3795
3814
  `${ok} .viberails/scan-result.json`,
3796
3815
  `${ok} .claude/settings.json \u2014 added viberails hook`,
3797
3816
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
3798
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk13.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3817
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk12.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3799
3818
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
3800
3819
  ].filter(Boolean);
3801
3820
  if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
@@ -3807,9 +3826,6 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
3807
3826
 
3808
3827
  // src/commands/init.ts
3809
3828
  var CONFIG_FILE5 = "viberails.config.json";
3810
- function getExemptedPackages2(config) {
3811
- return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3812
- }
3813
3829
  async function initCommand(options, cwd) {
3814
3830
  const projectRoot = findProjectRoot(cwd ?? process.cwd());
3815
3831
  if (!projectRoot) {
@@ -3817,14 +3833,14 @@ async function initCommand(options, cwd) {
3817
3833
  "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
3818
3834
  );
3819
3835
  }
3820
- const configPath = path20.join(projectRoot, CONFIG_FILE5);
3821
- if (fs20.existsSync(configPath) && !options.force) {
3836
+ const configPath = path21.join(projectRoot, CONFIG_FILE5);
3837
+ if (fs21.existsSync(configPath) && !options.force) {
3822
3838
  if (!options.yes) {
3823
3839
  return initInteractive(projectRoot, configPath, options);
3824
3840
  }
3825
3841
  console.log(
3826
- `${import_chalk14.default.yellow("!")} viberails is already initialized.
3827
- 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.`
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.`
3828
3844
  );
3829
3845
  return;
3830
3846
  }
@@ -3832,12 +3848,11 @@ async function initCommand(options, cwd) {
3832
3848
  await initInteractive(projectRoot, configPath, options);
3833
3849
  }
3834
3850
  async function initInteractive(projectRoot, configPath, options) {
3835
- clack11.intro("viberails");
3836
- const replacingExistingConfig = fs20.existsSync(configPath);
3837
- if (fs20.existsSync(configPath) && !options.force) {
3838
- const action = await promptExistingConfigAction(path20.basename(configPath));
3851
+ clack12.intro("viberails");
3852
+ if (fs21.existsSync(configPath) && !options.force) {
3853
+ const action = await promptExistingConfigAction(path21.basename(configPath));
3839
3854
  if (action === "cancel") {
3840
- clack11.outro("Aborted. No files were written.");
3855
+ clack12.outro("Aborted. No files were written.");
3841
3856
  return;
3842
3857
  }
3843
3858
  if (action === "edit") {
@@ -3846,143 +3861,93 @@ async function initInteractive(projectRoot, configPath, options) {
3846
3861
  }
3847
3862
  options.force = true;
3848
3863
  }
3849
- if (fs20.existsSync(configPath) && options.force) {
3864
+ if (fs21.existsSync(configPath) && options.force) {
3850
3865
  const replace = await confirmDangerous(
3851
- `${path20.basename(configPath)} already exists and will be replaced. Continue?`
3866
+ `${path21.basename(configPath)} already exists and will be replaced. Continue?`
3852
3867
  );
3853
3868
  if (!replace) {
3854
- clack11.outro("Aborted. No files were written.");
3869
+ clack12.outro("Aborted. No files were written.");
3855
3870
  return;
3856
3871
  }
3857
3872
  }
3858
- const s = clack11.spinner();
3873
+ const s = clack12.spinner();
3859
3874
  s.start("Scanning project...");
3860
3875
  const scanResult = await (0, import_scanner3.scan)(projectRoot);
3861
3876
  const config = (0, import_config9.generateConfig)(scanResult);
3862
3877
  s.stop("Scan complete");
3863
3878
  if (scanResult.statistics.totalFiles === 0) {
3864
- clack11.log.warn(
3879
+ clack12.log.warn(
3865
3880
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3866
3881
  );
3867
3882
  }
3868
- const exemptedPkgs = getExemptedPackages2(config);
3869
- let decision;
3870
- while (true) {
3871
- displayInitOverview(scanResult, config, exemptedPkgs);
3872
- const nextDecision = await promptInitDecision();
3873
- if (nextDecision === "review") {
3874
- clack11.note(formatScanResultsText(scanResult), "Detected details");
3875
- continue;
3876
- }
3877
- decision = nextDecision;
3878
- break;
3879
- }
3880
- if (decision === "customize") {
3881
- const { resolveNamingDefault: resolveNamingDefault2 } = await Promise.resolve().then(() => (init_prompt_naming_default(), prompt_naming_default_exports));
3882
- await resolveNamingDefault2(config, scanResult);
3883
- const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
3884
- const overrides = await promptRuleMenu({
3885
- maxFileLines: config.rules.maxFileLines,
3886
- maxTestFileLines: config.rules.maxTestFileLines,
3887
- testCoverage: config.rules.testCoverage,
3888
- enforceMissingTests: config.rules.enforceMissingTests,
3889
- enforceNaming: config.rules.enforceNaming,
3890
- fileNamingValue: rootPkg.conventions?.fileNaming,
3891
- componentNaming: rootPkg.conventions?.componentNaming,
3892
- hookNaming: rootPkg.conventions?.hookNaming,
3893
- importAlias: rootPkg.conventions?.importAlias,
3894
- coverageSummaryPath: "coverage/coverage-summary.json",
3895
- coverageCommand: config.defaults?.coverage?.command,
3896
- packageOverrides: config.packages
3897
- });
3898
- applyRuleOverrides(config, overrides);
3899
- }
3900
- if (config.packages.length > 1) {
3901
- clack11.note(
3902
- "Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
3903
- "Boundaries"
3883
+ const hasTestRunner = !!scanResult.stack.testRunner;
3884
+ if (!hasTestRunner) {
3885
+ clack12.log.info(
3886
+ "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."
3904
3887
  );
3905
- const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
3906
- if (shouldInfer) {
3907
- const bs = clack11.spinner();
3908
- bs.start("Building import graph...");
3909
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3910
- const packages = resolveWorkspacePackages(projectRoot, config.packages);
3911
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
3912
- const inferred = inferBoundaries(graph);
3913
- const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
3914
- if (denyCount > 0) {
3915
- config.boundaries = inferred;
3916
- config.rules.enforceBoundaries = true;
3917
- const pkgCount = Object.keys(inferred.deny).length;
3918
- bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3919
- } else {
3920
- bs.stop("No boundary rules inferred");
3921
- }
3922
- }
3923
3888
  }
3924
3889
  const hookManager = detectHookManager(projectRoot);
3925
3890
  const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
3926
- const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
3927
- if (hasMissingPrereqs) {
3928
- clack11.log.info("Some dependencies are needed for full functionality.");
3929
- }
3930
- const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
3931
- if (prereqResult.disableCoverage) {
3932
- config.rules.testCoverage = 0;
3933
- }
3934
3891
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
3935
- const integrations = await promptIntegrations(projectRoot, hookManager, {
3936
- isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
3937
- linter: rootPkgStack?.linter?.split("@")[0],
3938
- packageManager: rootPkgStack?.packageManager?.split("@")[0],
3939
- isWorkspace: config.packages.length > 1
3940
- });
3941
- displaySetupPlan(config, integrations, {
3942
- replacingExistingConfig,
3943
- configFile: path20.basename(configPath)
3892
+ const state = await promptMainMenu(config, scanResult, {
3893
+ hasTestRunner,
3894
+ hookManager,
3895
+ coveragePrereqs,
3896
+ projectRoot,
3897
+ tools: {
3898
+ isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
3899
+ linter: rootPkgStack?.linter?.split("@")[0],
3900
+ packageManager: rootPkgStack?.packageManager?.split("@")[0],
3901
+ isWorkspace: config.packages.length > 1
3902
+ }
3944
3903
  });
3945
3904
  const shouldWrite = await confirm3("Apply this setup?");
3946
3905
  if (!shouldWrite) {
3947
- clack11.outro("Aborted. No files were written.");
3906
+ clack12.outro("Aborted. No files were written.");
3948
3907
  return;
3949
3908
  }
3950
- const ws = clack11.spinner();
3909
+ if (state.deferredInstalls.length > 0) {
3910
+ await executeDeferredInstalls(projectRoot, state.deferredInstalls);
3911
+ }
3912
+ const ws = clack12.spinner();
3951
3913
  ws.start("Writing configuration...");
3952
3914
  const compacted = (0, import_config9.compactConfig)(config);
3953
- fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3915
+ fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3954
3916
  `);
3955
3917
  writeGeneratedFiles(projectRoot, config, scanResult);
3956
3918
  updateGitignore(projectRoot);
3957
3919
  ws.stop("Configuration written");
3958
- const ok = import_chalk14.default.green("\u2713");
3959
- clack11.log.step(`${ok} ${path20.basename(configPath)}`);
3960
- clack11.log.step(`${ok} .viberails/context.md`);
3961
- clack11.log.step(`${ok} .viberails/scan-result.json`);
3962
- setupSelectedIntegrations(projectRoot, integrations, {
3963
- linter: rootPkgStack?.linter?.split("@")[0],
3964
- packageManager: rootPkgStack?.packageManager?.split("@")[0]
3965
- });
3966
- clack11.outro(
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`);
3924
+ if (state.visited.integrations && state.integrations) {
3925
+ const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
3926
+ setupSelectedIntegrations(projectRoot, state.integrations, {
3927
+ linter: rootPkgStack?.linter?.split("@")[0],
3928
+ packageManager: rootPkgStack?.packageManager?.split("@")[0],
3929
+ lefthookExpected
3930
+ });
3931
+ }
3932
+ clack12.outro(
3967
3933
  `Done! Next: review viberails.config.json, then run viberails check
3968
- ${import_chalk14.default.dim("Tip: use")} ${import_chalk14.default.cyan("viberails check --enforce")} ${import_chalk14.default.dim("in CI to block PRs on violations.")}`
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.")}`
3969
3935
  );
3970
3936
  }
3971
3937
 
3972
3938
  // src/commands/sync.ts
3973
- var fs21 = __toESM(require("fs"), 1);
3974
- var path21 = __toESM(require("path"), 1);
3975
- var clack12 = __toESM(require("@clack/prompts"), 1);
3939
+ var fs22 = __toESM(require("fs"), 1);
3940
+ var path22 = __toESM(require("path"), 1);
3941
+ var clack13 = __toESM(require("@clack/prompts"), 1);
3976
3942
  var import_config11 = require("@viberails/config");
3977
3943
  var import_scanner4 = require("@viberails/scanner");
3978
- var import_chalk15 = __toESM(require("chalk"), 1);
3979
- init_prompt();
3944
+ var import_chalk14 = __toESM(require("chalk"), 1);
3980
3945
  var CONFIG_FILE6 = "viberails.config.json";
3981
3946
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
3982
3947
  function loadPreviousStats(projectRoot) {
3983
- const scanResultPath = path21.join(projectRoot, SCAN_RESULT_FILE2);
3948
+ const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
3984
3949
  try {
3985
- const raw = fs21.readFileSync(scanResultPath, "utf-8");
3950
+ const raw = fs22.readFileSync(scanResultPath, "utf-8");
3986
3951
  const parsed = JSON.parse(raw);
3987
3952
  if (parsed?.statistics?.totalFiles !== void 0) {
3988
3953
  return parsed.statistics;
@@ -3999,17 +3964,17 @@ async function syncCommand(options, cwd) {
3999
3964
  "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"
4000
3965
  );
4001
3966
  }
4002
- const configPath = path21.join(projectRoot, CONFIG_FILE6);
3967
+ const configPath = path22.join(projectRoot, CONFIG_FILE6);
4003
3968
  const existing = await (0, import_config11.loadConfig)(configPath);
4004
3969
  const previousStats = loadPreviousStats(projectRoot);
4005
- const s = clack12.spinner();
3970
+ const s = clack13.spinner();
4006
3971
  s.start("Scanning project...");
4007
3972
  const scanResult = await (0, import_scanner4.scan)(projectRoot);
4008
3973
  s.stop("Scan complete");
4009
3974
  const merged = (0, import_config11.mergeConfig)(existing, scanResult);
4010
3975
  const compacted = (0, import_config11.compactConfig)(merged);
4011
3976
  const compactedJson = JSON.stringify(compacted, null, 2);
4012
- const rawDisk = fs21.readFileSync(configPath, "utf-8").trim();
3977
+ const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
4013
3978
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
4014
3979
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
4015
3980
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -4017,19 +3982,19 @@ async function syncCommand(options, cwd) {
4017
3982
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
4018
3983
  if (changes.length > 0 || statsDelta) {
4019
3984
  console.log(`
4020
- ${import_chalk15.default.bold("Changes:")}`);
3985
+ ${import_chalk14.default.bold("Changes:")}`);
4021
3986
  for (const change of changes) {
4022
- const icon = change.type === "removed" ? import_chalk15.default.red("-") : import_chalk15.default.green("+");
3987
+ const icon = change.type === "removed" ? import_chalk14.default.red("-") : import_chalk14.default.green("+");
4023
3988
  console.log(` ${icon} ${change.description}`);
4024
3989
  }
4025
3990
  if (statsDelta) {
4026
- console.log(` ${import_chalk15.default.dim(statsDelta)}`);
3991
+ console.log(` ${import_chalk14.default.dim(statsDelta)}`);
4027
3992
  }
4028
3993
  }
4029
3994
  if (options?.interactive) {
4030
- clack12.intro("viberails sync (interactive)");
4031
- clack12.note(formatRulesText(merged).join("\n"), "Rules after sync");
4032
- const decision = await clack12.select({
3995
+ clack13.intro("viberails sync (interactive)");
3996
+ clack13.note(formatRulesText(merged).join("\n"), "Rules after sync");
3997
+ const decision = await clack13.select({
4033
3998
  message: "How would you like to proceed?",
4034
3999
  options: [
4035
4000
  { value: "accept", label: "Accept changes" },
@@ -4039,7 +4004,7 @@ ${import_chalk15.default.bold("Changes:")}`);
4039
4004
  });
4040
4005
  assertNotCancelled(decision);
4041
4006
  if (decision === "cancel") {
4042
- clack12.outro("Sync cancelled. No files were written.");
4007
+ clack13.outro("Sync cancelled. No files were written.");
4043
4008
  return;
4044
4009
  }
4045
4010
  if (decision === "customize") {
@@ -4060,30 +4025,30 @@ ${import_chalk15.default.bold("Changes:")}`);
4060
4025
  });
4061
4026
  applyRuleOverrides(merged, overrides);
4062
4027
  const recompacted = (0, import_config11.compactConfig)(merged);
4063
- fs21.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4028
+ fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4064
4029
  `);
4065
4030
  writeGeneratedFiles(projectRoot, merged, scanResult);
4066
- clack12.log.success("Updated config with your customizations.");
4067
- clack12.outro("Done! Run viberails check to verify.");
4031
+ clack13.log.success("Updated config with your customizations.");
4032
+ clack13.outro("Done! Run viberails check to verify.");
4068
4033
  return;
4069
4034
  }
4070
4035
  }
4071
- fs21.writeFileSync(configPath, `${compactedJson}
4036
+ fs22.writeFileSync(configPath, `${compactedJson}
4072
4037
  `);
4073
4038
  writeGeneratedFiles(projectRoot, merged, scanResult);
4074
4039
  console.log(`
4075
- ${import_chalk15.default.bold("Synced:")}`);
4040
+ ${import_chalk14.default.bold("Synced:")}`);
4076
4041
  if (configChanged) {
4077
- console.log(` ${import_chalk15.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4042
+ console.log(` ${import_chalk14.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4078
4043
  } else {
4079
- console.log(` ${import_chalk15.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4044
+ console.log(` ${import_chalk14.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4080
4045
  }
4081
- console.log(` ${import_chalk15.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4082
- console.log(` ${import_chalk15.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
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`);
4083
4048
  }
4084
4049
 
4085
4050
  // src/index.ts
4086
- var VERSION = "0.6.5";
4051
+ var VERSION = "0.6.6";
4087
4052
  var program = new import_commander.Command();
4088
4053
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
4089
4054
  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) => {
@@ -4091,7 +4056,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
4091
4056
  await initCommand(options);
4092
4057
  } catch (err) {
4093
4058
  const message = err instanceof Error ? err.message : String(err);
4094
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4059
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4095
4060
  process.exit(1);
4096
4061
  }
4097
4062
  });
@@ -4100,7 +4065,7 @@ program.command("sync").description("Re-scan and update generated files").option
4100
4065
  await syncCommand(options);
4101
4066
  } catch (err) {
4102
4067
  const message = err instanceof Error ? err.message : String(err);
4103
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4068
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4104
4069
  process.exit(1);
4105
4070
  }
4106
4071
  });
@@ -4109,7 +4074,7 @@ program.command("config").description("Interactively edit existing config rules"
4109
4074
  await configCommand(options);
4110
4075
  } catch (err) {
4111
4076
  const message = err instanceof Error ? err.message : String(err);
4112
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4077
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4113
4078
  process.exit(1);
4114
4079
  }
4115
4080
  });
@@ -4130,7 +4095,7 @@ program.command("check").description("Check files against enforced rules").optio
4130
4095
  process.exit(exitCode);
4131
4096
  } catch (err) {
4132
4097
  const message = err instanceof Error ? err.message : String(err);
4133
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4098
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4134
4099
  process.exit(1);
4135
4100
  }
4136
4101
  }
@@ -4141,7 +4106,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
4141
4106
  process.exit(exitCode);
4142
4107
  } catch (err) {
4143
4108
  const message = err instanceof Error ? err.message : String(err);
4144
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4109
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4145
4110
  process.exit(1);
4146
4111
  }
4147
4112
  });
@@ -4150,7 +4115,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
4150
4115
  await boundariesCommand(options);
4151
4116
  } catch (err) {
4152
4117
  const message = err instanceof Error ? err.message : String(err);
4153
- console.error(`${import_chalk16.default.red("Error:")} ${message}`);
4118
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
4154
4119
  process.exit(1);
4155
4120
  }
4156
4121
  });