viberails 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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,85 @@ 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_chalk16 = __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__";
87
+ var HINT_NOT_SET = "not set";
88
+ var HINT_NO_OVERRIDES = "no overrides";
89
+ var HINT_AUTO_DETECT = "auto-detect";
201
90
 
202
91
  // src/utils/prompt-submenus.ts
92
+ var clack = __toESM(require("@clack/prompts"), 1);
93
+ var FILE_NAMING_OPTIONS = [
94
+ { value: "kebab-case", label: "kebab-case" },
95
+ { value: "camelCase", label: "camelCase" },
96
+ { value: "PascalCase", label: "PascalCase" },
97
+ { value: "snake_case", label: "snake_case" }
98
+ ];
99
+ var COMPONENT_NAMING_OPTIONS = [
100
+ { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
101
+ { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
102
+ ];
103
+ var HOOK_NAMING_OPTIONS = [
104
+ { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
105
+ { value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
106
+ ];
203
107
  async function promptFileLimitsMenu(state) {
204
108
  while (true) {
205
- const choice = await clack2.select({
109
+ const choice = await clack.select({
206
110
  message: "File limits",
207
111
  options: [
208
112
  { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
@@ -214,10 +118,9 @@ async function promptFileLimitsMenu(state) {
214
118
  { value: "back", label: "Back" }
215
119
  ]
216
120
  });
217
- assertNotCancelled(choice);
218
- if (choice === "back") return;
121
+ if (isCancelled(choice) || choice === "back") return;
219
122
  if (choice === "maxFileLines") {
220
- const result = await clack2.text({
123
+ const result = await clack.text({
221
124
  message: "Maximum lines per source file?",
222
125
  initialValue: String(state.maxFileLines),
223
126
  validate: (v) => {
@@ -226,11 +129,11 @@ async function promptFileLimitsMenu(state) {
226
129
  if (Number.isNaN(n) || n < 1) return "Enter a positive number";
227
130
  }
228
131
  });
229
- assertNotCancelled(result);
132
+ if (isCancelled(result)) continue;
230
133
  state.maxFileLines = Number.parseInt(result, 10);
231
134
  }
232
135
  if (choice === "maxTestFileLines") {
233
- const result = await clack2.text({
136
+ const result = await clack.text({
234
137
  message: "Maximum lines per test file (0 to disable)?",
235
138
  initialValue: String(state.maxTestFileLines),
236
139
  validate: (v) => {
@@ -239,7 +142,7 @@ async function promptFileLimitsMenu(state) {
239
142
  if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
240
143
  }
241
144
  });
242
- assertNotCancelled(result);
145
+ if (isCancelled(result)) continue;
243
146
  state.maxTestFileLines = Number.parseInt(result, 10);
244
147
  }
245
148
  }
@@ -257,57 +160,56 @@ async function promptNamingMenu(state) {
257
160
  options.push({
258
161
  value: "fileNaming",
259
162
  label: "File naming convention",
260
- hint: state.fileNamingValue ?? "(not set)"
163
+ hint: state.fileNamingValue ?? HINT_NOT_SET
261
164
  });
262
165
  }
263
166
  options.push(
264
167
  {
265
168
  value: "componentNaming",
266
169
  label: "Component naming",
267
- hint: state.componentNaming ?? "(not set)"
170
+ hint: state.componentNaming ?? HINT_NOT_SET
268
171
  },
269
172
  {
270
173
  value: "hookNaming",
271
174
  label: "Hook naming",
272
- hint: state.hookNaming ?? "(not set)"
175
+ hint: state.hookNaming ?? HINT_NOT_SET
273
176
  },
274
177
  {
275
178
  value: "importAlias",
276
179
  label: "Import alias",
277
- hint: state.importAlias ?? "(not set)"
180
+ hint: state.importAlias ?? HINT_NOT_SET
278
181
  },
279
182
  { value: "back", label: "Back" }
280
183
  );
281
- const choice = await clack2.select({ message: "Naming & conventions", options });
282
- assertNotCancelled(choice);
283
- if (choice === "back") return;
184
+ const choice = await clack.select({ message: "Naming & conventions", options });
185
+ if (isCancelled(choice) || choice === "back") return;
284
186
  if (choice === "enforceNaming") {
285
- const result = await clack2.confirm({
187
+ const result = await clack.confirm({
286
188
  message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
287
189
  initialValue: state.enforceNaming
288
190
  });
289
- assertNotCancelled(result);
191
+ if (isCancelled(result)) continue;
290
192
  if (result && !state.fileNamingValue) {
291
- const selected = await clack2.select({
193
+ const selected = await clack.select({
292
194
  message: "Which file naming convention should be enforced?",
293
195
  options: [...FILE_NAMING_OPTIONS]
294
196
  });
295
- assertNotCancelled(selected);
197
+ if (isCancelled(selected)) continue;
296
198
  state.fileNamingValue = selected;
297
199
  }
298
200
  state.enforceNaming = result;
299
201
  }
300
202
  if (choice === "fileNaming") {
301
- const selected = await clack2.select({
203
+ const selected = await clack.select({
302
204
  message: "Which file naming convention should be enforced?",
303
205
  options: [...FILE_NAMING_OPTIONS],
304
206
  initialValue: state.fileNamingValue
305
207
  });
306
- assertNotCancelled(selected);
208
+ if (isCancelled(selected)) continue;
307
209
  state.fileNamingValue = selected;
308
210
  }
309
211
  if (choice === "componentNaming") {
310
- const selected = await clack2.select({
212
+ const selected = await clack.select({
311
213
  message: "Component naming convention",
312
214
  options: [
313
215
  ...COMPONENT_NAMING_OPTIONS,
@@ -315,11 +217,11 @@ async function promptNamingMenu(state) {
315
217
  ],
316
218
  initialValue: state.componentNaming ?? SENTINEL_CLEAR
317
219
  });
318
- assertNotCancelled(selected);
220
+ if (isCancelled(selected)) continue;
319
221
  state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
320
222
  }
321
223
  if (choice === "hookNaming") {
322
- const selected = await clack2.select({
224
+ const selected = await clack.select({
323
225
  message: "Hook naming convention",
324
226
  options: [
325
227
  ...HOOK_NAMING_OPTIONS,
@@ -327,11 +229,11 @@ async function promptNamingMenu(state) {
327
229
  ],
328
230
  initialValue: state.hookNaming ?? SENTINEL_CLEAR
329
231
  });
330
- assertNotCancelled(selected);
232
+ if (isCancelled(selected)) continue;
331
233
  state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
332
234
  }
333
235
  if (choice === "importAlias") {
334
- const selected = await clack2.select({
236
+ const selected = await clack.select({
335
237
  message: "Import alias pattern",
336
238
  options: [
337
239
  { value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
@@ -341,11 +243,11 @@ async function promptNamingMenu(state) {
341
243
  ],
342
244
  initialValue: state.importAlias ?? SENTINEL_CLEAR
343
245
  });
344
- assertNotCancelled(selected);
246
+ if (isCancelled(selected)) continue;
345
247
  if (selected === SENTINEL_CLEAR) {
346
248
  state.importAlias = void 0;
347
249
  } else if (selected === SENTINEL_CUSTOM) {
348
- const result = await clack2.text({
250
+ const result = await clack.text({
349
251
  message: "Custom import alias (e.g. #/*)?",
350
252
  initialValue: state.importAlias ?? "",
351
253
  placeholder: "e.g. #/*",
@@ -355,7 +257,7 @@ async function promptNamingMenu(state) {
355
257
  return "Must match pattern like @/*, ~/*, or #src/*";
356
258
  }
357
259
  });
358
- assertNotCancelled(result);
260
+ if (isCancelled(result)) continue;
359
261
  state.importAlias = result.trim();
360
262
  } else {
361
263
  state.importAlias = selected;
@@ -392,19 +294,18 @@ async function promptTestingMenu(state) {
392
294
  );
393
295
  }
394
296
  options.push({ value: "back", label: "Back" });
395
- const choice = await clack2.select({ message: "Testing & coverage", options });
396
- assertNotCancelled(choice);
397
- if (choice === "back") return;
297
+ const choice = await clack.select({ message: "Testing & coverage", options });
298
+ if (isCancelled(choice) || 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
  });
403
- assertNotCancelled(result);
304
+ if (isCancelled(result)) continue;
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) => {
@@ -413,55 +314,32 @@ async function promptTestingMenu(state) {
413
314
  if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
414
315
  }
415
316
  });
416
- assertNotCancelled(result);
317
+ if (isCancelled(result)) continue;
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) => {
424
325
  if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
425
326
  }
426
327
  });
427
- assertNotCancelled(result);
328
+ if (isCancelled(result)) continue;
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)"
435
336
  });
436
- assertNotCancelled(result);
337
+ if (isCancelled(result)) continue;
437
338
  const trimmed = result.trim();
438
339
  state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
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) {
@@ -500,13 +378,13 @@ function packageOverrideHint(pkg, defaults) {
500
378
  const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
501
379
  if (hasSummaryOverride) tags.push("summary override");
502
380
  if (hasCommandOverride) tags.push("command override");
503
- return tags.length > 0 ? tags.join(", ") : "(no overrides)";
381
+ return tags.length > 0 ? tags.join(", ") : HINT_NO_OVERRIDES;
504
382
  }
505
383
  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) => ({
@@ -517,8 +395,7 @@ async function promptPackageOverrides(packages, defaults) {
517
395
  { value: SENTINEL_DONE, label: "Done" }
518
396
  ]
519
397
  });
520
- assertNotCancelled(selectedPath);
521
- if (selectedPath === SENTINEL_DONE) break;
398
+ if (isCancelled(selectedPath) || selectedPath === SENTINEL_DONE) break;
522
399
  const target = editablePackages.find((pkg) => pkg.path === selectedPath);
523
400
  if (!target) continue;
524
401
  await promptSinglePackageOverrides(target, defaults);
@@ -532,12 +409,12 @@ async function promptSinglePackageOverrides(target, defaults) {
532
409
  const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
533
410
  const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
534
411
  const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
535
- const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
412
+ const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? HINT_AUTO_DETECT;
536
413
  const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
537
414
  const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
538
- const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
539
- const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
540
- const choice = await clack3.select({
415
+ const namingHint = hasNamingOverride ? String(effectiveNaming) : `inherits: ${effectiveNaming ?? "not set"}`;
416
+ const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `inherits: ${effectiveMaxLines}`;
417
+ const choice = await clack2.select({
541
418
  message: `Edit overrides for ${target.path}`,
542
419
  options: [
543
420
  { value: "fileNaming", label: "File naming", hint: namingHint },
@@ -549,10 +426,9 @@ async function promptSinglePackageOverrides(target, defaults) {
549
426
  { value: "back", label: "Back to package list" }
550
427
  ]
551
428
  });
552
- assertNotCancelled(choice);
553
- if (choice === "back") break;
429
+ if (isCancelled(choice) || choice === "back") break;
554
430
  if (choice === "fileNaming") {
555
- const selected = await clack3.select({
431
+ const selected = await clack2.select({
556
432
  message: `File naming for ${target.path}`,
557
433
  options: [
558
434
  ...FILE_NAMING_OPTIONS,
@@ -564,7 +440,7 @@ async function promptSinglePackageOverrides(target, defaults) {
564
440
  ],
565
441
  initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
566
442
  });
567
- assertNotCancelled(selected);
443
+ if (isCancelled(selected)) continue;
568
444
  if (selected === SENTINEL_INHERIT) {
569
445
  if (target.conventions) delete target.conventions.fileNaming;
570
446
  } else if (selected === SENTINEL_NONE) {
@@ -574,12 +450,12 @@ async function promptSinglePackageOverrides(target, defaults) {
574
450
  }
575
451
  }
576
452
  if (choice === "maxFileLines") {
577
- const result = await clack3.text({
453
+ const result = await clack2.text({
578
454
  message: `Max file lines for ${target.path} (blank to inherit default)?`,
579
455
  initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
580
456
  placeholder: String(defaults.maxFileLines)
581
457
  });
582
- assertNotCancelled(result);
458
+ if (isCancelled(result)) continue;
583
459
  const value = result.trim();
584
460
  if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
585
461
  if (target.rules) delete target.rules.maxFileLines;
@@ -588,7 +464,7 @@ async function promptSinglePackageOverrides(target, defaults) {
588
464
  }
589
465
  }
590
466
  if (choice === "testCoverage") {
591
- const result = await clack3.text({
467
+ const result = await clack2.text({
592
468
  message: "Package testCoverage (0 to exempt package)?",
593
469
  initialValue: String(effectiveCoverage),
594
470
  validate: (v) => {
@@ -597,7 +473,7 @@ async function promptSinglePackageOverrides(target, defaults) {
597
473
  if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
598
474
  }
599
475
  });
600
- assertNotCancelled(result);
476
+ if (isCancelled(result)) continue;
601
477
  const nextCoverage = Number.parseInt(result, 10);
602
478
  if (nextCoverage === defaults.testCoverage) {
603
479
  if (target.rules) delete target.rules.testCoverage;
@@ -606,12 +482,12 @@ async function promptSinglePackageOverrides(target, defaults) {
606
482
  }
607
483
  }
608
484
  if (choice === "summaryPath") {
609
- const result = await clack3.text({
485
+ const result = await clack2.text({
610
486
  message: "Path to coverage summary file (blank to inherit default)?",
611
487
  initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
612
488
  placeholder: defaults.coverageSummaryPath
613
489
  });
614
- assertNotCancelled(result);
490
+ if (isCancelled(result)) continue;
615
491
  const value = result.trim();
616
492
  if (value.length === 0 || value === defaults.coverageSummaryPath) {
617
493
  if (target.coverage) delete target.coverage.summaryPath;
@@ -620,12 +496,12 @@ async function promptSinglePackageOverrides(target, defaults) {
620
496
  }
621
497
  }
622
498
  if (choice === "command") {
623
- const result = await clack3.text({
499
+ const result = await clack2.text({
624
500
  message: "Coverage command (blank to auto-detect)?",
625
501
  initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
626
502
  placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
627
503
  });
628
- assertNotCancelled(result);
504
+ if (isCancelled(result)) continue;
629
505
  const value = result.trim();
630
506
  const defaultCommand = defaults.coverageCommand ?? "";
631
507
  if (value.length === 0 || value === defaultCommand) {
@@ -644,16 +520,6 @@ async function promptSinglePackageOverrides(target, defaults) {
644
520
  }
645
521
  }
646
522
  }
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
523
 
658
524
  // src/utils/prompt-menu-handlers.ts
659
525
  function getPackageDiffs(pkg, root) {
@@ -695,11 +561,11 @@ function getPackageDiffs(pkg, root) {
695
561
  return diffs;
696
562
  }
697
563
  function buildMenuOptions(state, packageCount) {
698
- const fileLimitsHint = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
564
+ const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
699
565
  const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
700
566
  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
567
  const options = [
702
- { value: "fileLimits", label: "File limits", hint: fileLimitsHint },
568
+ { value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
703
569
  { value: "naming", label: "Naming & conventions", hint: namingHint },
704
570
  { value: "testing", label: "Testing & coverage", hint: testingHint }
705
571
  ];
@@ -733,7 +599,7 @@ async function handleMenuChoice(choice, state, defaults, root) {
733
599
  state.coverageSummaryPath = defaults.coverageSummaryPath;
734
600
  state.coverageCommand = defaults.coverageCommand;
735
601
  state.packageOverrides = clonePackages(defaults.packageOverrides);
736
- clack4.log.info("Reset all rules to detected defaults.");
602
+ clack3.log.info("Reset all rules to detected defaults.");
737
603
  return;
738
604
  }
739
605
  if (choice === "fileLimits") {
@@ -761,21 +627,12 @@ async function handleMenuChoice(choice, state, defaults, root) {
761
627
  const lines = packageDiffs.map((entry) => `${entry.pkg.path}
762
628
  ${entry.diffs.join(", ")}`);
763
629
  if (lines.length > 0) {
764
- clack4.note(lines.join("\n\n"), "Existing package differences");
630
+ clack3.note(lines.join("\n\n"), "Existing package differences");
765
631
  }
766
632
  }
767
633
  return;
768
634
  }
769
635
  }
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
636
 
780
637
  // src/utils/prompt-rules.ts
781
638
  async function promptRuleMenu(defaults) {
@@ -787,7 +644,7 @@ async function promptRuleMenu(defaults) {
787
644
  const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
788
645
  while (true) {
789
646
  const options = buildMenuOptions(state, packageCount);
790
- const choice = await clack5.select({ message: "Customize rules", options });
647
+ const choice = await clack4.select({ message: "Customize rules", options });
791
648
  assertNotCancelled(choice);
792
649
  if (choice === "done") break;
793
650
  await handleMenuChoice(choice, state, defaults, root);
@@ -807,36 +664,29 @@ async function promptRuleMenu(defaults) {
807
664
  packageOverrides: state.packageOverrides
808
665
  };
809
666
  }
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
667
 
821
668
  // src/utils/prompt.ts
822
669
  function assertNotCancelled(value) {
823
- if (clack6.isCancel(value)) {
824
- clack6.cancel("Setup cancelled.");
670
+ if (clack5.isCancel(value)) {
671
+ clack5.cancel("Setup cancelled.");
825
672
  process.exit(0);
826
673
  }
827
674
  }
675
+ function isCancelled(value) {
676
+ return clack5.isCancel(value);
677
+ }
828
678
  async function confirm3(message) {
829
- const result = await clack6.confirm({ message, initialValue: true });
679
+ const result = await clack5.confirm({ message, initialValue: true });
830
680
  assertNotCancelled(result);
831
681
  return result;
832
682
  }
833
683
  async function confirmDangerous(message) {
834
- const result = await clack6.confirm({ message, initialValue: false });
684
+ const result = await clack5.confirm({ message, initialValue: false });
835
685
  assertNotCancelled(result);
836
686
  return result;
837
687
  }
838
688
  async function promptExistingConfigAction(configFile) {
839
- const result = await clack6.select({
689
+ const result = await clack5.select({
840
690
  message: `${configFile} already exists. What do you want to do?`,
841
691
  options: [
842
692
  {
@@ -859,134 +709,6 @@ async function promptExistingConfigAction(configFile) {
859
709
  assertNotCancelled(result);
860
710
  return result;
861
711
  }
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
712
 
991
713
  // src/utils/resolve-workspace-packages.ts
992
714
  var fs2 = __toESM(require("fs"), 1);
@@ -1179,7 +901,7 @@ function resolveIgnoreForFile(relPath, config) {
1179
901
  }
1180
902
 
1181
903
  // src/commands/check-coverage.ts
1182
- var import_node_child_process2 = require("child_process");
904
+ var import_node_child_process = require("child_process");
1183
905
  var fs4 = __toESM(require("fs"), 1);
1184
906
  var path4 = __toESM(require("path"), 1);
1185
907
  var import_config3 = require("@viberails/config");
@@ -1222,7 +944,7 @@ function readCoveragePercentage(summaryPath) {
1222
944
  }
1223
945
  }
1224
946
  function runCoverageCommand(pkgRoot, command) {
1225
- const result = (0, import_node_child_process2.spawnSync)(command, {
947
+ const result = (0, import_node_child_process.spawnSync)(command, {
1226
948
  cwd: pkgRoot,
1227
949
  shell: true,
1228
950
  encoding: "utf-8",
@@ -1317,7 +1039,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
1317
1039
  }
1318
1040
 
1319
1041
  // src/commands/check-files.ts
1320
- var import_node_child_process3 = require("child_process");
1042
+ var import_node_child_process2 = require("child_process");
1321
1043
  var fs5 = __toESM(require("fs"), 1);
1322
1044
  var path5 = __toESM(require("path"), 1);
1323
1045
  var import_config4 = require("@viberails/config");
@@ -1390,7 +1112,7 @@ function checkNaming(relPath, conventions) {
1390
1112
  }
1391
1113
  function getStagedFiles(projectRoot) {
1392
1114
  try {
1393
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1115
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1394
1116
  cwd: projectRoot,
1395
1117
  encoding: "utf-8",
1396
1118
  stdio: ["ignore", "pipe", "ignore"]
@@ -1402,12 +1124,12 @@ function getStagedFiles(projectRoot) {
1402
1124
  }
1403
1125
  function getDiffFiles(projectRoot, base) {
1404
1126
  try {
1405
- const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1127
+ const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1406
1128
  cwd: projectRoot,
1407
1129
  encoding: "utf-8",
1408
1130
  stdio: ["ignore", "pipe", "ignore"]
1409
1131
  });
1410
- const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1132
+ const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1411
1133
  cwd: projectRoot,
1412
1134
  encoding: "utf-8",
1413
1135
  stdio: ["ignore", "pipe", "ignore"]
@@ -1453,7 +1175,7 @@ function deletedTestFileToSourceFile(deletedTestFile, config) {
1453
1175
  }
1454
1176
  function getStagedDeletedTestSourceFiles(projectRoot, config) {
1455
1177
  try {
1456
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=D", {
1178
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=D", {
1457
1179
  cwd: projectRoot,
1458
1180
  encoding: "utf-8",
1459
1181
  stdio: ["ignore", "pipe", "ignore"]
@@ -1465,7 +1187,7 @@ function getStagedDeletedTestSourceFiles(projectRoot, config) {
1465
1187
  }
1466
1188
  function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
1467
1189
  try {
1468
- const output = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1190
+ const output = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1469
1191
  cwd: projectRoot,
1470
1192
  encoding: "utf-8",
1471
1193
  stdio: ["ignore", "pipe", "ignore"]
@@ -1691,9 +1413,9 @@ async function checkCommand(options, cwd) {
1691
1413
  }
1692
1414
  const violations = [];
1693
1415
  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)) : () => {
1416
+ const log9 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1695
1417
  };
1696
- log7(" Checking files...");
1418
+ log9(" Checking files...");
1697
1419
  for (const file of filesToCheck) {
1698
1420
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1699
1421
  const relPath = path7.relative(projectRoot, absPath);
@@ -1726,9 +1448,9 @@ async function checkCommand(options, cwd) {
1726
1448
  }
1727
1449
  }
1728
1450
  }
1729
- log7(" done\n");
1451
+ log9(" done\n");
1730
1452
  if (!options.files) {
1731
- log7(" Checking missing tests...");
1453
+ log9(" Checking missing tests...");
1732
1454
  const testViolations = checkMissingTests(projectRoot, config, severity);
1733
1455
  if (options.staged) {
1734
1456
  const stagedSet = new Set(filesToCheck);
@@ -1741,14 +1463,14 @@ async function checkCommand(options, cwd) {
1741
1463
  } else {
1742
1464
  violations.push(...testViolations);
1743
1465
  }
1744
- log7(" done\n");
1466
+ log9(" done\n");
1745
1467
  }
1746
1468
  if (!options.files && !options.staged && !options.diffBase) {
1747
- log7(" Running test coverage...\n");
1469
+ log9(" Running test coverage...\n");
1748
1470
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1749
1471
  staged: options.staged,
1750
1472
  enforce: options.enforce,
1751
- onProgress: (pkg) => log7(` Coverage: ${pkg}...
1473
+ onProgress: (pkg) => log9(` Coverage: ${pkg}...
1752
1474
  `)
1753
1475
  });
1754
1476
  violations.push(...coverageViolations);
@@ -1773,7 +1495,7 @@ async function checkCommand(options, cwd) {
1773
1495
  severity
1774
1496
  });
1775
1497
  }
1776
- log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1498
+ log9(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1777
1499
  `);
1778
1500
  }
1779
1501
  if (options.format === "json") {
@@ -1849,7 +1571,7 @@ async function hookCheckCommand(cwd) {
1849
1571
  // src/commands/config.ts
1850
1572
  var fs10 = __toESM(require("fs"), 1);
1851
1573
  var path9 = __toESM(require("path"), 1);
1852
- var clack7 = __toESM(require("@clack/prompts"), 1);
1574
+ var clack6 = __toESM(require("@clack/prompts"), 1);
1853
1575
  var import_config6 = require("@viberails/config");
1854
1576
  var import_scanner = require("@viberails/scanner");
1855
1577
  var import_chalk6 = __toESM(require("chalk"), 1);
@@ -2273,7 +1995,6 @@ function formatScanResultsText(scanResult) {
2273
1995
  }
2274
1996
 
2275
1997
  // src/utils/apply-rule-overrides.ts
2276
- init_get_root_package();
2277
1998
  function applyRuleOverrides(config, overrides) {
2278
1999
  if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
2279
2000
  const rootPkg = getRootPackage(config.packages);
@@ -2444,9 +2165,6 @@ function formatStatsDelta(oldStats, newStats) {
2444
2165
  return `${parts.join(", ")} since last sync`;
2445
2166
  }
2446
2167
 
2447
- // src/commands/config.ts
2448
- init_prompt();
2449
-
2450
2168
  // src/utils/write-generated-files.ts
2451
2169
  var fs9 = __toESM(require("fs"), 1);
2452
2170
  var path8 = __toESM(require("path"), 1);
@@ -2486,11 +2204,11 @@ async function configCommand(options, cwd) {
2486
2204
  return;
2487
2205
  }
2488
2206
  if (!options.suppressIntro) {
2489
- clack7.intro("viberails config");
2207
+ clack6.intro("viberails config");
2490
2208
  }
2491
2209
  const config = await (0, import_config6.loadConfig)(configPath);
2492
2210
  let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2493
- clack7.note(formatRulesText(config).join("\n"), "Current rules");
2211
+ clack6.note(formatRulesText(config).join("\n"), "Current rules");
2494
2212
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2495
2213
  const overrides = await promptRuleMenu({
2496
2214
  maxFileLines: config.rules.maxFileLines,
@@ -2510,7 +2228,7 @@ async function configCommand(options, cwd) {
2510
2228
  if (options.rescan && config.packages.length > 1) {
2511
2229
  const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
2512
2230
  if (shouldInfer) {
2513
- const bs = clack7.spinner();
2231
+ const bs = clack6.spinner();
2514
2232
  bs.start("Building import graph...");
2515
2233
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2516
2234
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -2528,29 +2246,29 @@ async function configCommand(options, cwd) {
2528
2246
  }
2529
2247
  const shouldWrite = await confirm3("Save updated configuration?");
2530
2248
  if (!shouldWrite) {
2531
- clack7.outro("No changes written.");
2249
+ clack6.outro("No changes written.");
2532
2250
  return;
2533
2251
  }
2534
2252
  const compacted = (0, import_config6.compactConfig)(config);
2535
2253
  fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2536
2254
  `);
2537
2255
  if (!scanResult) {
2538
- const s = clack7.spinner();
2256
+ const s = clack6.spinner();
2539
2257
  s.start("Scanning for context generation...");
2540
2258
  scanResult = await (0, import_scanner.scan)(projectRoot);
2541
2259
  s.stop("Scan complete");
2542
2260
  }
2543
2261
  writeGeneratedFiles(projectRoot, config, scanResult);
2544
- clack7.log.success(
2262
+ clack6.log.success(
2545
2263
  `Updated:
2546
2264
  ${CONFIG_FILE3}
2547
2265
  .viberails/context.md
2548
2266
  .viberails/scan-result.json`
2549
2267
  );
2550
- clack7.outro("Done! Run viberails check to verify.");
2268
+ clack6.outro("Done! Run viberails check to verify.");
2551
2269
  }
2552
2270
  async function rescanAndMerge(projectRoot, config) {
2553
- const s = clack7.spinner();
2271
+ const s = clack6.spinner();
2554
2272
  s.start("Re-scanning project...");
2555
2273
  const scanResult = await (0, import_scanner.scan)(projectRoot);
2556
2274
  const merged = (0, import_config6.mergeConfig)(config, scanResult);
@@ -2561,9 +2279,9 @@ async function rescanAndMerge(projectRoot, config) {
2561
2279
  const icon = c.type === "removed" ? "-" : "+";
2562
2280
  return `${icon} ${c.description}`;
2563
2281
  }).join("\n");
2564
- clack7.note(changeLines, "Changes detected");
2282
+ clack6.note(changeLines, "Changes detected");
2565
2283
  } else {
2566
- clack7.log.info("No new changes detected from scan.");
2284
+ clack6.log.info("No new changes detected from scan.");
2567
2285
  }
2568
2286
  Object.assign(config, merged);
2569
2287
  return scanResult;
@@ -2574,10 +2292,9 @@ var fs13 = __toESM(require("fs"), 1);
2574
2292
  var path13 = __toESM(require("path"), 1);
2575
2293
  var import_config7 = require("@viberails/config");
2576
2294
  var import_chalk8 = __toESM(require("chalk"), 1);
2577
- init_prompt();
2578
2295
 
2579
2296
  // src/commands/fix-helpers.ts
2580
- var import_node_child_process4 = require("child_process");
2297
+ var import_node_child_process3 = require("child_process");
2581
2298
  var import_chalk7 = __toESM(require("chalk"), 1);
2582
2299
  function printPlan(renames, stubs) {
2583
2300
  if (renames.length > 0) {
@@ -2595,7 +2312,7 @@ function printPlan(renames, stubs) {
2595
2312
  }
2596
2313
  function checkGitDirty(projectRoot) {
2597
2314
  try {
2598
- const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
2315
+ const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
2599
2316
  cwd: projectRoot,
2600
2317
  encoding: "utf-8",
2601
2318
  stdio: ["ignore", "pipe", "ignore"]
@@ -3058,161 +2775,42 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
3058
2775
  }
3059
2776
 
3060
2777
  // 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);
2778
+ var fs21 = __toESM(require("fs"), 1);
2779
+ var path21 = __toESM(require("path"), 1);
2780
+ var clack13 = __toESM(require("@clack/prompts"), 1);
3064
2781
  var import_config9 = require("@viberails/config");
3065
2782
  var import_scanner3 = require("@viberails/scanner");
3066
2783
  var import_chalk14 = __toESM(require("chalk"), 1);
3067
2784
 
3068
- // src/display-init.ts
3069
- var import_types6 = require("@viberails/types");
2785
+ // src/utils/check-prerequisites.ts
2786
+ var fs14 = __toESM(require("fs"), 1);
2787
+ var path14 = __toESM(require("path"), 1);
2788
+ var clack7 = __toESM(require("@clack/prompts"), 1);
3070
2789
  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
2790
+
2791
+ // src/utils/spawn-async.ts
2792
+ var import_node_child_process4 = require("child_process");
2793
+ function spawnAsync(command, cwd) {
2794
+ return new Promise((resolve4) => {
2795
+ const child = (0, import_node_child_process4.spawn)(command, { cwd, shell: true, stdio: "pipe" });
2796
+ let stdout = "";
2797
+ let stderr = "";
2798
+ child.stdout.on("data", (d) => {
2799
+ stdout += d.toString();
2800
+ });
2801
+ child.stderr.on("data", (d) => {
2802
+ stderr += d.toString();
2803
+ });
2804
+ child.on("close", (status) => {
2805
+ resolve4({ status, stdout, stderr });
2806
+ });
2807
+ child.on("error", () => {
2808
+ resolve4({ status: 1, stdout, stderr });
2809
+ });
3194
2810
  });
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
2811
  }
3208
2812
 
3209
2813
  // 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
2814
  function checkCoveragePrereqs(projectRoot, scanResult) {
3217
2815
  const pm = scanResult.stack.packageManager.name;
3218
2816
  const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
@@ -3243,116 +2841,572 @@ function displayMissingPrereqs(prereqs) {
3243
2841
  const missing = prereqs.filter((p) => !p.installed);
3244
2842
  for (const m of missing) {
3245
2843
  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}`);
2844
+ console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
3247
2845
  if (m.installCommand) {
3248
- console.log(` Install: ${import_chalk10.default.cyan(m.installCommand)}`);
2846
+ console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
3249
2847
  }
3250
2848
  }
3251
2849
  }
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,
2850
+ function planCoverageInstall(prereqs) {
2851
+ const missing = prereqs.find((p) => !p.installed && p.installCommand);
2852
+ if (!missing?.installCommand) return void 0;
2853
+ return {
2854
+ label: missing.label,
2855
+ command: missing.installCommand
2856
+ };
2857
+ }
2858
+ function hasDependency(projectRoot, name) {
2859
+ try {
2860
+ const pkgPath = path14.join(projectRoot, "package.json");
2861
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2862
+ return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2863
+ } catch {
2864
+ return false;
2865
+ }
2866
+ }
2867
+
2868
+ // src/utils/deferred-install.ts
2869
+ var clack8 = __toESM(require("@clack/prompts"), 1);
2870
+ async function executeDeferredInstalls(projectRoot, installs) {
2871
+ if (installs.length === 0) return 0;
2872
+ let successCount = 0;
2873
+ for (const install of installs) {
2874
+ const s = clack8.spinner();
2875
+ s.start(`Installing ${install.label}...`);
2876
+ const result = await spawnAsync(install.command, projectRoot);
2877
+ if (result.status === 0) {
2878
+ s.stop(`Installed ${install.label}`);
2879
+ install.onSuccess?.();
2880
+ successCount++;
2881
+ } else {
2882
+ s.stop(`Failed to install ${install.label}`);
2883
+ clack8.log.warn(`Install manually: ${install.command}`);
2884
+ install.onFailure?.();
2885
+ }
2886
+ }
2887
+ return successCount;
2888
+ }
2889
+
2890
+ // src/utils/prompt-main-menu.ts
2891
+ var clack11 = __toESM(require("@clack/prompts"), 1);
2892
+
2893
+ // src/utils/prompt-main-menu-handlers.ts
2894
+ var clack10 = __toESM(require("@clack/prompts"), 1);
2895
+
2896
+ // src/utils/prompt-integrations.ts
2897
+ var fs15 = __toESM(require("fs"), 1);
2898
+ var path15 = __toESM(require("path"), 1);
2899
+ var clack9 = __toESM(require("@clack/prompts"), 1);
2900
+ function buildLefthookInstallCommand(pm, isWorkspace) {
2901
+ if (pm === "yarn") return "yarn add -D lefthook";
2902
+ if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
2903
+ if (pm === "npm") return "npm install -D lefthook";
2904
+ return `${pm} add -D lefthook`;
2905
+ }
2906
+ async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
2907
+ const options = [];
2908
+ const needsLefthook = !hookManager;
2909
+ if (needsLefthook) {
2910
+ const pm = packageManager ?? "npm";
2911
+ options.push({
2912
+ value: "installLefthook",
2913
+ label: "Install Lefthook",
2914
+ hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
2915
+ });
2916
+ }
2917
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
2918
+ const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
2919
+ options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
2920
+ if (tools?.isTypeScript) {
2921
+ options.push({
2922
+ value: "typecheck",
2923
+ label: "Typecheck (tsc --noEmit)",
2924
+ hint: "pre-commit hook + CI check"
2925
+ });
2926
+ }
2927
+ if (tools?.linter) {
2928
+ const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
2929
+ options.push({
2930
+ value: "lint",
2931
+ label: `Lint check (${linterName})`,
2932
+ hint: "pre-commit hook + CI check"
2933
+ });
2934
+ }
2935
+ options.push(
2936
+ {
2937
+ value: "claude",
2938
+ label: "Claude Code hook",
2939
+ hint: "checks files when Claude edits them"
2940
+ },
2941
+ {
2942
+ value: "claudeMd",
2943
+ label: "CLAUDE.md reference",
2944
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
2945
+ },
2946
+ {
2947
+ value: "githubAction",
2948
+ label: "GitHub Actions workflow",
2949
+ hint: "blocks PRs that fail viberails check"
2950
+ }
2951
+ );
2952
+ const initialValues = options.map((o) => o.value);
2953
+ const result = await clack9.multiselect({
2954
+ message: "Integrations",
2955
+ options,
2956
+ initialValues,
2957
+ required: false
2958
+ });
2959
+ assertNotCancelled(result);
2960
+ let lefthookInstall;
2961
+ if (needsLefthook && result.includes("installLefthook")) {
2962
+ const pm = packageManager ?? "npm";
2963
+ lefthookInstall = {
2964
+ label: "Lefthook",
2965
+ command: buildLefthookInstallCommand(pm, isWorkspace),
2966
+ onSuccess: projectRoot ? () => {
2967
+ const ymlPath = path15.join(projectRoot, "lefthook.yml");
2968
+ if (!fs15.existsSync(ymlPath)) {
2969
+ fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
2970
+ }
2971
+ } : void 0
2972
+ };
2973
+ }
2974
+ return {
2975
+ choice: {
2976
+ preCommitHook: result.includes("preCommit"),
2977
+ claudeCodeHook: result.includes("claude"),
2978
+ claudeMdRef: result.includes("claudeMd"),
2979
+ githubAction: result.includes("githubAction"),
2980
+ typecheckHook: result.includes("typecheck"),
2981
+ lintHook: result.includes("lint")
2982
+ },
2983
+ lefthookInstall
2984
+ };
2985
+ }
2986
+
2987
+ // src/utils/prompt-main-menu-handlers.ts
2988
+ async function handleAdvancedNaming(config) {
2989
+ const rootPkg = getRootPackage(config.packages);
2990
+ const state = {
2991
+ maxFileLines: config.rules.maxFileLines,
2992
+ maxTestFileLines: config.rules.maxTestFileLines,
2993
+ testCoverage: config.rules.testCoverage,
2994
+ enforceMissingTests: config.rules.enforceMissingTests,
2995
+ enforceNaming: config.rules.enforceNaming,
2996
+ fileNamingValue: rootPkg.conventions?.fileNaming,
2997
+ componentNaming: rootPkg.conventions?.componentNaming,
2998
+ hookNaming: rootPkg.conventions?.hookNaming,
2999
+ importAlias: rootPkg.conventions?.importAlias,
3000
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3001
+ coverageCommand: config.defaults?.coverage?.command
3002
+ };
3003
+ await promptNamingMenu(state);
3004
+ rootPkg.conventions = rootPkg.conventions ?? {};
3005
+ config.rules.enforceNaming = state.enforceNaming;
3006
+ if (state.fileNamingValue) {
3007
+ rootPkg.conventions.fileNaming = state.fileNamingValue;
3008
+ } else {
3009
+ delete rootPkg.conventions.fileNaming;
3010
+ }
3011
+ rootPkg.conventions.componentNaming = state.componentNaming || void 0;
3012
+ rootPkg.conventions.hookNaming = state.hookNaming || void 0;
3013
+ rootPkg.conventions.importAlias = state.importAlias || void 0;
3014
+ }
3015
+ async function handleFileNaming(config, scanResult) {
3016
+ const isMonorepo = config.packages.length > 1;
3017
+ if (isMonorepo) {
3018
+ const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
3019
+ path: p.relativePath,
3020
+ naming: p.conventions.fileNaming
3021
+ }));
3022
+ if (pkgData.length > 0) {
3023
+ const lines = pkgData.map(
3024
+ (p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
3025
+ );
3026
+ clack10.note(lines.join("\n"), "Per-package file naming detected");
3027
+ }
3028
+ }
3029
+ const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
3030
+ if (isMonorepo) {
3031
+ const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
3032
+ const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
3033
+ return { value: opt.value, label: opt.label, hint };
3034
+ }
3035
+ return { value: opt.value, label: opt.label };
3036
+ });
3037
+ const rootPkg = getRootPackage(config.packages);
3038
+ const selected = await clack10.select({
3039
+ message: isMonorepo ? "Default file naming convention" : "File naming convention",
3040
+ options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
3041
+ initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
3042
+ });
3043
+ if (isCancelled(selected)) return;
3044
+ if (selected === SENTINEL_SKIP) {
3045
+ config.rules.enforceNaming = false;
3046
+ if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
3047
+ } else {
3048
+ config.rules.enforceNaming = true;
3049
+ rootPkg.conventions = rootPkg.conventions ?? {};
3050
+ rootPkg.conventions.fileNaming = selected;
3051
+ }
3052
+ }
3053
+ async function handleMissingTests(config) {
3054
+ const result = await clack10.confirm({
3055
+ message: "Require every source file to have a test file?",
3056
+ initialValue: config.rules.enforceMissingTests
3057
+ });
3058
+ if (isCancelled(result)) return;
3059
+ config.rules.enforceMissingTests = result;
3060
+ }
3061
+ async function handleCoverage(config, state, opts) {
3062
+ if (!opts.hasTestRunner) {
3063
+ clack10.note(
3064
+ "No test runner (vitest, jest, etc.) was detected.\nInstall one, then re-run viberails init to configure coverage.",
3065
+ "Coverage inactive"
3066
+ );
3067
+ return;
3068
+ }
3069
+ const planned = planCoverageInstall(opts.coveragePrereqs);
3070
+ if (planned) {
3071
+ const choice = await clack10.select({
3072
+ message: `${planned.label} is not installed. Needed for coverage checks.`,
3268
3073
  options: [
3269
3074
  {
3270
3075
  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"
3076
+ label: "Install (after final confirmation)",
3077
+ hint: planned.command
3278
3078
  },
3079
+ { value: "disable", label: "Disable coverage checks" },
3279
3080
  {
3280
3081
  value: "skip",
3281
3082
  label: "Skip for now",
3282
- hint: `install later: ${m.installCommand}`
3083
+ hint: `install later: ${planned.command}`
3283
3084
  }
3284
3085
  ]
3285
3086
  });
3286
- assertNotCancelled(choice);
3087
+ if (isCancelled(choice)) return;
3088
+ state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
3287
3089
  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
- }
3090
+ planned.onFailure = () => {
3091
+ config.rules.testCoverage = 0;
3092
+ };
3093
+ state.deferredInstalls.push(planned);
3300
3094
  } 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
- );
3095
+ config.rules.testCoverage = 0;
3096
+ return;
3308
3097
  }
3309
3098
  }
3310
- return { disableCoverage };
3099
+ const result = await clack10.text({
3100
+ message: "Test coverage target (0 = disable)?",
3101
+ initialValue: String(config.rules.testCoverage),
3102
+ validate: (v) => {
3103
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
3104
+ const n = Number.parseInt(v, 10);
3105
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
3106
+ }
3107
+ });
3108
+ if (isCancelled(result)) return;
3109
+ config.rules.testCoverage = Number.parseInt(result, 10);
3311
3110
  }
3312
- function hasDependency(projectRoot, name) {
3111
+ async function handlePackageOverrides(config) {
3112
+ const rootPkg = getRootPackage(config.packages);
3113
+ config.packages = await promptPackageOverrides(config.packages, {
3114
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3115
+ maxFileLines: config.rules.maxFileLines,
3116
+ testCoverage: config.rules.testCoverage,
3117
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3118
+ coverageCommand: config.defaults?.coverage?.command
3119
+ });
3120
+ normalizePackageOverrides(config.packages);
3121
+ }
3122
+ async function handleBoundaries(config, state, opts) {
3123
+ const shouldInfer = await clack10.confirm({
3124
+ message: "Infer boundary rules from current import patterns?",
3125
+ initialValue: false
3126
+ });
3127
+ if (isCancelled(shouldInfer)) return;
3128
+ state.visited.boundaries = true;
3129
+ if (!shouldInfer) {
3130
+ config.rules.enforceBoundaries = false;
3131
+ return;
3132
+ }
3133
+ const bs = clack10.spinner();
3134
+ bs.start("Building import graph...");
3313
3135
  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;
3136
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3137
+ const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
3138
+ const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
3139
+ const inferred = inferBoundaries(graph);
3140
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
3141
+ if (denyCount > 0) {
3142
+ config.boundaries = inferred;
3143
+ config.rules.enforceBoundaries = true;
3144
+ const pkgCount = Object.keys(inferred.deny).length;
3145
+ bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3146
+ } else {
3147
+ bs.stop("No boundary rules inferred");
3148
+ }
3149
+ } catch (err) {
3150
+ bs.stop("Failed to build import graph");
3151
+ clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
3152
+ }
3153
+ }
3154
+ async function handleIntegrations(state, opts) {
3155
+ const result = await promptIntegrationsDeferred(
3156
+ state.hookManager,
3157
+ opts.tools,
3158
+ opts.tools.packageManager,
3159
+ opts.tools.isWorkspace,
3160
+ opts.projectRoot
3161
+ );
3162
+ state.visited.integrations = true;
3163
+ state.integrations = result.choice;
3164
+ state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
3165
+ if (result.lefthookInstall) {
3166
+ state.deferredInstalls.push(result.lefthookInstall);
3319
3167
  }
3320
3168
  }
3321
3169
 
3322
- // src/commands/init.ts
3323
- init_prompt();
3170
+ // src/utils/prompt-main-menu-hints.ts
3171
+ var import_chalk10 = __toESM(require("chalk"), 1);
3172
+ function fileLimitsHint(config) {
3173
+ const max = config.rules.maxFileLines;
3174
+ const test = config.rules.maxTestFileLines;
3175
+ return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
3176
+ }
3177
+ function fileNamingHint(config, scanResult) {
3178
+ const rootPkg = getRootPackage(config.packages);
3179
+ const naming = rootPkg.conventions?.fileNaming;
3180
+ if (!config.rules.enforceNaming) return "not enforced";
3181
+ if (naming) {
3182
+ const detected = scanResult.packages.some(
3183
+ (p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
3184
+ );
3185
+ return detected ? `${naming} (detected)` : naming;
3186
+ }
3187
+ return "mixed \u2014 will not enforce if skipped";
3188
+ }
3189
+ function fileNamingStatus(config) {
3190
+ if (!config.rules.enforceNaming) return "disabled";
3191
+ const rootPkg = getRootPackage(config.packages);
3192
+ return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
3193
+ }
3194
+ function missingTestsHint(config) {
3195
+ if (!config.rules.enforceMissingTests) return "not enforced";
3196
+ const rootPkg = getRootPackage(config.packages);
3197
+ const pattern = rootPkg.structure?.testPattern;
3198
+ return pattern ? `enforced (${pattern})` : "enforced";
3199
+ }
3200
+ function coverageHint(config, hasTestRunner) {
3201
+ if (config.rules.testCoverage === 0) return "disabled";
3202
+ if (!hasTestRunner)
3203
+ return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3204
+ const isMonorepo = config.packages.length > 1;
3205
+ if (isMonorepo) {
3206
+ const withCov = config.packages.filter(
3207
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3208
+ );
3209
+ const exempt = config.packages.length - withCov.length;
3210
+ return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3211
+ }
3212
+ return `${config.rules.testCoverage}%`;
3213
+ }
3214
+ function advancedNamingHint(config) {
3215
+ const rootPkg = getRootPackage(config.packages);
3216
+ const parts = [];
3217
+ if (rootPkg.conventions?.componentNaming)
3218
+ parts.push(`${rootPkg.conventions.componentNaming} components`);
3219
+ if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
3220
+ if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
3221
+ return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
3222
+ }
3223
+ function integrationsHint(state) {
3224
+ if (!state.visited.integrations || !state.integrations)
3225
+ return "not configured \u2014 select to set up";
3226
+ const items = [];
3227
+ if (state.integrations.preCommitHook) items.push("pre-commit");
3228
+ if (state.integrations.typecheckHook) items.push("typecheck");
3229
+ if (state.integrations.lintHook) items.push("lint");
3230
+ if (state.integrations.claudeCodeHook) items.push("Claude");
3231
+ if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
3232
+ if (state.integrations.githubAction) items.push("CI");
3233
+ return items.length > 0 ? items.join(" \xB7 ") : "none selected";
3234
+ }
3235
+ function packageOverridesHint(config) {
3236
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3237
+ const editable = config.packages.filter((p) => p.path !== ".");
3238
+ const customized = editable.filter(
3239
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3240
+ ).length;
3241
+ return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3242
+ }
3243
+ function boundariesHint(config, state) {
3244
+ if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3245
+ const deny = config.boundaries?.deny;
3246
+ if (!deny) return "enabled";
3247
+ const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3248
+ const pkgCount = Object.keys(deny).length;
3249
+ return `${ruleCount} rules across ${pkgCount} packages`;
3250
+ }
3251
+ function advancedNamingStatus(config) {
3252
+ const rootPkg = getRootPackage(config.packages);
3253
+ const hasAny = !!rootPkg.conventions?.componentNaming || !!rootPkg.conventions?.hookNaming || !!rootPkg.conventions?.importAlias;
3254
+ return hasAny ? "ok" : "unconfigured";
3255
+ }
3256
+ function packageOverridesStatus(config) {
3257
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3258
+ const editable = config.packages.filter((p) => p.path !== ".");
3259
+ const customized = editable.some(
3260
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3261
+ );
3262
+ return customized ? "ok" : "unconfigured";
3263
+ }
3264
+ function statusIcon(status) {
3265
+ if (status === "ok") return import_chalk10.default.green("\u2713");
3266
+ if (status === "needs-input") return import_chalk10.default.yellow("?");
3267
+ if (status === "unconfigured") return import_chalk10.default.dim("-");
3268
+ return import_chalk10.default.yellow("~");
3269
+ }
3270
+ function buildMainMenuOptions(config, scanResult, state) {
3271
+ const namingStatus = fileNamingStatus(config);
3272
+ const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
3273
+ const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
3274
+ const options = [
3275
+ {
3276
+ value: "fileLimits",
3277
+ label: `${statusIcon("ok")} Max file size`,
3278
+ hint: fileLimitsHint(config)
3279
+ },
3280
+ {
3281
+ value: "fileNaming",
3282
+ label: `${statusIcon(namingStatus)} Default file naming`,
3283
+ hint: fileNamingHint(config, scanResult)
3284
+ },
3285
+ {
3286
+ value: "missingTests",
3287
+ label: `${statusIcon(missingTestsStatus)} Missing tests`,
3288
+ hint: missingTestsHint(config)
3289
+ },
3290
+ {
3291
+ value: "coverage",
3292
+ label: `${statusIcon(coverageStatus)} Coverage`,
3293
+ hint: coverageHint(config, state.hasTestRunner)
3294
+ },
3295
+ {
3296
+ value: "advancedNaming",
3297
+ label: `${statusIcon(advancedNamingStatus(config))} Advanced naming`,
3298
+ hint: advancedNamingHint(config)
3299
+ }
3300
+ ];
3301
+ if (config.packages.length > 1) {
3302
+ const bIcon = statusIcon(
3303
+ state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
3304
+ );
3305
+ const poIcon = statusIcon(packageOverridesStatus(config));
3306
+ options.push(
3307
+ {
3308
+ value: "packageOverrides",
3309
+ label: `${poIcon} Per-package overrides`,
3310
+ hint: packageOverridesHint(config)
3311
+ },
3312
+ { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3313
+ );
3314
+ }
3315
+ const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
3316
+ options.push(
3317
+ { value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
3318
+ { value: "reset", label: " Reset all to defaults" },
3319
+ { value: "review", label: " Review scan details" },
3320
+ { value: "done", label: " Done \u2014 write config" }
3321
+ );
3322
+ return options;
3323
+ }
3324
+
3325
+ // src/utils/prompt-main-menu.ts
3326
+ async function promptMainMenu(config, scanResult, opts) {
3327
+ const originalConfig = structuredClone(config);
3328
+ const state = {
3329
+ visited: { integrations: false, boundaries: false },
3330
+ deferredInstalls: [],
3331
+ hasTestRunner: opts.hasTestRunner,
3332
+ hookManager: opts.hookManager
3333
+ };
3334
+ while (true) {
3335
+ const options = buildMainMenuOptions(config, scanResult, state);
3336
+ const choice = await clack11.select({ message: "Configure viberails", options });
3337
+ assertNotCancelled(choice);
3338
+ if (choice === "done") {
3339
+ if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
3340
+ config.rules.enforceNaming = false;
3341
+ }
3342
+ break;
3343
+ }
3344
+ if (choice === "fileLimits") {
3345
+ const s = {
3346
+ maxFileLines: config.rules.maxFileLines,
3347
+ maxTestFileLines: config.rules.maxTestFileLines
3348
+ };
3349
+ await promptFileLimitsMenu(s);
3350
+ config.rules.maxFileLines = s.maxFileLines;
3351
+ config.rules.maxTestFileLines = s.maxTestFileLines;
3352
+ }
3353
+ if (choice === "fileNaming") await handleFileNaming(config, scanResult);
3354
+ if (choice === "missingTests") await handleMissingTests(config);
3355
+ if (choice === "coverage") await handleCoverage(config, state, opts);
3356
+ if (choice === "advancedNaming") await handleAdvancedNaming(config);
3357
+ if (choice === "packageOverrides") await handlePackageOverrides(config);
3358
+ if (choice === "boundaries") await handleBoundaries(config, state, opts);
3359
+ if (choice === "integrations") await handleIntegrations(state, opts);
3360
+ if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
3361
+ if (choice === "reset") {
3362
+ const confirmed = await clack11.confirm({
3363
+ message: "Reset all settings to scan-detected defaults?",
3364
+ initialValue: false
3365
+ });
3366
+ assertNotCancelled(confirmed);
3367
+ if (confirmed) {
3368
+ Object.assign(config, structuredClone(originalConfig));
3369
+ state.deferredInstalls = [];
3370
+ state.visited = { integrations: false, boundaries: false };
3371
+ state.integrations = void 0;
3372
+ clack11.log.info("Reset all settings to scan-detected defaults.");
3373
+ }
3374
+ }
3375
+ }
3376
+ return state;
3377
+ }
3324
3378
 
3325
3379
  // src/utils/update-gitignore.ts
3326
- var fs15 = __toESM(require("fs"), 1);
3327
- var path15 = __toESM(require("path"), 1);
3380
+ var fs16 = __toESM(require("fs"), 1);
3381
+ var path16 = __toESM(require("path"), 1);
3328
3382
  function updateGitignore(projectRoot) {
3329
- const gitignorePath = path15.join(projectRoot, ".gitignore");
3383
+ const gitignorePath = path16.join(projectRoot, ".gitignore");
3330
3384
  let content = "";
3331
- if (fs15.existsSync(gitignorePath)) {
3332
- content = fs15.readFileSync(gitignorePath, "utf-8");
3385
+ if (fs16.existsSync(gitignorePath)) {
3386
+ content = fs16.readFileSync(gitignorePath, "utf-8");
3333
3387
  }
3334
3388
  if (!content.includes(".viberails/scan-result.json")) {
3335
3389
  const block = "\n# viberails\n.viberails/scan-result.json\n";
3336
3390
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
3337
3391
  `;
3338
- fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
3392
+ fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
3339
3393
  }
3340
3394
  }
3341
3395
 
3342
3396
  // src/commands/init-hooks.ts
3343
- var fs17 = __toESM(require("fs"), 1);
3344
- var path17 = __toESM(require("path"), 1);
3397
+ var fs18 = __toESM(require("fs"), 1);
3398
+ var path18 = __toESM(require("path"), 1);
3345
3399
  var import_chalk11 = __toESM(require("chalk"), 1);
3346
3400
  var import_yaml = require("yaml");
3347
3401
 
3348
3402
  // src/commands/resolve-typecheck.ts
3349
- var fs16 = __toESM(require("fs"), 1);
3350
- var path16 = __toESM(require("path"), 1);
3403
+ var fs17 = __toESM(require("fs"), 1);
3404
+ var path17 = __toESM(require("path"), 1);
3351
3405
  function hasTurboTask(projectRoot, taskName) {
3352
- const turboPath = path16.join(projectRoot, "turbo.json");
3353
- if (!fs16.existsSync(turboPath)) return false;
3406
+ const turboPath = path17.join(projectRoot, "turbo.json");
3407
+ if (!fs17.existsSync(turboPath)) return false;
3354
3408
  try {
3355
- const turbo = JSON.parse(fs16.readFileSync(turboPath, "utf-8"));
3409
+ const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
3356
3410
  const tasks = turbo.tasks ?? turbo.pipeline ?? {};
3357
3411
  return taskName in tasks;
3358
3412
  } catch {
@@ -3363,10 +3417,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3363
3417
  if (hasTurboTask(projectRoot, "typecheck")) {
3364
3418
  return { command: "npx turbo typecheck", label: "turbo typecheck" };
3365
3419
  }
3366
- const pkgJsonPath = path16.join(projectRoot, "package.json");
3367
- if (fs16.existsSync(pkgJsonPath)) {
3420
+ const pkgJsonPath = path17.join(projectRoot, "package.json");
3421
+ if (fs17.existsSync(pkgJsonPath)) {
3368
3422
  try {
3369
- const pkg = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
3423
+ const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
3370
3424
  if (pkg.scripts?.typecheck) {
3371
3425
  const pm = packageManager ?? "npm";
3372
3426
  return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
@@ -3374,7 +3428,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3374
3428
  } catch {
3375
3429
  }
3376
3430
  }
3377
- if (fs16.existsSync(path16.join(projectRoot, "tsconfig.json"))) {
3431
+ if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
3378
3432
  return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
3379
3433
  }
3380
3434
  return {
@@ -3384,23 +3438,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3384
3438
 
3385
3439
  // src/commands/init-hooks.ts
3386
3440
  function setupPreCommitHook(projectRoot) {
3387
- const lefthookPath = path17.join(projectRoot, "lefthook.yml");
3388
- if (fs17.existsSync(lefthookPath)) {
3441
+ const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3442
+ if (fs18.existsSync(lefthookPath)) {
3389
3443
  addLefthookPreCommit(lefthookPath);
3390
3444
  console.log(` ${import_chalk11.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3391
3445
  return "lefthook.yml";
3392
3446
  }
3393
- const huskyDir = path17.join(projectRoot, ".husky");
3394
- if (fs17.existsSync(huskyDir)) {
3447
+ const huskyDir = path18.join(projectRoot, ".husky");
3448
+ if (fs18.existsSync(huskyDir)) {
3395
3449
  writeHuskyPreCommit(huskyDir);
3396
3450
  console.log(` ${import_chalk11.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3397
3451
  return ".husky/pre-commit";
3398
3452
  }
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 });
3453
+ const gitDir = path18.join(projectRoot, ".git");
3454
+ if (fs18.existsSync(gitDir)) {
3455
+ const hooksDir = path18.join(gitDir, "hooks");
3456
+ if (!fs18.existsSync(hooksDir)) {
3457
+ fs18.mkdirSync(hooksDir, { recursive: true });
3404
3458
  }
3405
3459
  writeGitHookPreCommit(hooksDir);
3406
3460
  console.log(` ${import_chalk11.default.green("\u2713")} .git/hooks/pre-commit`);
@@ -3409,11 +3463,11 @@ function setupPreCommitHook(projectRoot) {
3409
3463
  return void 0;
3410
3464
  }
3411
3465
  function writeGitHookPreCommit(hooksDir) {
3412
- const hookPath = path17.join(hooksDir, "pre-commit");
3413
- if (fs17.existsSync(hookPath)) {
3414
- const existing = fs17.readFileSync(hookPath, "utf-8");
3466
+ const hookPath = path18.join(hooksDir, "pre-commit");
3467
+ if (fs18.existsSync(hookPath)) {
3468
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3415
3469
  if (existing.includes("viberails")) return;
3416
- fs17.writeFileSync(
3470
+ fs18.writeFileSync(
3417
3471
  hookPath,
3418
3472
  `${existing.trimEnd()}
3419
3473
 
@@ -3430,10 +3484,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
3430
3484
  "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
3431
3485
  ""
3432
3486
  ].join("\n");
3433
- fs17.writeFileSync(hookPath, script, { mode: 493 });
3487
+ fs18.writeFileSync(hookPath, script, { mode: 493 });
3434
3488
  }
3435
3489
  function addLefthookPreCommit(lefthookPath) {
3436
- const content = fs17.readFileSync(lefthookPath, "utf-8");
3490
+ const content = fs18.readFileSync(lefthookPath, "utf-8");
3437
3491
  if (content.includes("viberails")) return;
3438
3492
  const doc = (0, import_yaml.parse)(content) ?? {};
3439
3493
  if (!doc["pre-commit"]) {
@@ -3445,23 +3499,23 @@ function addLefthookPreCommit(lefthookPath) {
3445
3499
  doc["pre-commit"].commands.viberails = {
3446
3500
  run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
3447
3501
  };
3448
- fs17.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3502
+ fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3449
3503
  }
3450
3504
  function detectHookManager(projectRoot) {
3451
- if (fs17.existsSync(path17.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3452
- if (fs17.existsSync(path17.join(projectRoot, ".husky"))) return "Husky";
3505
+ if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3506
+ if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
3453
3507
  return void 0;
3454
3508
  }
3455
3509
  function setupClaudeCodeHook(projectRoot) {
3456
- const claudeDir = path17.join(projectRoot, ".claude");
3457
- if (!fs17.existsSync(claudeDir)) {
3458
- fs17.mkdirSync(claudeDir, { recursive: true });
3510
+ const claudeDir = path18.join(projectRoot, ".claude");
3511
+ if (!fs18.existsSync(claudeDir)) {
3512
+ fs18.mkdirSync(claudeDir, { recursive: true });
3459
3513
  }
3460
- const settingsPath = path17.join(claudeDir, "settings.json");
3514
+ const settingsPath = path18.join(claudeDir, "settings.json");
3461
3515
  let settings = {};
3462
- if (fs17.existsSync(settingsPath)) {
3516
+ if (fs18.existsSync(settingsPath)) {
3463
3517
  try {
3464
- settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
3518
+ settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
3465
3519
  } catch {
3466
3520
  console.warn(
3467
3521
  ` ${import_chalk11.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
@@ -3487,30 +3541,30 @@ function setupClaudeCodeHook(projectRoot) {
3487
3541
  }
3488
3542
  ];
3489
3543
  settings.hooks = hooks;
3490
- fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3544
+ fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3491
3545
  `);
3492
3546
  console.log(` ${import_chalk11.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3493
3547
  }
3494
3548
  function setupClaudeMdReference(projectRoot) {
3495
- const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
3549
+ const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
3496
3550
  let content = "";
3497
- if (fs17.existsSync(claudeMdPath)) {
3498
- content = fs17.readFileSync(claudeMdPath, "utf-8");
3551
+ if (fs18.existsSync(claudeMdPath)) {
3552
+ content = fs18.readFileSync(claudeMdPath, "utf-8");
3499
3553
  }
3500
3554
  if (content.includes("@.viberails/context.md")) return;
3501
3555
  const ref = "\n@.viberails/context.md\n";
3502
3556
  const prefix = content.length === 0 ? "" : content.trimEnd();
3503
- fs17.writeFileSync(claudeMdPath, prefix + ref);
3557
+ fs18.writeFileSync(claudeMdPath, prefix + ref);
3504
3558
  console.log(` ${import_chalk11.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3505
3559
  }
3506
3560
  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");
3561
+ const workflowDir = path18.join(projectRoot, ".github", "workflows");
3562
+ const workflowPath = path18.join(workflowDir, "viberails.yml");
3563
+ if (fs18.existsSync(workflowPath)) {
3564
+ const existing = fs18.readFileSync(workflowPath, "utf-8");
3511
3565
  if (existing.includes("viberails")) return void 0;
3512
3566
  }
3513
- fs17.mkdirSync(workflowDir, { recursive: true });
3567
+ fs18.mkdirSync(workflowDir, { recursive: true });
3514
3568
  const pm = packageManager || "npm";
3515
3569
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
3516
3570
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -3564,74 +3618,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
3564
3618
  ""
3565
3619
  );
3566
3620
  const content = lines.filter((l) => l !== void 0).join("\n");
3567
- fs17.writeFileSync(workflowPath, content);
3621
+ fs18.writeFileSync(workflowPath, content);
3568
3622
  return ".github/workflows/viberails.yml";
3569
3623
  }
3570
3624
  function writeHuskyPreCommit(huskyDir) {
3571
- const hookPath = path17.join(huskyDir, "pre-commit");
3625
+ const hookPath = path18.join(huskyDir, "pre-commit");
3572
3626
  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");
3627
+ if (fs18.existsSync(hookPath)) {
3628
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3575
3629
  if (!existing.includes("viberails")) {
3576
- fs17.writeFileSync(hookPath, `${existing.trimEnd()}
3630
+ fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3577
3631
  ${cmd}
3578
3632
  `);
3579
3633
  }
3580
3634
  return;
3581
3635
  }
3582
- fs17.writeFileSync(hookPath, `#!/bin/sh
3636
+ fs18.writeFileSync(hookPath, `#!/bin/sh
3583
3637
  ${cmd}
3584
3638
  `, { mode: 493 });
3585
3639
  }
3586
3640
 
3587
3641
  // src/commands/init-hooks-extra.ts
3588
- var fs18 = __toESM(require("fs"), 1);
3589
- var path18 = __toESM(require("path"), 1);
3642
+ var fs19 = __toESM(require("fs"), 1);
3643
+ var path19 = __toESM(require("path"), 1);
3590
3644
  var import_chalk12 = __toESM(require("chalk"), 1);
3591
3645
  var import_yaml2 = require("yaml");
3592
3646
  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");
3647
+ const lefthookPath = path19.join(projectRoot, "lefthook.yml");
3648
+ if (fs19.existsSync(lefthookPath)) {
3649
+ const content = fs19.readFileSync(lefthookPath, "utf-8");
3596
3650
  if (content.includes(marker)) return void 0;
3597
3651
  const doc = (0, import_yaml2.parse)(content) ?? {};
3598
3652
  if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
3599
3653
  if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
3600
3654
  doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
3601
- fs18.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3655
+ fs19.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3602
3656
  return "lefthook.yml";
3603
3657
  }
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");
3658
+ const huskyDir = path19.join(projectRoot, ".husky");
3659
+ if (fs19.existsSync(huskyDir)) {
3660
+ const hookPath = path19.join(huskyDir, "pre-commit");
3661
+ if (fs19.existsSync(hookPath)) {
3662
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3609
3663
  if (existing.includes(marker)) return void 0;
3610
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3664
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3611
3665
  ${command}
3612
3666
  `);
3613
3667
  } else {
3614
- fs18.writeFileSync(hookPath, `#!/bin/sh
3668
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3615
3669
  ${command}
3616
3670
  `, { mode: 493 });
3617
3671
  }
3618
3672
  return ".husky/pre-commit";
3619
3673
  }
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");
3674
+ const gitDir = path19.join(projectRoot, ".git");
3675
+ if (fs19.existsSync(gitDir)) {
3676
+ const hooksDir = path19.join(gitDir, "hooks");
3677
+ if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
3678
+ const hookPath = path19.join(hooksDir, "pre-commit");
3679
+ if (fs19.existsSync(hookPath)) {
3680
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3627
3681
  if (existing.includes(marker)) return void 0;
3628
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3682
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3629
3683
 
3630
3684
  # ${name}
3631
3685
  ${command}
3632
3686
  `);
3633
3687
  } else {
3634
- fs18.writeFileSync(hookPath, `#!/bin/sh
3688
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3635
3689
  # Generated by viberails
3636
3690
 
3637
3691
  # ${name}
@@ -3657,7 +3711,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
3657
3711
  return target;
3658
3712
  }
3659
3713
  function setupLintHook(projectRoot, linter) {
3660
- const isLefthook = fs18.existsSync(path18.join(projectRoot, "lefthook.yml"));
3714
+ const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
3661
3715
  const linterName = linter === "biome" ? "Biome" : "ESLint";
3662
3716
  let command;
3663
3717
  let lefthookExtra;
@@ -3681,6 +3735,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3681
3735
  const created = [];
3682
3736
  if (integrations.preCommitHook) {
3683
3737
  const t = setupPreCommitHook(projectRoot);
3738
+ if (t && opts.lefthookExpected && !t.includes("lefthook")) {
3739
+ console.log(` ${import_chalk12.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3740
+ }
3684
3741
  created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
3685
3742
  }
3686
3743
  if (integrations.typecheckHook) {
@@ -3710,9 +3767,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3710
3767
  }
3711
3768
 
3712
3769
  // 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);
3770
+ var fs20 = __toESM(require("fs"), 1);
3771
+ var path20 = __toESM(require("path"), 1);
3772
+ var clack12 = __toESM(require("@clack/prompts"), 1);
3716
3773
  var import_config8 = require("@viberails/config");
3717
3774
  var import_scanner2 = require("@viberails/scanner");
3718
3775
  var import_chalk13 = __toESM(require("chalk"), 1);
@@ -3736,7 +3793,7 @@ function getExemptedPackages(config) {
3736
3793
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3737
3794
  }
3738
3795
  async function initNonInteractive(projectRoot, configPath) {
3739
- const s = clack9.spinner();
3796
+ const s = clack12.spinner();
3740
3797
  s.start("Scanning project...");
3741
3798
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
3742
3799
  const config = (0, import_config8.generateConfig)(scanResult);
@@ -3755,7 +3812,7 @@ async function initNonInteractive(projectRoot, configPath) {
3755
3812
  );
3756
3813
  }
3757
3814
  if (config.packages.length > 1) {
3758
- const bs = clack9.spinner();
3815
+ const bs = clack12.spinner();
3759
3816
  bs.start("Building import graph...");
3760
3817
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3761
3818
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3771,7 +3828,7 @@ async function initNonInteractive(projectRoot, configPath) {
3771
3828
  }
3772
3829
  }
3773
3830
  const compacted = (0, import_config8.compactConfig)(config);
3774
- fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3831
+ fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3775
3832
  `);
3776
3833
  writeGeneratedFiles(projectRoot, config, scanResult);
3777
3834
  updateGitignore(projectRoot);
@@ -3790,7 +3847,7 @@ async function initNonInteractive(projectRoot, configPath) {
3790
3847
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3791
3848
  const ok = import_chalk13.default.green("\u2713");
3792
3849
  const created = [
3793
- `${ok} ${path19.basename(configPath)}`,
3850
+ `${ok} ${path20.basename(configPath)}`,
3794
3851
  `${ok} .viberails/context.md`,
3795
3852
  `${ok} .viberails/scan-result.json`,
3796
3853
  `${ok} .claude/settings.json \u2014 added viberails hook`,
@@ -3807,9 +3864,6 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
3807
3864
 
3808
3865
  // src/commands/init.ts
3809
3866
  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
3867
  async function initCommand(options, cwd) {
3814
3868
  const projectRoot = findProjectRoot(cwd ?? process.cwd());
3815
3869
  if (!projectRoot) {
@@ -3817,8 +3871,8 @@ async function initCommand(options, cwd) {
3817
3871
  "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
3818
3872
  );
3819
3873
  }
3820
- const configPath = path20.join(projectRoot, CONFIG_FILE5);
3821
- if (fs20.existsSync(configPath) && !options.force) {
3874
+ const configPath = path21.join(projectRoot, CONFIG_FILE5);
3875
+ if (fs21.existsSync(configPath) && !options.force) {
3822
3876
  if (!options.yes) {
3823
3877
  return initInteractive(projectRoot, configPath, options);
3824
3878
  }
@@ -3832,12 +3886,11 @@ async function initCommand(options, cwd) {
3832
3886
  await initInteractive(projectRoot, configPath, options);
3833
3887
  }
3834
3888
  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));
3889
+ clack13.intro("viberails");
3890
+ if (fs21.existsSync(configPath) && !options.force) {
3891
+ const action = await promptExistingConfigAction(path21.basename(configPath));
3839
3892
  if (action === "cancel") {
3840
- clack11.outro("Aborted. No files were written.");
3893
+ clack13.outro("Aborted. No files were written.");
3841
3894
  return;
3842
3895
  }
3843
3896
  if (action === "edit") {
@@ -3846,143 +3899,93 @@ async function initInteractive(projectRoot, configPath, options) {
3846
3899
  }
3847
3900
  options.force = true;
3848
3901
  }
3849
- if (fs20.existsSync(configPath) && options.force) {
3902
+ if (fs21.existsSync(configPath) && options.force) {
3850
3903
  const replace = await confirmDangerous(
3851
- `${path20.basename(configPath)} already exists and will be replaced. Continue?`
3904
+ `${path21.basename(configPath)} already exists and will be replaced. Continue?`
3852
3905
  );
3853
3906
  if (!replace) {
3854
- clack11.outro("Aborted. No files were written.");
3907
+ clack13.outro("Aborted. No files were written.");
3855
3908
  return;
3856
3909
  }
3857
3910
  }
3858
- const s = clack11.spinner();
3911
+ const s = clack13.spinner();
3859
3912
  s.start("Scanning project...");
3860
3913
  const scanResult = await (0, import_scanner3.scan)(projectRoot);
3861
3914
  const config = (0, import_config9.generateConfig)(scanResult);
3862
3915
  s.stop("Scan complete");
3863
3916
  if (scanResult.statistics.totalFiles === 0) {
3864
- clack11.log.warn(
3917
+ clack13.log.warn(
3865
3918
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3866
3919
  );
3867
3920
  }
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"
3921
+ const hasTestRunner = !!scanResult.stack.testRunner;
3922
+ if (!hasTestRunner) {
3923
+ clack13.log.info(
3924
+ "No test runner detected. Coverage checks are inactive until a test runner is installed.\nInstall a test runner (e.g. vitest) and re-run viberails init."
3904
3925
  );
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
3926
  }
3924
3927
  const hookManager = detectHookManager(projectRoot);
3925
3928
  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
3929
  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)
3930
+ const state = await promptMainMenu(config, scanResult, {
3931
+ hasTestRunner,
3932
+ hookManager,
3933
+ coveragePrereqs,
3934
+ projectRoot,
3935
+ tools: {
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
+ }
3944
3941
  });
3945
3942
  const shouldWrite = await confirm3("Apply this setup?");
3946
3943
  if (!shouldWrite) {
3947
- clack11.outro("Aborted. No files were written.");
3944
+ clack13.outro("Aborted. No files were written.");
3948
3945
  return;
3949
3946
  }
3950
- const ws = clack11.spinner();
3947
+ if (state.deferredInstalls.length > 0) {
3948
+ await executeDeferredInstalls(projectRoot, state.deferredInstalls);
3949
+ }
3950
+ const ws = clack13.spinner();
3951
3951
  ws.start("Writing configuration...");
3952
3952
  const compacted = (0, import_config9.compactConfig)(config);
3953
- fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3953
+ fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3954
3954
  `);
3955
3955
  writeGeneratedFiles(projectRoot, config, scanResult);
3956
3956
  updateGitignore(projectRoot);
3957
3957
  ws.stop("Configuration written");
3958
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(
3959
+ clack13.log.step(`${ok} ${path21.basename(configPath)}`);
3960
+ clack13.log.step(`${ok} .viberails/context.md`);
3961
+ clack13.log.step(`${ok} .viberails/scan-result.json`);
3962
+ if (state.visited.integrations && state.integrations) {
3963
+ const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
3964
+ setupSelectedIntegrations(projectRoot, state.integrations, {
3965
+ linter: rootPkgStack?.linter?.split("@")[0],
3966
+ packageManager: rootPkgStack?.packageManager?.split("@")[0],
3967
+ lefthookExpected
3968
+ });
3969
+ }
3970
+ clack13.outro(
3967
3971
  `Done! Next: review viberails.config.json, then run viberails check
3968
3972
  ${import_chalk14.default.dim("Tip: use")} ${import_chalk14.default.cyan("viberails check --enforce")} ${import_chalk14.default.dim("in CI to block PRs on violations.")}`
3969
3973
  );
3970
3974
  }
3971
3975
 
3972
3976
  // 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);
3977
+ var fs22 = __toESM(require("fs"), 1);
3978
+ var path22 = __toESM(require("path"), 1);
3979
+ var clack14 = __toESM(require("@clack/prompts"), 1);
3976
3980
  var import_config11 = require("@viberails/config");
3977
3981
  var import_scanner4 = require("@viberails/scanner");
3978
3982
  var import_chalk15 = __toESM(require("chalk"), 1);
3979
- init_prompt();
3980
3983
  var CONFIG_FILE6 = "viberails.config.json";
3981
3984
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
3982
3985
  function loadPreviousStats(projectRoot) {
3983
- const scanResultPath = path21.join(projectRoot, SCAN_RESULT_FILE2);
3986
+ const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
3984
3987
  try {
3985
- const raw = fs21.readFileSync(scanResultPath, "utf-8");
3988
+ const raw = fs22.readFileSync(scanResultPath, "utf-8");
3986
3989
  const parsed = JSON.parse(raw);
3987
3990
  if (parsed?.statistics?.totalFiles !== void 0) {
3988
3991
  return parsed.statistics;
@@ -3999,17 +4002,17 @@ async function syncCommand(options, cwd) {
3999
4002
  "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
4003
  );
4001
4004
  }
4002
- const configPath = path21.join(projectRoot, CONFIG_FILE6);
4005
+ const configPath = path22.join(projectRoot, CONFIG_FILE6);
4003
4006
  const existing = await (0, import_config11.loadConfig)(configPath);
4004
4007
  const previousStats = loadPreviousStats(projectRoot);
4005
- const s = clack12.spinner();
4008
+ const s = clack14.spinner();
4006
4009
  s.start("Scanning project...");
4007
4010
  const scanResult = await (0, import_scanner4.scan)(projectRoot);
4008
4011
  s.stop("Scan complete");
4009
4012
  const merged = (0, import_config11.mergeConfig)(existing, scanResult);
4010
4013
  const compacted = (0, import_config11.compactConfig)(merged);
4011
4014
  const compactedJson = JSON.stringify(compacted, null, 2);
4012
- const rawDisk = fs21.readFileSync(configPath, "utf-8").trim();
4015
+ const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
4013
4016
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
4014
4017
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
4015
4018
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -4027,9 +4030,9 @@ ${import_chalk15.default.bold("Changes:")}`);
4027
4030
  }
4028
4031
  }
4029
4032
  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({
4033
+ clack14.intro("viberails sync (interactive)");
4034
+ clack14.note(formatRulesText(merged).join("\n"), "Rules after sync");
4035
+ const decision = await clack14.select({
4033
4036
  message: "How would you like to proceed?",
4034
4037
  options: [
4035
4038
  { value: "accept", label: "Accept changes" },
@@ -4039,7 +4042,7 @@ ${import_chalk15.default.bold("Changes:")}`);
4039
4042
  });
4040
4043
  assertNotCancelled(decision);
4041
4044
  if (decision === "cancel") {
4042
- clack12.outro("Sync cancelled. No files were written.");
4045
+ clack14.outro("Sync cancelled. No files were written.");
4043
4046
  return;
4044
4047
  }
4045
4048
  if (decision === "customize") {
@@ -4060,15 +4063,15 @@ ${import_chalk15.default.bold("Changes:")}`);
4060
4063
  });
4061
4064
  applyRuleOverrides(merged, overrides);
4062
4065
  const recompacted = (0, import_config11.compactConfig)(merged);
4063
- fs21.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4066
+ fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4064
4067
  `);
4065
4068
  writeGeneratedFiles(projectRoot, merged, scanResult);
4066
- clack12.log.success("Updated config with your customizations.");
4067
- clack12.outro("Done! Run viberails check to verify.");
4069
+ clack14.log.success("Updated config with your customizations.");
4070
+ clack14.outro("Done! Run viberails check to verify.");
4068
4071
  return;
4069
4072
  }
4070
4073
  }
4071
- fs21.writeFileSync(configPath, `${compactedJson}
4074
+ fs22.writeFileSync(configPath, `${compactedJson}
4072
4075
  `);
4073
4076
  writeGeneratedFiles(projectRoot, merged, scanResult);
4074
4077
  console.log(`
@@ -4083,7 +4086,7 @@ ${import_chalk15.default.bold("Synced:")}`);
4083
4086
  }
4084
4087
 
4085
4088
  // src/index.ts
4086
- var VERSION = "0.6.5";
4089
+ var VERSION = "0.6.7";
4087
4090
  var program = new import_commander.Command();
4088
4091
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
4089
4092
  program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {