viberails 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,7 +34,7 @@ __export(index_exports, {
34
34
  VERSION: () => VERSION
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_chalk14 = __toESM(require("chalk"), 1);
37
+ var import_chalk15 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -63,146 +63,285 @@ function findProjectRoot(startDir) {
63
63
  // src/utils/prompt.ts
64
64
  var clack5 = __toESM(require("@clack/prompts"), 1);
65
65
 
66
- // src/utils/prompt-integrations.ts
67
- var clack = __toESM(require("@clack/prompts"), 1);
66
+ // src/utils/prompt-rules.ts
67
+ var clack4 = __toESM(require("@clack/prompts"), 1);
68
68
 
69
- // src/utils/spawn-async.ts
70
- var import_node_child_process = require("child_process");
71
- function spawnAsync(command, cwd) {
72
- return new Promise((resolve4) => {
73
- const child = (0, import_node_child_process.spawn)(command, { cwd, shell: true, stdio: "pipe" });
74
- let stdout = "";
75
- let stderr = "";
76
- child.stdout.on("data", (d) => {
77
- stdout += d.toString();
78
- });
79
- child.stderr.on("data", (d) => {
80
- stderr += d.toString();
81
- });
82
- child.on("close", (status) => {
83
- resolve4({ status, stdout, stderr });
84
- });
85
- child.on("error", () => {
86
- resolve4({ status: 1, stdout, stderr });
87
- });
88
- });
69
+ // src/utils/get-root-package.ts
70
+ function getRootPackage(packages) {
71
+ return packages.find((pkg) => pkg.path === ".") ?? packages[0];
89
72
  }
90
73
 
91
- // src/utils/prompt-integrations.ts
92
- async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
93
- const choice = await clack.select({
94
- message: "No shared git hook manager detected. Install Lefthook?",
95
- options: [
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);
79
+
80
+ // src/utils/prompt-constants.ts
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
+
88
+ // src/utils/prompt-submenus.ts
89
+ var clack = __toESM(require("@clack/prompts"), 1);
90
+ var FILE_NAMING_OPTIONS = [
91
+ { value: "kebab-case", label: "kebab-case" },
92
+ { value: "camelCase", label: "camelCase" },
93
+ { value: "PascalCase", label: "PascalCase" },
94
+ { value: "snake_case", label: "snake_case" }
95
+ ];
96
+ var COMPONENT_NAMING_OPTIONS = [
97
+ { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
98
+ { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
99
+ ];
100
+ var HOOK_NAMING_OPTIONS = [
101
+ { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
102
+ { value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
103
+ ];
104
+ async function promptFileLimitsMenu(state) {
105
+ while (true) {
106
+ const choice = await clack.select({
107
+ message: "File limits",
108
+ options: [
109
+ { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
110
+ {
111
+ value: "maxTestFileLines",
112
+ label: "Max test file lines",
113
+ hint: state.maxTestFileLines > 0 ? String(state.maxTestFileLines) : "0 (unlimited)"
114
+ },
115
+ { value: "back", label: "Back" }
116
+ ]
117
+ });
118
+ assertNotCancelled(choice);
119
+ if (choice === "back") return;
120
+ if (choice === "maxFileLines") {
121
+ const result = await clack.text({
122
+ message: "Maximum lines per source file?",
123
+ initialValue: String(state.maxFileLines),
124
+ validate: (v) => {
125
+ if (typeof v !== "string") return "Enter a positive number";
126
+ const n = Number.parseInt(v, 10);
127
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
128
+ }
129
+ });
130
+ assertNotCancelled(result);
131
+ state.maxFileLines = Number.parseInt(result, 10);
132
+ }
133
+ if (choice === "maxTestFileLines") {
134
+ const result = await clack.text({
135
+ message: "Maximum lines per test file (0 to disable)?",
136
+ initialValue: String(state.maxTestFileLines),
137
+ validate: (v) => {
138
+ if (typeof v !== "string") return "Enter a number (0 or positive)";
139
+ const n = Number.parseInt(v, 10);
140
+ if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
141
+ }
142
+ });
143
+ assertNotCancelled(result);
144
+ state.maxTestFileLines = Number.parseInt(result, 10);
145
+ }
146
+ }
147
+ }
148
+ async function promptNamingMenu(state) {
149
+ while (true) {
150
+ const options = [
96
151
  {
97
- value: "install",
98
- label: "Yes, install Lefthook",
99
- hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
152
+ value: "enforceNaming",
153
+ label: "Enforce file naming",
154
+ hint: state.enforceNaming ? "yes" : "no"
155
+ }
156
+ ];
157
+ if (state.enforceNaming) {
158
+ options.push({
159
+ value: "fileNaming",
160
+ label: "File naming convention",
161
+ hint: state.fileNamingValue ?? "(not set)"
162
+ });
163
+ }
164
+ options.push(
165
+ {
166
+ value: "componentNaming",
167
+ label: "Component naming",
168
+ hint: state.componentNaming ?? "(not set)"
100
169
  },
101
170
  {
102
- value: "skip",
103
- label: "No, skip",
104
- hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
105
- }
106
- ]
107
- });
108
- assertNotCancelled(choice);
109
- if (choice !== "install") return void 0;
110
- const pm = packageManager || "npm";
111
- const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
112
- const s = clack.spinner();
113
- s.start("Installing Lefthook...");
114
- const result = await spawnAsync(installCmd, projectRoot);
115
- if (result.status === 0) {
116
- const fs21 = await import("fs");
117
- const path21 = await import("path");
118
- const lefthookPath = path21.join(projectRoot, "lefthook.yml");
119
- if (!fs21.existsSync(lefthookPath)) {
120
- fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
121
- }
122
- s.stop("Installed Lefthook");
123
- return "Lefthook";
124
- }
125
- s.stop("Failed to install Lefthook");
126
- clack.log.warn(`Install manually: ${installCmd}`);
127
- return void 0;
128
- }
129
- async function promptIntegrations(projectRoot, hookManager, tools) {
130
- let resolvedHookManager = hookManager;
131
- if (!resolvedHookManager) {
132
- resolvedHookManager = await promptHookManagerInstall(
133
- projectRoot,
134
- tools?.packageManager ?? "npm",
135
- tools?.isWorkspace
171
+ value: "hookNaming",
172
+ label: "Hook naming",
173
+ hint: state.hookNaming ?? "(not set)"
174
+ },
175
+ {
176
+ value: "importAlias",
177
+ label: "Import alias",
178
+ hint: state.importAlias ?? "(not set)"
179
+ },
180
+ { value: "back", label: "Back" }
136
181
  );
137
- }
138
- const isBareHook = !resolvedHookManager;
139
- const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
140
- const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
141
- const options = [
142
- {
143
- value: "preCommit",
144
- label: hookLabel,
145
- hint: hookHint
182
+ const choice = await clack.select({ message: "Naming & conventions", options });
183
+ assertNotCancelled(choice);
184
+ if (choice === "back") return;
185
+ if (choice === "enforceNaming") {
186
+ const result = await clack.confirm({
187
+ message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
188
+ initialValue: state.enforceNaming
189
+ });
190
+ assertNotCancelled(result);
191
+ if (result && !state.fileNamingValue) {
192
+ const selected = await clack.select({
193
+ message: "Which file naming convention should be enforced?",
194
+ options: [...FILE_NAMING_OPTIONS]
195
+ });
196
+ assertNotCancelled(selected);
197
+ state.fileNamingValue = selected;
198
+ }
199
+ state.enforceNaming = result;
200
+ }
201
+ if (choice === "fileNaming") {
202
+ const selected = await clack.select({
203
+ message: "Which file naming convention should be enforced?",
204
+ options: [...FILE_NAMING_OPTIONS],
205
+ initialValue: state.fileNamingValue
206
+ });
207
+ assertNotCancelled(selected);
208
+ state.fileNamingValue = selected;
209
+ }
210
+ if (choice === "componentNaming") {
211
+ const selected = await clack.select({
212
+ message: "Component naming convention",
213
+ options: [
214
+ ...COMPONENT_NAMING_OPTIONS,
215
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
216
+ ],
217
+ initialValue: state.componentNaming ?? SENTINEL_CLEAR
218
+ });
219
+ assertNotCancelled(selected);
220
+ state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
221
+ }
222
+ if (choice === "hookNaming") {
223
+ const selected = await clack.select({
224
+ message: "Hook naming convention",
225
+ options: [
226
+ ...HOOK_NAMING_OPTIONS,
227
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
228
+ ],
229
+ initialValue: state.hookNaming ?? SENTINEL_CLEAR
230
+ });
231
+ assertNotCancelled(selected);
232
+ state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
233
+ }
234
+ if (choice === "importAlias") {
235
+ const selected = await clack.select({
236
+ message: "Import alias pattern",
237
+ options: [
238
+ { value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
239
+ { value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
240
+ { value: SENTINEL_CUSTOM, label: "Custom..." },
241
+ { value: SENTINEL_CLEAR, label: "Clear (no alias)" }
242
+ ],
243
+ initialValue: state.importAlias ?? SENTINEL_CLEAR
244
+ });
245
+ assertNotCancelled(selected);
246
+ if (selected === SENTINEL_CLEAR) {
247
+ state.importAlias = void 0;
248
+ } else if (selected === SENTINEL_CUSTOM) {
249
+ const result = await clack.text({
250
+ message: "Custom import alias (e.g. #/*)?",
251
+ initialValue: state.importAlias ?? "",
252
+ placeholder: "e.g. #/*",
253
+ validate: (v) => {
254
+ if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
255
+ if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
256
+ return "Must match pattern like @/*, ~/*, or #src/*";
257
+ }
258
+ });
259
+ assertNotCancelled(result);
260
+ state.importAlias = result.trim();
261
+ } else {
262
+ state.importAlias = selected;
263
+ }
146
264
  }
147
- ];
148
- if (tools?.isTypeScript) {
149
- options.push({
150
- value: "typecheck",
151
- label: "Typecheck (tsc --noEmit)",
152
- hint: "pre-commit hook + CI check"
153
- });
154
- }
155
- if (tools?.linter) {
156
- const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
157
- options.push({
158
- value: "lint",
159
- label: `Lint check (${linterName})`,
160
- hint: "pre-commit hook + CI check"
161
- });
162
265
  }
163
- options.push(
164
- {
165
- value: "claude",
166
- label: "Claude Code hook",
167
- hint: "checks files when Claude edits them"
168
- },
169
- {
170
- value: "claudeMd",
171
- label: "CLAUDE.md reference",
172
- hint: "appends @.viberails/context.md so Claude loads rules automatically"
173
- },
174
- {
175
- value: "githubAction",
176
- label: "GitHub Actions workflow",
177
- hint: "blocks PRs that fail viberails check"
266
+ }
267
+ async function promptTestingMenu(state) {
268
+ while (true) {
269
+ const options = [
270
+ {
271
+ value: "enforceMissingTests",
272
+ label: "Enforce missing tests",
273
+ hint: state.enforceMissingTests ? "yes" : "no"
274
+ },
275
+ {
276
+ value: "testCoverage",
277
+ label: "Test coverage target",
278
+ hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
279
+ }
280
+ ];
281
+ if (state.testCoverage > 0) {
282
+ options.push(
283
+ {
284
+ value: "coverageSummaryPath",
285
+ label: "Coverage summary path",
286
+ hint: state.coverageSummaryPath
287
+ },
288
+ {
289
+ value: "coverageCommand",
290
+ label: "Coverage command",
291
+ hint: state.coverageCommand ?? "auto-detect from package.json test runner"
292
+ }
293
+ );
178
294
  }
179
- );
180
- const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
181
- const result = await clack.multiselect({
182
- message: "Optional integrations",
183
- options,
184
- initialValues,
185
- required: false
186
- });
187
- assertNotCancelled(result);
188
- return {
189
- preCommitHook: result.includes("preCommit"),
190
- claudeCodeHook: result.includes("claude"),
191
- claudeMdRef: result.includes("claudeMd"),
192
- githubAction: result.includes("githubAction"),
193
- typecheckHook: result.includes("typecheck"),
194
- lintHook: result.includes("lint")
195
- };
295
+ options.push({ value: "back", label: "Back" });
296
+ const choice = await clack.select({ message: "Testing & coverage", options });
297
+ assertNotCancelled(choice);
298
+ if (choice === "back") return;
299
+ if (choice === "enforceMissingTests") {
300
+ const result = await clack.confirm({
301
+ message: "Require every source file to have a corresponding test file?",
302
+ initialValue: state.enforceMissingTests
303
+ });
304
+ assertNotCancelled(result);
305
+ state.enforceMissingTests = result;
306
+ }
307
+ if (choice === "testCoverage") {
308
+ const result = await clack.text({
309
+ message: "Test coverage target (0 disables coverage checks)?",
310
+ initialValue: String(state.testCoverage),
311
+ validate: (v) => {
312
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
313
+ const n = Number.parseInt(v, 10);
314
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
315
+ }
316
+ });
317
+ assertNotCancelled(result);
318
+ state.testCoverage = Number.parseInt(result, 10);
319
+ }
320
+ if (choice === "coverageSummaryPath") {
321
+ const result = await clack.text({
322
+ message: "Coverage summary path (relative to package root)?",
323
+ initialValue: state.coverageSummaryPath,
324
+ validate: (v) => {
325
+ if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
326
+ }
327
+ });
328
+ assertNotCancelled(result);
329
+ state.coverageSummaryPath = result.trim();
330
+ }
331
+ if (choice === "coverageCommand") {
332
+ const result = await clack.text({
333
+ message: "Coverage command (blank to auto-detect from package.json)?",
334
+ initialValue: state.coverageCommand ?? "",
335
+ placeholder: "(auto-detect from package.json test runner)"
336
+ });
337
+ assertNotCancelled(result);
338
+ const trimmed = result.trim();
339
+ state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
340
+ }
341
+ }
196
342
  }
197
343
 
198
- // src/utils/prompt-rules.ts
199
- var clack4 = __toESM(require("@clack/prompts"), 1);
200
-
201
- // src/utils/prompt-menu-handlers.ts
202
- var clack3 = __toESM(require("@clack/prompts"), 1);
203
-
204
344
  // src/utils/prompt-package-overrides.ts
205
- var clack2 = __toESM(require("@clack/prompts"), 1);
206
345
  function normalizePackageOverrides(packages) {
207
346
  for (const pkg of packages) {
208
347
  if (pkg.rules && Object.keys(pkg.rules).length === 0) {
@@ -211,121 +350,177 @@ function normalizePackageOverrides(packages) {
211
350
  if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
212
351
  delete pkg.coverage;
213
352
  }
353
+ if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
354
+ delete pkg.conventions;
355
+ }
214
356
  }
215
357
  return packages;
216
358
  }
217
- function packageCoverageHint(pkg, defaults) {
359
+ function packageOverrideHint(pkg, defaults) {
360
+ const tags = [];
361
+ if (pkg.conventions?.fileNaming && pkg.conventions.fileNaming !== defaults.fileNamingValue) {
362
+ tags.push(pkg.conventions.fileNaming);
363
+ }
364
+ if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== defaults.maxFileLines && pkg.rules.maxFileLines > 0) {
365
+ tags.push(`${pkg.rules.maxFileLines} lines`);
366
+ }
218
367
  const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
219
368
  const isExempt = coverage === 0;
369
+ const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
370
+ const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
371
+ if (isExempt) {
372
+ tags.push(isTypesOnly ? "exempt (types-only)" : "exempt");
373
+ } else if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== defaults.testCoverage) {
374
+ tags.push(`${coverage}%`);
375
+ }
220
376
  const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
221
377
  const defaultCommand = defaults.coverageCommand ?? "";
222
378
  const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
223
- const tags = [];
224
- const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
225
- const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
226
- tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
227
379
  if (hasSummaryOverride) tags.push("summary override");
228
380
  if (hasCommandOverride) tags.push("command override");
229
- return tags.join(", ");
381
+ return tags.length > 0 ? tags.join(", ") : "(no overrides)";
230
382
  }
231
- async function promptPackageCoverageOverrides(packages, defaults) {
383
+ async function promptPackageOverrides(packages, defaults) {
232
384
  const editablePackages = packages.filter((pkg) => pkg.path !== ".");
233
385
  if (editablePackages.length === 0) return packages;
234
386
  while (true) {
235
387
  const selectedPath = await clack2.select({
236
- message: "Select package to edit coverage overrides",
388
+ message: "Select package to edit overrides",
237
389
  options: [
238
390
  ...editablePackages.map((pkg) => ({
239
391
  value: pkg.path,
240
392
  label: `${pkg.path} (${pkg.name})`,
241
- hint: packageCoverageHint(pkg, defaults)
393
+ hint: packageOverrideHint(pkg, defaults)
242
394
  })),
243
- { value: "__done__", label: "Done" }
395
+ { value: SENTINEL_DONE, label: "Done" }
244
396
  ]
245
397
  });
246
398
  assertNotCancelled(selectedPath);
247
- if (selectedPath === "__done__") break;
399
+ if (selectedPath === SENTINEL_DONE) break;
248
400
  const target = editablePackages.find((pkg) => pkg.path === selectedPath);
249
401
  if (!target) continue;
250
- while (true) {
251
- const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
252
- const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
253
- const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
254
- const choice = await clack2.select({
255
- message: `Edit coverage overrides for ${target.path}`,
402
+ await promptSinglePackageOverrides(target, defaults);
403
+ normalizePackageOverrides(editablePackages);
404
+ }
405
+ return normalizePackageOverrides(packages);
406
+ }
407
+ async function promptSinglePackageOverrides(target, defaults) {
408
+ while (true) {
409
+ const effectiveNaming = target.conventions?.fileNaming ?? defaults.fileNamingValue;
410
+ const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
411
+ const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
412
+ const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
413
+ const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
414
+ const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
415
+ const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
416
+ const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
417
+ const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
418
+ const choice = await clack2.select({
419
+ message: `Edit overrides for ${target.path}`,
420
+ options: [
421
+ { value: "fileNaming", label: "File naming", hint: namingHint },
422
+ { value: "maxFileLines", label: "Max file lines", hint: maxLinesHint },
423
+ { value: "testCoverage", label: "Test coverage", hint: String(effectiveCoverage) },
424
+ { value: "summaryPath", label: "Coverage summary path", hint: effectiveSummary },
425
+ { value: "command", label: "Coverage command", hint: effectiveCommand },
426
+ { value: "reset", label: "Reset all overrides for this package" },
427
+ { value: "back", label: "Back to package list" }
428
+ ]
429
+ });
430
+ assertNotCancelled(choice);
431
+ if (choice === "back") break;
432
+ if (choice === "fileNaming") {
433
+ const selected = await clack2.select({
434
+ message: `File naming for ${target.path}`,
256
435
  options: [
257
- { value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
258
- { value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
259
- { value: "command", label: "coverage.command", hint: effectiveCommand },
260
- { value: "reset", label: "Reset this package to inherit defaults" },
261
- { value: "back", label: "Back to package list" }
262
- ]
263
- });
264
- assertNotCancelled(choice);
265
- if (choice === "back") break;
266
- if (choice === "testCoverage") {
267
- const result = await clack2.text({
268
- message: "Package testCoverage (0 to exempt package)?",
269
- initialValue: String(effectiveCoverage),
270
- validate: (v) => {
271
- if (typeof v !== "string") return "Enter a number between 0 and 100";
272
- const n = Number.parseInt(v, 10);
273
- if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
436
+ ...FILE_NAMING_OPTIONS,
437
+ { value: SENTINEL_NONE, label: "(none \u2014 exempt from checks)" },
438
+ {
439
+ value: SENTINEL_INHERIT,
440
+ label: `Inherit default${defaults.fileNamingValue ? ` (${defaults.fileNamingValue})` : ""}`
274
441
  }
275
- });
276
- assertNotCancelled(result);
277
- const nextCoverage = Number.parseInt(result, 10);
278
- if (nextCoverage === defaults.testCoverage) {
279
- if (target.rules) {
280
- delete target.rules.testCoverage;
281
- }
282
- } else {
283
- target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
284
- }
442
+ ],
443
+ initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
444
+ });
445
+ assertNotCancelled(selected);
446
+ if (selected === SENTINEL_INHERIT) {
447
+ if (target.conventions) delete target.conventions.fileNaming;
448
+ } else if (selected === SENTINEL_NONE) {
449
+ target.conventions = { ...target.conventions ?? {}, fileNaming: "" };
450
+ } else {
451
+ target.conventions = { ...target.conventions ?? {}, fileNaming: selected };
285
452
  }
286
- if (choice === "summaryPath") {
287
- const result = await clack2.text({
288
- message: "Package coverage.summaryPath (blank to inherit default)?",
289
- initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
290
- placeholder: defaults.coverageSummaryPath
291
- });
292
- assertNotCancelled(result);
293
- const value = result.trim();
294
- if (value.length === 0 || value === defaults.coverageSummaryPath) {
295
- if (target.coverage) {
296
- delete target.coverage.summaryPath;
297
- }
298
- } else {
299
- target.coverage = { ...target.coverage ?? {}, summaryPath: value };
300
- }
453
+ }
454
+ if (choice === "maxFileLines") {
455
+ const result = await clack2.text({
456
+ message: `Max file lines for ${target.path} (blank to inherit default)?`,
457
+ initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
458
+ placeholder: String(defaults.maxFileLines)
459
+ });
460
+ assertNotCancelled(result);
461
+ const value = result.trim();
462
+ if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
463
+ if (target.rules) delete target.rules.maxFileLines;
464
+ } else {
465
+ target.rules = { ...target.rules ?? {}, maxFileLines: Number.parseInt(value, 10) };
301
466
  }
302
- if (choice === "command") {
303
- const result = await clack2.text({
304
- message: "Package coverage.command (blank to inherit default/auto)?",
305
- initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
306
- placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
307
- });
308
- assertNotCancelled(result);
309
- const value = result.trim();
310
- const defaultCommand = defaults.coverageCommand ?? "";
311
- if (value.length === 0 || value === defaultCommand) {
312
- if (target.coverage) {
313
- delete target.coverage.command;
314
- }
315
- } else {
316
- target.coverage = { ...target.coverage ?? {}, command: value };
467
+ }
468
+ if (choice === "testCoverage") {
469
+ const result = await clack2.text({
470
+ message: "Package testCoverage (0 to exempt package)?",
471
+ initialValue: String(effectiveCoverage),
472
+ validate: (v) => {
473
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
474
+ const n = Number.parseInt(v, 10);
475
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
317
476
  }
477
+ });
478
+ assertNotCancelled(result);
479
+ const nextCoverage = Number.parseInt(result, 10);
480
+ if (nextCoverage === defaults.testCoverage) {
481
+ if (target.rules) delete target.rules.testCoverage;
482
+ } else {
483
+ target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
318
484
  }
319
- if (choice === "reset") {
320
- if (target.rules) {
321
- delete target.rules.testCoverage;
322
- }
323
- delete target.coverage;
485
+ }
486
+ if (choice === "summaryPath") {
487
+ const result = await clack2.text({
488
+ message: "Path to coverage summary file (blank to inherit default)?",
489
+ initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
490
+ placeholder: defaults.coverageSummaryPath
491
+ });
492
+ assertNotCancelled(result);
493
+ const value = result.trim();
494
+ if (value.length === 0 || value === defaults.coverageSummaryPath) {
495
+ if (target.coverage) delete target.coverage.summaryPath;
496
+ } else {
497
+ target.coverage = { ...target.coverage ?? {}, summaryPath: value };
498
+ }
499
+ }
500
+ if (choice === "command") {
501
+ const result = await clack2.text({
502
+ message: "Coverage command (blank to auto-detect)?",
503
+ initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
504
+ placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
505
+ });
506
+ assertNotCancelled(result);
507
+ const value = result.trim();
508
+ const defaultCommand = defaults.coverageCommand ?? "";
509
+ if (value.length === 0 || value === defaultCommand) {
510
+ if (target.coverage) delete target.coverage.command;
511
+ } else {
512
+ target.coverage = { ...target.coverage ?? {}, command: value };
513
+ }
514
+ }
515
+ if (choice === "reset") {
516
+ if (target.rules) {
517
+ delete target.rules.testCoverage;
518
+ delete target.rules.maxFileLines;
324
519
  }
325
- normalizePackageOverrides(editablePackages);
520
+ delete target.coverage;
521
+ delete target.conventions;
326
522
  }
327
523
  }
328
- return normalizePackageOverrides(packages);
329
524
  }
330
525
 
331
526
  // src/utils/prompt-menu-handlers.ts
@@ -368,48 +563,21 @@ function getPackageDiffs(pkg, root) {
368
563
  return diffs;
369
564
  }
370
565
  function buildMenuOptions(state, packageCount) {
371
- const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
566
+ const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
567
+ const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
568
+ const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
372
569
  const options = [
373
- { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
374
- { value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
570
+ { value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
571
+ { value: "naming", label: "Naming & conventions", hint: namingHint },
572
+ { value: "testing", label: "Testing & coverage", hint: testingHint }
375
573
  ];
376
- if (state.fileNamingValue) {
574
+ if (packageCount > 0) {
377
575
  options.push({
378
- value: "fileNaming",
379
- label: "File naming convention",
380
- hint: state.fileNamingValue
576
+ value: "packageOverrides",
577
+ label: "Per-package overrides",
578
+ hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
381
579
  });
382
580
  }
383
- const isMonorepo = packageCount > 0;
384
- const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
385
- const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
386
- options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
387
- options.push({
388
- value: "enforceMissingTests",
389
- label: "Enforce missing tests",
390
- hint: state.enforceMissingTests ? "yes" : "no"
391
- });
392
- if (state.testCoverage > 0) {
393
- options.push(
394
- {
395
- value: "coverageSummaryPath",
396
- label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
397
- hint: state.coverageSummaryPath
398
- },
399
- {
400
- value: "coverageCommand",
401
- label: isMonorepo ? "Default coverage command" : "Coverage command",
402
- hint: state.coverageCommand ?? "auto-detect from package.json test runner"
403
- }
404
- );
405
- if (isMonorepo) {
406
- options.push({
407
- value: "packageOverrides",
408
- label: "Per-package coverage overrides",
409
- hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
410
- });
411
- }
412
- }
413
581
  options.push(
414
582
  { value: "reset", label: "Reset all to detected defaults" },
415
583
  { value: "done", label: "Done" }
@@ -417,37 +585,43 @@ function buildMenuOptions(state, packageCount) {
417
585
  return options;
418
586
  }
419
587
  function clonePackages(packages) {
420
- return packages?.map((pkg) => ({
421
- ...pkg,
422
- stack: pkg.stack ? { ...pkg.stack } : void 0,
423
- structure: pkg.structure ? { ...pkg.structure } : void 0,
424
- conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
425
- rules: pkg.rules ? { ...pkg.rules } : void 0,
426
- coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
427
- ignore: pkg.ignore ? [...pkg.ignore] : void 0,
428
- boundaries: pkg.boundaries ? {
429
- deny: [...pkg.boundaries.deny],
430
- ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
431
- } : void 0
432
- }));
588
+ return packages ? structuredClone(packages) : void 0;
433
589
  }
434
590
  async function handleMenuChoice(choice, state, defaults, root) {
435
591
  if (choice === "reset") {
436
592
  state.maxFileLines = defaults.maxFileLines;
593
+ state.maxTestFileLines = defaults.maxTestFileLines;
437
594
  state.testCoverage = defaults.testCoverage;
438
595
  state.enforceMissingTests = defaults.enforceMissingTests;
439
596
  state.enforceNaming = defaults.enforceNaming;
440
597
  state.fileNamingValue = defaults.fileNamingValue;
598
+ state.componentNaming = defaults.componentNaming;
599
+ state.hookNaming = defaults.hookNaming;
600
+ state.importAlias = defaults.importAlias;
441
601
  state.coverageSummaryPath = defaults.coverageSummaryPath;
442
602
  state.coverageCommand = defaults.coverageCommand;
443
603
  state.packageOverrides = clonePackages(defaults.packageOverrides);
444
604
  clack3.log.info("Reset all rules to detected defaults.");
445
605
  return;
446
606
  }
607
+ if (choice === "fileLimits") {
608
+ await promptFileLimitsMenu(state);
609
+ return;
610
+ }
611
+ if (choice === "naming") {
612
+ await promptNamingMenu(state);
613
+ return;
614
+ }
615
+ if (choice === "testing") {
616
+ await promptTestingMenu(state);
617
+ return;
618
+ }
447
619
  if (choice === "packageOverrides") {
448
620
  if (state.packageOverrides) {
449
621
  const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
450
- state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
622
+ state.packageOverrides = await promptPackageOverrides(state.packageOverrides, {
623
+ fileNamingValue: state.fileNamingValue,
624
+ maxFileLines: state.maxFileLines,
451
625
  testCoverage: state.testCoverage,
452
626
  coverageSummaryPath: state.coverageSummaryPath,
453
627
  coverageCommand: state.coverageCommand
@@ -460,89 +634,9 @@ async function handleMenuChoice(choice, state, defaults, root) {
460
634
  }
461
635
  return;
462
636
  }
463
- if (choice === "maxFileLines") {
464
- const result = await clack3.text({
465
- message: "Maximum lines per source file?",
466
- initialValue: String(state.maxFileLines),
467
- validate: (v) => {
468
- if (typeof v !== "string") return "Enter a positive number";
469
- const n = Number.parseInt(v, 10);
470
- if (Number.isNaN(n) || n < 1) return "Enter a positive number";
471
- }
472
- });
473
- assertNotCancelled(result);
474
- state.maxFileLines = Number.parseInt(result, 10);
475
- }
476
- if (choice === "enforceMissingTests") {
477
- const result = await clack3.confirm({
478
- message: "Require every source file to have a corresponding test file?",
479
- initialValue: state.enforceMissingTests
480
- });
481
- assertNotCancelled(result);
482
- state.enforceMissingTests = result;
483
- }
484
- if (choice === "testCoverage") {
485
- const result = await clack3.text({
486
- message: "Test coverage target (0 disables coverage checks)?",
487
- initialValue: String(state.testCoverage),
488
- validate: (v) => {
489
- if (typeof v !== "string") return "Enter a number between 0 and 100";
490
- const n = Number.parseInt(v, 10);
491
- if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
492
- }
493
- });
494
- assertNotCancelled(result);
495
- state.testCoverage = Number.parseInt(result, 10);
496
- }
497
- if (choice === "coverageSummaryPath") {
498
- const result = await clack3.text({
499
- message: "Coverage summary path (relative to package root)?",
500
- initialValue: state.coverageSummaryPath,
501
- validate: (v) => {
502
- if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
503
- }
504
- });
505
- assertNotCancelled(result);
506
- state.coverageSummaryPath = result.trim();
507
- }
508
- if (choice === "coverageCommand") {
509
- const result = await clack3.text({
510
- message: "Coverage command (blank to auto-detect from package.json)?",
511
- initialValue: state.coverageCommand ?? "",
512
- placeholder: "(auto-detect from package.json test runner)"
513
- });
514
- assertNotCancelled(result);
515
- const trimmed = result.trim();
516
- state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
517
- }
518
- if (choice === "enforceNaming") {
519
- const result = await clack3.confirm({
520
- message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
521
- initialValue: state.enforceNaming
522
- });
523
- assertNotCancelled(result);
524
- state.enforceNaming = result;
525
- }
526
- if (choice === "fileNaming") {
527
- const selected = await clack3.select({
528
- message: "Which file naming convention should be enforced?",
529
- options: [
530
- { value: "kebab-case", label: "kebab-case" },
531
- { value: "camelCase", label: "camelCase" },
532
- { value: "PascalCase", label: "PascalCase" },
533
- { value: "snake_case", label: "snake_case" }
534
- ],
535
- initialValue: state.fileNamingValue
536
- });
537
- assertNotCancelled(selected);
538
- state.fileNamingValue = selected;
539
- }
540
637
  }
541
638
 
542
639
  // src/utils/prompt-rules.ts
543
- function getRootPackage(packages) {
544
- return packages.find((pkg) => pkg.path === ".") ?? packages[0];
545
- }
546
640
  async function promptRuleMenu(defaults) {
547
641
  const state = {
548
642
  ...defaults,
@@ -559,10 +653,14 @@ async function promptRuleMenu(defaults) {
559
653
  }
560
654
  return {
561
655
  maxFileLines: state.maxFileLines,
656
+ maxTestFileLines: state.maxTestFileLines,
562
657
  testCoverage: state.testCoverage,
563
658
  enforceMissingTests: state.enforceMissingTests,
564
659
  enforceNaming: state.enforceNaming,
565
660
  fileNamingValue: state.fileNamingValue,
661
+ componentNaming: state.componentNaming,
662
+ hookNaming: state.hookNaming,
663
+ importAlias: state.importAlias,
566
664
  coverageSummaryPath: state.coverageSummaryPath,
567
665
  coverageCommand: state.coverageCommand,
568
666
  packageOverrides: state.packageOverrides
@@ -610,30 +708,6 @@ async function promptExistingConfigAction(configFile) {
610
708
  assertNotCancelled(result);
611
709
  return result;
612
710
  }
613
- async function promptInitDecision() {
614
- const result = await clack5.select({
615
- message: "How do you want to proceed?",
616
- options: [
617
- {
618
- value: "accept",
619
- label: "Accept defaults",
620
- hint: "writes the config with these defaults; use --enforce in CI to block"
621
- },
622
- {
623
- value: "customize",
624
- label: "Customize rules",
625
- hint: "edit limits, naming, test coverage, and package overrides"
626
- },
627
- {
628
- value: "review",
629
- label: "Review detected details",
630
- hint: "show the full scan report with package and structure details"
631
- }
632
- ]
633
- });
634
- assertNotCancelled(result);
635
- return result;
636
- }
637
711
 
638
712
  // src/utils/resolve-workspace-packages.ts
639
713
  var fs2 = __toESM(require("fs"), 1);
@@ -826,7 +900,7 @@ function resolveIgnoreForFile(relPath, config) {
826
900
  }
827
901
 
828
902
  // src/commands/check-coverage.ts
829
- var import_node_child_process2 = require("child_process");
903
+ var import_node_child_process = require("child_process");
830
904
  var fs4 = __toESM(require("fs"), 1);
831
905
  var path4 = __toESM(require("path"), 1);
832
906
  var import_config3 = require("@viberails/config");
@@ -869,7 +943,7 @@ function readCoveragePercentage(summaryPath) {
869
943
  }
870
944
  }
871
945
  function runCoverageCommand(pkgRoot, command) {
872
- const result = (0, import_node_child_process2.spawnSync)(command, {
946
+ const result = (0, import_node_child_process.spawnSync)(command, {
873
947
  cwd: pkgRoot,
874
948
  shell: true,
875
949
  encoding: "utf-8",
@@ -964,7 +1038,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
964
1038
  }
965
1039
 
966
1040
  // src/commands/check-files.ts
967
- var import_node_child_process3 = require("child_process");
1041
+ var import_node_child_process2 = require("child_process");
968
1042
  var fs5 = __toESM(require("fs"), 1);
969
1043
  var path5 = __toESM(require("path"), 1);
970
1044
  var import_config4 = require("@viberails/config");
@@ -1037,7 +1111,7 @@ function checkNaming(relPath, conventions) {
1037
1111
  }
1038
1112
  function getStagedFiles(projectRoot) {
1039
1113
  try {
1040
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1114
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
1041
1115
  cwd: projectRoot,
1042
1116
  encoding: "utf-8",
1043
1117
  stdio: ["ignore", "pipe", "ignore"]
@@ -1049,12 +1123,12 @@ function getStagedFiles(projectRoot) {
1049
1123
  }
1050
1124
  function getDiffFiles(projectRoot, base) {
1051
1125
  try {
1052
- const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1126
+ const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1053
1127
  cwd: projectRoot,
1054
1128
  encoding: "utf-8",
1055
1129
  stdio: ["ignore", "pipe", "ignore"]
1056
1130
  });
1057
- const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1131
+ const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1058
1132
  cwd: projectRoot,
1059
1133
  encoding: "utf-8",
1060
1134
  stdio: ["ignore", "pipe", "ignore"]
@@ -1100,7 +1174,7 @@ function deletedTestFileToSourceFile(deletedTestFile, config) {
1100
1174
  }
1101
1175
  function getStagedDeletedTestSourceFiles(projectRoot, config) {
1102
1176
  try {
1103
- const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=D", {
1177
+ const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=D", {
1104
1178
  cwd: projectRoot,
1105
1179
  encoding: "utf-8",
1106
1180
  stdio: ["ignore", "pipe", "ignore"]
@@ -1112,7 +1186,7 @@ function getStagedDeletedTestSourceFiles(projectRoot, config) {
1112
1186
  }
1113
1187
  function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
1114
1188
  try {
1115
- const output = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1189
+ const output = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
1116
1190
  cwd: projectRoot,
1117
1191
  encoding: "utf-8",
1118
1192
  stdio: ["ignore", "pipe", "ignore"]
@@ -1251,13 +1325,13 @@ function checkMissingTests(projectRoot, config, severity) {
1251
1325
  const testSuffix = testPattern.replace("*", "");
1252
1326
  const sourceFiles = collectSourceFiles(srcPath, projectRoot);
1253
1327
  for (const relFile of sourceFiles) {
1254
- const basename9 = path6.basename(relFile);
1255
- if (basename9.includes(".test.") || basename9.includes(".spec.") || basename9.startsWith("index.") || basename9.endsWith(".d.ts")) {
1328
+ const basename10 = path6.basename(relFile);
1329
+ if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
1256
1330
  continue;
1257
1331
  }
1258
- const ext = path6.extname(basename9);
1332
+ const ext = path6.extname(basename10);
1259
1333
  if (!SOURCE_EXTS2.has(ext)) continue;
1260
- const stem = basename9.slice(0, -ext.length);
1334
+ const stem = basename10.slice(0, -ext.length);
1261
1335
  const expectedTestFile = `${stem}${testSuffix}`;
1262
1336
  const dir = path6.dirname(path6.join(projectRoot, relFile));
1263
1337
  const colocatedTest = path6.join(dir, expectedTestFile);
@@ -1338,9 +1412,9 @@ async function checkCommand(options, cwd) {
1338
1412
  }
1339
1413
  const violations = [];
1340
1414
  const severity = options.enforce ? "error" : "warn";
1341
- const log7 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1415
+ const log8 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
1342
1416
  };
1343
- log7(" Checking files...");
1417
+ log8(" Checking files...");
1344
1418
  for (const file of filesToCheck) {
1345
1419
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1346
1420
  const relPath = path7.relative(projectRoot, absPath);
@@ -1373,9 +1447,9 @@ async function checkCommand(options, cwd) {
1373
1447
  }
1374
1448
  }
1375
1449
  }
1376
- log7(" done\n");
1450
+ log8(" done\n");
1377
1451
  if (!options.files) {
1378
- log7(" Checking missing tests...");
1452
+ log8(" Checking missing tests...");
1379
1453
  const testViolations = checkMissingTests(projectRoot, config, severity);
1380
1454
  if (options.staged) {
1381
1455
  const stagedSet = new Set(filesToCheck);
@@ -1388,14 +1462,14 @@ async function checkCommand(options, cwd) {
1388
1462
  } else {
1389
1463
  violations.push(...testViolations);
1390
1464
  }
1391
- log7(" done\n");
1465
+ log8(" done\n");
1392
1466
  }
1393
1467
  if (!options.files && !options.staged && !options.diffBase) {
1394
- log7(" Running test coverage...\n");
1468
+ log8(" Running test coverage...\n");
1395
1469
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1396
1470
  staged: options.staged,
1397
1471
  enforce: options.enforce,
1398
- onProgress: (pkg) => log7(` Coverage: ${pkg}...
1472
+ onProgress: (pkg) => log8(` Coverage: ${pkg}...
1399
1473
  `)
1400
1474
  });
1401
1475
  violations.push(...coverageViolations);
@@ -1420,7 +1494,7 @@ async function checkCommand(options, cwd) {
1420
1494
  severity
1421
1495
  });
1422
1496
  }
1423
- log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1497
+ log8(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1424
1498
  `);
1425
1499
  }
1426
1500
  if (options.format === "json") {
@@ -1675,15 +1749,6 @@ function formatMonorepoResultsText(scanResult) {
1675
1749
  }
1676
1750
 
1677
1751
  // src/display.ts
1678
- var INIT_OVERVIEW_NAMES = {
1679
- typescript: "TypeScript",
1680
- javascript: "JavaScript",
1681
- eslint: "ESLint",
1682
- prettier: "Prettier",
1683
- jest: "Jest",
1684
- vitest: "Vitest",
1685
- biome: "Biome"
1686
- };
1687
1752
  function formatItem(item, nameMap) {
1688
1753
  const name = nameMap?.[item.name] ?? item.name;
1689
1754
  return item.version ? `${name} ${item.version}` : name;
@@ -1813,134 +1878,6 @@ function displayRulesPreview(config) {
1813
1878
  );
1814
1879
  console.log("");
1815
1880
  }
1816
- function formatDetectedOverview(scanResult) {
1817
- const { stack } = scanResult;
1818
- const primaryParts = [];
1819
- const secondaryParts = [];
1820
- const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
1821
- if (scanResult.packages.length > 1) {
1822
- primaryParts.push("monorepo");
1823
- primaryParts.push(`${scanResult.packages.length} packages`);
1824
- } else if (stack.framework) {
1825
- primaryParts.push(formatItem(stack.framework, import_types3.FRAMEWORK_NAMES));
1826
- } else {
1827
- primaryParts.push("single package");
1828
- }
1829
- primaryParts.push(formatOverviewItem(stack.language));
1830
- if (stack.styling) {
1831
- primaryParts.push(formatOverviewItem(stack.styling, import_types3.STYLING_NAMES));
1832
- }
1833
- if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
1834
- if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
1835
- if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
1836
- if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
1837
- const primary = primaryParts.map((part) => import_chalk5.default.cyan(part)).join(import_chalk5.default.dim(" \xB7 "));
1838
- const secondary = secondaryParts.join(import_chalk5.default.dim(" \xB7 "));
1839
- return secondary ? `${primary}
1840
- ${import_chalk5.default.dim(secondary)}` : primary;
1841
- }
1842
- function displayInitOverview(scanResult, config, exemptedPackages) {
1843
- const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1844
- const isMonorepo = config.packages.length > 1;
1845
- const ok = import_chalk5.default.green("\u2713");
1846
- const info = import_chalk5.default.yellow("~");
1847
- console.log("");
1848
- console.log(` ${import_chalk5.default.bold("Ready to initialize:")}`);
1849
- console.log(` ${formatDetectedOverview(scanResult)}`);
1850
- console.log("");
1851
- console.log(` ${import_chalk5.default.bold("Rules to apply:")}`);
1852
- console.log(` ${ok} Max file size: ${import_chalk5.default.cyan(`${config.rules.maxFileLines} lines`)}`);
1853
- const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
1854
- if (config.rules.enforceNaming && fileNaming) {
1855
- console.log(` ${ok} File naming: ${import_chalk5.default.cyan(fileNaming)}`);
1856
- } else {
1857
- console.log(` ${info} File naming: ${import_chalk5.default.dim("not enforced")}`);
1858
- }
1859
- const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
1860
- if (config.rules.enforceMissingTests && testPattern) {
1861
- console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan(`enforced (${testPattern})`)}`);
1862
- } else if (config.rules.enforceMissingTests) {
1863
- console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan("enforced")}`);
1864
- } else {
1865
- console.log(` ${info} Missing tests: ${import_chalk5.default.dim("not enforced")}`);
1866
- }
1867
- if (config.rules.testCoverage > 0) {
1868
- if (isMonorepo) {
1869
- const withCoverage = config.packages.filter(
1870
- (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
1871
- );
1872
- console.log(
1873
- ` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk5.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
1874
- );
1875
- } else {
1876
- console.log(` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)}`);
1877
- }
1878
- } else {
1879
- console.log(` ${info} Coverage: ${import_chalk5.default.dim("disabled")}`);
1880
- }
1881
- if (exemptedPackages.length > 0) {
1882
- console.log(
1883
- ` ${import_chalk5.default.dim(" exempted:")} ${import_chalk5.default.dim(exemptedPackages.join(", "))} ${import_chalk5.default.dim("(types-only)")}`
1884
- );
1885
- }
1886
- console.log("");
1887
- console.log(` ${import_chalk5.default.bold("Also available:")}`);
1888
- if (isMonorepo) {
1889
- console.log(` ${info} Infer boundaries from current imports`);
1890
- }
1891
- console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
1892
- console.log(
1893
- `
1894
- ${import_chalk5.default.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
1895
- );
1896
- console.log("");
1897
- }
1898
- function summarizeSelectedIntegrations(integrations, opts) {
1899
- const lines = [];
1900
- if (opts.hasBoundaries) {
1901
- lines.push("\u2713 Boundary rules: inferred from current imports");
1902
- } else {
1903
- lines.push("~ Boundary rules: not enabled");
1904
- }
1905
- if (opts.hasCoverage) {
1906
- lines.push("\u2713 Coverage checks: enabled");
1907
- } else {
1908
- lines.push("~ Coverage checks: disabled");
1909
- }
1910
- const selectedIntegrations = [
1911
- integrations.preCommitHook ? "pre-commit hook" : void 0,
1912
- integrations.typecheckHook ? "typecheck" : void 0,
1913
- integrations.lintHook ? "lint check" : void 0,
1914
- integrations.claudeCodeHook ? "Claude Code hook" : void 0,
1915
- integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
1916
- integrations.githubAction ? "GitHub Actions workflow" : void 0
1917
- ].filter(Boolean);
1918
- if (selectedIntegrations.length > 0) {
1919
- lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
1920
- } else {
1921
- lines.push("~ Integrations: none selected");
1922
- }
1923
- return lines;
1924
- }
1925
- function displaySetupPlan(config, integrations, opts = {}) {
1926
- const configFile = opts.configFile ?? "viberails.config.json";
1927
- const lines = summarizeSelectedIntegrations(integrations, {
1928
- hasBoundaries: config.rules.enforceBoundaries,
1929
- hasCoverage: config.rules.testCoverage > 0
1930
- });
1931
- console.log("");
1932
- console.log(` ${import_chalk5.default.bold("Ready to write:")}`);
1933
- console.log(
1934
- ` ${opts.replacingExistingConfig ? import_chalk5.default.yellow("!") : import_chalk5.default.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? import_chalk5.default.dim(" (replacing existing config)") : ""}`
1935
- );
1936
- console.log(` ${import_chalk5.default.green("\u2713")} .viberails/context.md`);
1937
- console.log(` ${import_chalk5.default.green("\u2713")} .viberails/scan-result.json`);
1938
- for (const line of lines) {
1939
- const icon = line.startsWith("\u2713") ? import_chalk5.default.green("\u2713") : import_chalk5.default.yellow("~");
1940
- console.log(` ${icon} ${line.slice(2)}`);
1941
- }
1942
- console.log("");
1943
- }
1944
1881
 
1945
1882
  // src/display-text.ts
1946
1883
  function plainConfidenceLabel(convention) {
@@ -2059,7 +1996,9 @@ function formatScanResultsText(scanResult) {
2059
1996
  // src/utils/apply-rule-overrides.ts
2060
1997
  function applyRuleOverrides(config, overrides) {
2061
1998
  if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
1999
+ const rootPkg = getRootPackage(config.packages);
2062
2000
  config.rules.maxFileLines = overrides.maxFileLines;
2001
+ config.rules.maxTestFileLines = overrides.maxTestFileLines;
2063
2002
  config.rules.testCoverage = overrides.testCoverage;
2064
2003
  config.rules.enforceMissingTests = overrides.enforceMissingTests;
2065
2004
  config.rules.enforceNaming = overrides.enforceNaming;
@@ -2073,7 +2012,6 @@ function applyRuleOverrides(config, overrides) {
2073
2012
  }
2074
2013
  }
2075
2014
  if (overrides.fileNamingValue) {
2076
- const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2077
2015
  const oldNaming = rootPkg.conventions?.fileNaming;
2078
2016
  rootPkg.conventions = rootPkg.conventions ?? {};
2079
2017
  rootPkg.conventions.fileNaming = overrides.fileNamingValue;
@@ -2085,6 +2023,18 @@ function applyRuleOverrides(config, overrides) {
2085
2023
  }
2086
2024
  }
2087
2025
  }
2026
+ if (rootPkg) {
2027
+ rootPkg.conventions = rootPkg.conventions ?? {};
2028
+ if (overrides.componentNaming !== void 0) {
2029
+ rootPkg.conventions.componentNaming = overrides.componentNaming || void 0;
2030
+ }
2031
+ if (overrides.hookNaming !== void 0) {
2032
+ rootPkg.conventions.hookNaming = overrides.hookNaming || void 0;
2033
+ }
2034
+ if (overrides.importAlias !== void 0) {
2035
+ rootPkg.conventions.importAlias = overrides.importAlias || void 0;
2036
+ }
2037
+ }
2088
2038
  }
2089
2039
 
2090
2040
  // src/utils/diff-configs.ts
@@ -2261,10 +2211,14 @@ async function configCommand(options, cwd) {
2261
2211
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2262
2212
  const overrides = await promptRuleMenu({
2263
2213
  maxFileLines: config.rules.maxFileLines,
2214
+ maxTestFileLines: config.rules.maxTestFileLines,
2264
2215
  testCoverage: config.rules.testCoverage,
2265
2216
  enforceMissingTests: config.rules.enforceMissingTests,
2266
2217
  enforceNaming: config.rules.enforceNaming,
2267
2218
  fileNamingValue: rootPkg.conventions?.fileNaming,
2219
+ componentNaming: rootPkg.conventions?.componentNaming,
2220
+ hookNaming: rootPkg.conventions?.hookNaming,
2221
+ importAlias: rootPkg.conventions?.importAlias,
2268
2222
  coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
2269
2223
  coverageCommand: config.defaults?.coverage?.command,
2270
2224
  packageOverrides: config.packages
@@ -2339,7 +2293,7 @@ var import_config7 = require("@viberails/config");
2339
2293
  var import_chalk8 = __toESM(require("chalk"), 1);
2340
2294
 
2341
2295
  // src/commands/fix-helpers.ts
2342
- var import_node_child_process4 = require("child_process");
2296
+ var import_node_child_process3 = require("child_process");
2343
2297
  var import_chalk7 = __toESM(require("chalk"), 1);
2344
2298
  function printPlan(renames, stubs) {
2345
2299
  if (renames.length > 0) {
@@ -2357,7 +2311,7 @@ function printPlan(renames, stubs) {
2357
2311
  }
2358
2312
  function checkGitDirty(projectRoot) {
2359
2313
  try {
2360
- const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
2314
+ const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
2361
2315
  cwd: projectRoot,
2362
2316
  encoding: "utf-8",
2363
2317
  stdio: ["ignore", "pipe", "ignore"]
@@ -2645,10 +2599,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
2645
2599
  const pkg = resolvePackageForFile(sourceRelPath, config);
2646
2600
  const testPattern = pkg?.structure?.testPattern;
2647
2601
  if (!testPattern) return null;
2648
- const basename9 = path12.basename(sourceRelPath);
2649
- const ext = path12.extname(basename9);
2602
+ const basename10 = path12.basename(sourceRelPath);
2603
+ const ext = path12.extname(basename10);
2650
2604
  if (!ext) return null;
2651
- const stem = basename9.slice(0, -ext.length);
2605
+ const stem = basename10.slice(0, -ext.length);
2652
2606
  const testSuffix = testPattern.replace("*", "");
2653
2607
  const testFilename = `${stem}${testSuffix}`;
2654
2608
  const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
@@ -2820,169 +2774,601 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
2820
2774
  }
2821
2775
 
2822
2776
  // src/commands/init.ts
2823
- var fs19 = __toESM(require("fs"), 1);
2824
- var path19 = __toESM(require("path"), 1);
2777
+ var fs21 = __toESM(require("fs"), 1);
2778
+ var path21 = __toESM(require("path"), 1);
2779
+ var clack12 = __toESM(require("@clack/prompts"), 1);
2780
+ var import_config9 = require("@viberails/config");
2781
+ var import_scanner3 = require("@viberails/scanner");
2782
+ var import_chalk13 = __toESM(require("chalk"), 1);
2783
+
2784
+ // src/utils/check-prerequisites.ts
2785
+ var fs14 = __toESM(require("fs"), 1);
2786
+ var path14 = __toESM(require("path"), 1);
2787
+ var clack7 = __toESM(require("@clack/prompts"), 1);
2788
+ var import_chalk9 = __toESM(require("chalk"), 1);
2789
+
2790
+ // src/utils/spawn-async.ts
2791
+ var import_node_child_process4 = require("child_process");
2792
+ function spawnAsync(command, cwd) {
2793
+ return new Promise((resolve4) => {
2794
+ const child = (0, import_node_child_process4.spawn)(command, { cwd, shell: true, stdio: "pipe" });
2795
+ let stdout = "";
2796
+ let stderr = "";
2797
+ child.stdout.on("data", (d) => {
2798
+ stdout += d.toString();
2799
+ });
2800
+ child.stderr.on("data", (d) => {
2801
+ stderr += d.toString();
2802
+ });
2803
+ child.on("close", (status) => {
2804
+ resolve4({ status, stdout, stderr });
2805
+ });
2806
+ child.on("error", () => {
2807
+ resolve4({ status: 1, stdout, stderr });
2808
+ });
2809
+ });
2810
+ }
2811
+
2812
+ // src/utils/check-prerequisites.ts
2813
+ function checkCoveragePrereqs(projectRoot, scanResult) {
2814
+ const pm = scanResult.stack.packageManager.name;
2815
+ const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
2816
+ const hasVitest = vitestPackages.length > 0 || scanResult.stack.testRunner?.name === "vitest";
2817
+ if (!hasVitest) return [];
2818
+ let installed = hasDependency(projectRoot, "@vitest/coverage-v8") || hasDependency(projectRoot, "@vitest/coverage-istanbul");
2819
+ if (!installed && vitestPackages.length > 0) {
2820
+ installed = vitestPackages.every((rel) => {
2821
+ const pkgDir = path14.join(projectRoot, rel);
2822
+ return hasDependency(pkgDir, "@vitest/coverage-v8") || hasDependency(pkgDir, "@vitest/coverage-istanbul");
2823
+ });
2824
+ }
2825
+ const isWorkspace = scanResult.packages.length > 1;
2826
+ const addCmd = pm === "yarn" ? "yarn add -D" : pm === "pnpm" && isWorkspace ? "pnpm add -D -w" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
2827
+ const affectedPackages = vitestPackages.length > 1 ? vitestPackages : void 0;
2828
+ const reason = affectedPackages ? `Required for coverage in: ${affectedPackages.join(", ")}` : "Required for coverage percentage checks with vitest";
2829
+ return [
2830
+ {
2831
+ label: "@vitest/coverage-v8",
2832
+ installed,
2833
+ installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
2834
+ reason,
2835
+ affectedPackages
2836
+ }
2837
+ ];
2838
+ }
2839
+ function displayMissingPrereqs(prereqs) {
2840
+ const missing = prereqs.filter((p) => !p.installed);
2841
+ for (const m of missing) {
2842
+ const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
2843
+ console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
2844
+ if (m.installCommand) {
2845
+ console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
2846
+ }
2847
+ }
2848
+ }
2849
+ function planCoverageInstall(prereqs) {
2850
+ const missing = prereqs.find((p) => !p.installed && p.installCommand);
2851
+ if (!missing?.installCommand) return void 0;
2852
+ return {
2853
+ label: missing.label,
2854
+ command: missing.installCommand
2855
+ };
2856
+ }
2857
+ function hasDependency(projectRoot, name) {
2858
+ try {
2859
+ const pkgPath = path14.join(projectRoot, "package.json");
2860
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2861
+ return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2862
+ } catch {
2863
+ return false;
2864
+ }
2865
+ }
2866
+
2867
+ // src/utils/deferred-install.ts
2825
2868
  var clack8 = __toESM(require("@clack/prompts"), 1);
2826
- var import_config8 = require("@viberails/config");
2827
- var import_scanner2 = require("@viberails/scanner");
2828
- var import_chalk12 = __toESM(require("chalk"), 1);
2869
+ async function executeDeferredInstalls(projectRoot, installs) {
2870
+ if (installs.length === 0) return 0;
2871
+ let successCount = 0;
2872
+ for (const install of installs) {
2873
+ const s = clack8.spinner();
2874
+ s.start(`Installing ${install.label}...`);
2875
+ const result = await spawnAsync(install.command, projectRoot);
2876
+ if (result.status === 0) {
2877
+ s.stop(`Installed ${install.label}`);
2878
+ install.onSuccess?.();
2879
+ successCount++;
2880
+ } else {
2881
+ s.stop(`Failed to install ${install.label}`);
2882
+ clack8.log.warn(`Install manually: ${install.command}`);
2883
+ install.onFailure?.();
2884
+ }
2885
+ }
2886
+ return successCount;
2887
+ }
2888
+
2889
+ // src/utils/prompt-main-menu.ts
2890
+ var clack10 = __toESM(require("@clack/prompts"), 1);
2891
+
2892
+ // src/utils/prompt-integrations.ts
2893
+ var fs15 = __toESM(require("fs"), 1);
2894
+ var path15 = __toESM(require("path"), 1);
2895
+ var clack9 = __toESM(require("@clack/prompts"), 1);
2896
+ function buildLefthookInstallCommand(pm, isWorkspace) {
2897
+ if (pm === "yarn") return "yarn add -D lefthook";
2898
+ if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
2899
+ if (pm === "npm") return "npm install -D lefthook";
2900
+ return `${pm} add -D lefthook`;
2901
+ }
2902
+ async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
2903
+ const options = [];
2904
+ const needsLefthook = !hookManager;
2905
+ if (needsLefthook) {
2906
+ const pm = packageManager ?? "npm";
2907
+ options.push({
2908
+ value: "installLefthook",
2909
+ label: "Install Lefthook",
2910
+ hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
2911
+ });
2912
+ }
2913
+ const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
2914
+ const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
2915
+ options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
2916
+ if (tools?.isTypeScript) {
2917
+ options.push({
2918
+ value: "typecheck",
2919
+ label: "Typecheck (tsc --noEmit)",
2920
+ hint: "pre-commit hook + CI check"
2921
+ });
2922
+ }
2923
+ if (tools?.linter) {
2924
+ const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
2925
+ options.push({
2926
+ value: "lint",
2927
+ label: `Lint check (${linterName})`,
2928
+ hint: "pre-commit hook + CI check"
2929
+ });
2930
+ }
2931
+ options.push(
2932
+ {
2933
+ value: "claude",
2934
+ label: "Claude Code hook",
2935
+ hint: "checks files when Claude edits them"
2936
+ },
2937
+ {
2938
+ value: "claudeMd",
2939
+ label: "CLAUDE.md reference",
2940
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
2941
+ },
2942
+ {
2943
+ value: "githubAction",
2944
+ label: "GitHub Actions workflow",
2945
+ hint: "blocks PRs that fail viberails check"
2946
+ }
2947
+ );
2948
+ const initialValues = options.map((o) => o.value);
2949
+ const result = await clack9.multiselect({
2950
+ message: "Integrations",
2951
+ options,
2952
+ initialValues,
2953
+ required: false
2954
+ });
2955
+ assertNotCancelled(result);
2956
+ let lefthookInstall;
2957
+ if (needsLefthook && result.includes("installLefthook")) {
2958
+ const pm = packageManager ?? "npm";
2959
+ lefthookInstall = {
2960
+ label: "Lefthook",
2961
+ command: buildLefthookInstallCommand(pm, isWorkspace),
2962
+ onSuccess: projectRoot ? () => {
2963
+ const ymlPath = path15.join(projectRoot, "lefthook.yml");
2964
+ if (!fs15.existsSync(ymlPath)) {
2965
+ fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
2966
+ }
2967
+ } : void 0
2968
+ };
2969
+ }
2970
+ return {
2971
+ choice: {
2972
+ preCommitHook: result.includes("preCommit"),
2973
+ claudeCodeHook: result.includes("claude"),
2974
+ claudeMdRef: result.includes("claudeMd"),
2975
+ githubAction: result.includes("githubAction"),
2976
+ typecheckHook: result.includes("typecheck"),
2977
+ lintHook: result.includes("lint")
2978
+ },
2979
+ lefthookInstall
2980
+ };
2981
+ }
2982
+
2983
+ // src/utils/prompt-main-menu-hints.ts
2984
+ function fileLimitsHint(config) {
2985
+ const max = config.rules.maxFileLines;
2986
+ const test = config.rules.maxTestFileLines;
2987
+ return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
2988
+ }
2989
+ function fileNamingHint(config, scanResult) {
2990
+ const rootPkg = getRootPackage(config.packages);
2991
+ const naming = rootPkg.conventions?.fileNaming;
2992
+ if (!config.rules.enforceNaming) return "not enforced";
2993
+ if (naming) {
2994
+ const detected = scanResult.packages.some(
2995
+ (p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
2996
+ );
2997
+ return detected ? `${naming} (detected)` : naming;
2998
+ }
2999
+ return "mixed \u2014 will not enforce if skipped";
3000
+ }
3001
+ function fileNamingStatus(config) {
3002
+ if (!config.rules.enforceNaming) return "disabled";
3003
+ const rootPkg = getRootPackage(config.packages);
3004
+ return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
3005
+ }
3006
+ function missingTestsHint(config) {
3007
+ if (!config.rules.enforceMissingTests) return "not enforced";
3008
+ const rootPkg = getRootPackage(config.packages);
3009
+ const pattern = rootPkg.structure?.testPattern;
3010
+ return pattern ? `enforced (${pattern})` : "enforced";
3011
+ }
3012
+ function coverageHint(config, hasTestRunner) {
3013
+ if (config.rules.testCoverage === 0) return "disabled";
3014
+ if (!hasTestRunner)
3015
+ return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3016
+ const isMonorepo = config.packages.length > 1;
3017
+ if (isMonorepo) {
3018
+ const withCov = config.packages.filter(
3019
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3020
+ );
3021
+ const exempt = config.packages.length - withCov.length;
3022
+ return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3023
+ }
3024
+ return `${config.rules.testCoverage}%`;
3025
+ }
3026
+ function advancedNamingHint(config) {
3027
+ const rootPkg = getRootPackage(config.packages);
3028
+ const parts = [];
3029
+ if (rootPkg.conventions?.componentNaming)
3030
+ parts.push(`${rootPkg.conventions.componentNaming} components`);
3031
+ if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
3032
+ if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
3033
+ return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
3034
+ }
3035
+ function integrationsHint(state) {
3036
+ if (!state.visited.integrations || !state.integrations)
3037
+ return "not configured \u2014 select to set up";
3038
+ const items = [];
3039
+ if (state.integrations.preCommitHook) items.push("pre-commit");
3040
+ if (state.integrations.typecheckHook) items.push("typecheck");
3041
+ if (state.integrations.lintHook) items.push("lint");
3042
+ if (state.integrations.claudeCodeHook) items.push("Claude");
3043
+ if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
3044
+ if (state.integrations.githubAction) items.push("CI");
3045
+ return items.length > 0 ? items.join(" \xB7 ") : "none selected";
3046
+ }
3047
+ function packageOverridesHint(config) {
3048
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3049
+ const editable = config.packages.filter((p) => p.path !== ".");
3050
+ const customized = editable.filter(
3051
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3052
+ ).length;
3053
+ return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3054
+ }
3055
+ function boundariesHint(config, state) {
3056
+ if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3057
+ const deny = config.boundaries?.deny;
3058
+ if (!deny) return "enabled";
3059
+ const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3060
+ const pkgCount = Object.keys(deny).length;
3061
+ return `${ruleCount} rules across ${pkgCount} packages`;
3062
+ }
3063
+ function statusIcon(status) {
3064
+ if (status === "ok") return "\u2713";
3065
+ if (status === "needs-input") return "?";
3066
+ return "~";
3067
+ }
3068
+ function buildMainMenuOptions(config, scanResult, state) {
3069
+ const namingStatus = fileNamingStatus(config);
3070
+ const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
3071
+ const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
3072
+ const options = [
3073
+ {
3074
+ value: "fileLimits",
3075
+ label: `${statusIcon("ok")} Max file size`,
3076
+ hint: fileLimitsHint(config)
3077
+ },
3078
+ {
3079
+ value: "fileNaming",
3080
+ label: `${statusIcon(namingStatus)} File naming`,
3081
+ hint: fileNamingHint(config, scanResult)
3082
+ },
3083
+ {
3084
+ value: "missingTests",
3085
+ label: `${statusIcon(missingTestsStatus)} Missing tests`,
3086
+ hint: missingTestsHint(config)
3087
+ },
3088
+ {
3089
+ value: "coverage",
3090
+ label: `${statusIcon(coverageStatus)} Coverage`,
3091
+ hint: coverageHint(config, state.hasTestRunner)
3092
+ },
3093
+ { value: "advancedNaming", label: " Advanced naming", hint: advancedNamingHint(config) }
3094
+ ];
3095
+ if (config.packages.length > 1) {
3096
+ const bIcon = state.visited.boundaries && config.rules.enforceBoundaries ? statusIcon("ok") : " ";
3097
+ options.push(
3098
+ {
3099
+ value: "packageOverrides",
3100
+ label: " Per-package overrides",
3101
+ hint: packageOverridesHint(config)
3102
+ },
3103
+ { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3104
+ );
3105
+ }
3106
+ const iIcon = state.visited.integrations ? statusIcon("ok") : " ";
3107
+ options.push(
3108
+ { value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
3109
+ { value: "reset", label: " Reset all to defaults" },
3110
+ { value: "review", label: " Review scan details" },
3111
+ { value: "done", label: " Done \u2014 write config" }
3112
+ );
3113
+ return options;
3114
+ }
2829
3115
 
2830
- // src/utils/check-prerequisites.ts
2831
- var fs14 = __toESM(require("fs"), 1);
2832
- var path14 = __toESM(require("path"), 1);
2833
- var clack7 = __toESM(require("@clack/prompts"), 1);
2834
- var import_chalk9 = __toESM(require("chalk"), 1);
2835
- function checkCoveragePrereqs(projectRoot, scanResult) {
2836
- const pm = scanResult.stack.packageManager.name;
2837
- const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
2838
- const hasVitest = vitestPackages.length > 0 || scanResult.stack.testRunner?.name === "vitest";
2839
- if (!hasVitest) return [];
2840
- let installed = hasDependency(projectRoot, "@vitest/coverage-v8") || hasDependency(projectRoot, "@vitest/coverage-istanbul");
2841
- if (!installed && vitestPackages.length > 0) {
2842
- installed = vitestPackages.every((rel) => {
2843
- const pkgDir = path14.join(projectRoot, rel);
2844
- return hasDependency(pkgDir, "@vitest/coverage-v8") || hasDependency(pkgDir, "@vitest/coverage-istanbul");
2845
- });
3116
+ // src/utils/prompt-main-menu.ts
3117
+ async function handleAdvancedNaming(config) {
3118
+ const rootPkg = getRootPackage(config.packages);
3119
+ const state = {
3120
+ maxFileLines: config.rules.maxFileLines,
3121
+ maxTestFileLines: config.rules.maxTestFileLines,
3122
+ testCoverage: config.rules.testCoverage,
3123
+ enforceMissingTests: config.rules.enforceMissingTests,
3124
+ enforceNaming: config.rules.enforceNaming,
3125
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3126
+ componentNaming: rootPkg.conventions?.componentNaming,
3127
+ hookNaming: rootPkg.conventions?.hookNaming,
3128
+ importAlias: rootPkg.conventions?.importAlias,
3129
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3130
+ coverageCommand: config.defaults?.coverage?.command
3131
+ };
3132
+ await promptNamingMenu(state);
3133
+ rootPkg.conventions = rootPkg.conventions ?? {};
3134
+ config.rules.enforceNaming = state.enforceNaming;
3135
+ if (state.fileNamingValue) {
3136
+ rootPkg.conventions.fileNaming = state.fileNamingValue;
3137
+ } else {
3138
+ delete rootPkg.conventions.fileNaming;
2846
3139
  }
2847
- const isWorkspace = scanResult.packages.length > 1;
2848
- const addCmd = pm === "yarn" ? "yarn add -D" : pm === "pnpm" && isWorkspace ? "pnpm add -D -w" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
2849
- const affectedPackages = vitestPackages.length > 1 ? vitestPackages : void 0;
2850
- const reason = affectedPackages ? `Required for coverage in: ${affectedPackages.join(", ")}` : "Required for coverage percentage checks with vitest";
2851
- return [
2852
- {
2853
- label: "@vitest/coverage-v8",
2854
- installed,
2855
- installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
2856
- reason,
2857
- affectedPackages
3140
+ rootPkg.conventions.componentNaming = state.componentNaming || void 0;
3141
+ rootPkg.conventions.hookNaming = state.hookNaming || void 0;
3142
+ rootPkg.conventions.importAlias = state.importAlias || void 0;
3143
+ }
3144
+ async function promptMainMenu(config, scanResult, opts) {
3145
+ const originalConfig = structuredClone(config);
3146
+ const state = {
3147
+ visited: { integrations: false, boundaries: false },
3148
+ deferredInstalls: [],
3149
+ hasTestRunner: opts.hasTestRunner,
3150
+ hookManager: opts.hookManager
3151
+ };
3152
+ while (true) {
3153
+ const options = buildMainMenuOptions(config, scanResult, state);
3154
+ const choice = await clack10.select({ message: "Configure viberails", options });
3155
+ assertNotCancelled(choice);
3156
+ if (choice === "done") {
3157
+ if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
3158
+ config.rules.enforceNaming = false;
3159
+ }
3160
+ break;
2858
3161
  }
2859
- ];
3162
+ if (choice === "fileLimits") {
3163
+ const s = {
3164
+ maxFileLines: config.rules.maxFileLines,
3165
+ maxTestFileLines: config.rules.maxTestFileLines
3166
+ };
3167
+ await promptFileLimitsMenu(s);
3168
+ config.rules.maxFileLines = s.maxFileLines;
3169
+ config.rules.maxTestFileLines = s.maxTestFileLines;
3170
+ }
3171
+ if (choice === "fileNaming") await handleFileNaming(config, scanResult);
3172
+ if (choice === "missingTests") await handleMissingTests(config);
3173
+ if (choice === "coverage") await handleCoverage(config, state, opts);
3174
+ if (choice === "advancedNaming") await handleAdvancedNaming(config);
3175
+ if (choice === "packageOverrides") await handlePackageOverrides(config);
3176
+ if (choice === "boundaries") await handleBoundaries(config, state, opts);
3177
+ if (choice === "integrations") await handleIntegrations(state, opts);
3178
+ if (choice === "review") clack10.note(formatScanResultsText(scanResult), "Scan details");
3179
+ if (choice === "reset") {
3180
+ Object.assign(config, structuredClone(originalConfig));
3181
+ state.deferredInstalls = [];
3182
+ state.visited = { integrations: false, boundaries: false };
3183
+ state.integrations = void 0;
3184
+ clack10.log.info("Reset all settings to scan-detected defaults.");
3185
+ }
3186
+ }
3187
+ return state;
2860
3188
  }
2861
- function displayMissingPrereqs(prereqs) {
2862
- const missing = prereqs.filter((p) => !p.installed);
2863
- for (const m of missing) {
2864
- const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
2865
- console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
2866
- if (m.installCommand) {
2867
- console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
3189
+ async function handleFileNaming(config, scanResult) {
3190
+ const isMonorepo = config.packages.length > 1;
3191
+ if (isMonorepo) {
3192
+ const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
3193
+ path: p.relativePath,
3194
+ naming: p.conventions.fileNaming
3195
+ }));
3196
+ if (pkgData.length > 0) {
3197
+ const lines = pkgData.map(
3198
+ (p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
3199
+ );
3200
+ clack10.note(lines.join("\n"), "Per-package file naming detected");
3201
+ }
3202
+ }
3203
+ const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
3204
+ if (isMonorepo) {
3205
+ const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
3206
+ const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
3207
+ return { value: opt.value, label: opt.label, hint };
2868
3208
  }
3209
+ return { value: opt.value, label: opt.label };
3210
+ });
3211
+ const rootPkg = getRootPackage(config.packages);
3212
+ const selected = await clack10.select({
3213
+ message: isMonorepo ? "Default file naming convention" : "File naming convention",
3214
+ options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
3215
+ initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
3216
+ });
3217
+ assertNotCancelled(selected);
3218
+ if (selected === SENTINEL_SKIP) {
3219
+ config.rules.enforceNaming = false;
3220
+ if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
3221
+ } else {
3222
+ config.rules.enforceNaming = true;
3223
+ rootPkg.conventions = rootPkg.conventions ?? {};
3224
+ rootPkg.conventions.fileNaming = selected;
2869
3225
  }
2870
3226
  }
2871
- async function promptMissingPrereqs(projectRoot, prereqs) {
2872
- const missing = prereqs.filter((p) => !p.installed);
2873
- if (missing.length === 0) return { disableCoverage: false };
2874
- const prereqLines = prereqs.map((p) => {
2875
- if (p.installed) return `\u2713 ${p.label}`;
2876
- const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
2877
- return `\u2717 ${p.label} \u2014 ${detail}`;
2878
- }).join("\n");
2879
- clack7.note(prereqLines, "Coverage support");
2880
- let disableCoverage = false;
2881
- for (const m of missing) {
2882
- if (!m.installCommand) continue;
2883
- const pkgCount = m.affectedPackages?.length;
2884
- 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.`;
2885
- const choice = await clack7.select({
2886
- message,
3227
+ async function handleMissingTests(config) {
3228
+ const result = await clack10.confirm({
3229
+ message: "Require every source file to have a test file?",
3230
+ initialValue: config.rules.enforceMissingTests
3231
+ });
3232
+ assertNotCancelled(result);
3233
+ config.rules.enforceMissingTests = result;
3234
+ }
3235
+ async function handleCoverage(config, state, opts) {
3236
+ if (!opts.hasTestRunner) {
3237
+ clack10.log.info("Coverage checks are inactive \u2014 no test runner detected.");
3238
+ return;
3239
+ }
3240
+ const planned = planCoverageInstall(opts.coveragePrereqs);
3241
+ if (planned) {
3242
+ const choice = await clack10.select({
3243
+ message: `${planned.label} is not installed. Needed for coverage checks.`,
2887
3244
  options: [
2888
3245
  {
2889
3246
  value: "install",
2890
- label: "Install now",
2891
- hint: m.installCommand
2892
- },
2893
- {
2894
- value: "disable",
2895
- label: "Disable coverage checks",
2896
- hint: "missing-test checks still stay active"
3247
+ label: "Install (after final confirmation)",
3248
+ hint: planned.command
2897
3249
  },
3250
+ { value: "disable", label: "Disable coverage checks" },
2898
3251
  {
2899
3252
  value: "skip",
2900
3253
  label: "Skip for now",
2901
- hint: `install later: ${m.installCommand}`
3254
+ hint: `install later: ${planned.command}`
2902
3255
  }
2903
3256
  ]
2904
3257
  });
2905
3258
  assertNotCancelled(choice);
3259
+ state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
2906
3260
  if (choice === "install") {
2907
- const is = clack7.spinner();
2908
- is.start(`Installing ${m.label}...`);
2909
- const result = await spawnAsync(m.installCommand, projectRoot);
2910
- if (result.status === 0) {
2911
- is.stop(`Installed ${m.label}`);
2912
- } else {
2913
- is.stop(`Failed to install ${m.label}`);
2914
- clack7.log.warn(
2915
- `Install manually: ${m.installCommand}
2916
- Coverage percentage checks will not work until the dependency is installed.`
2917
- );
2918
- }
3261
+ planned.onFailure = () => {
3262
+ config.rules.testCoverage = 0;
3263
+ };
3264
+ state.deferredInstalls.push(planned);
2919
3265
  } else if (choice === "disable") {
2920
- disableCoverage = true;
2921
- clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2922
- } else {
2923
- clack7.log.info(
2924
- `Coverage percentage checks will fail until ${m.label} is installed.
2925
- Install later: ${m.installCommand}`
2926
- );
3266
+ config.rules.testCoverage = 0;
3267
+ return;
2927
3268
  }
2928
3269
  }
2929
- return { disableCoverage };
3270
+ const result = await clack10.text({
3271
+ message: "Test coverage target (0 = disable)?",
3272
+ initialValue: String(config.rules.testCoverage),
3273
+ validate: (v) => {
3274
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
3275
+ const n = Number.parseInt(v, 10);
3276
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
3277
+ }
3278
+ });
3279
+ assertNotCancelled(result);
3280
+ config.rules.testCoverage = Number.parseInt(result, 10);
2930
3281
  }
2931
- function hasDependency(projectRoot, name) {
3282
+ async function handlePackageOverrides(config) {
3283
+ const rootPkg = getRootPackage(config.packages);
3284
+ config.packages = await promptPackageOverrides(config.packages, {
3285
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3286
+ maxFileLines: config.rules.maxFileLines,
3287
+ testCoverage: config.rules.testCoverage,
3288
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3289
+ coverageCommand: config.defaults?.coverage?.command
3290
+ });
3291
+ normalizePackageOverrides(config.packages);
3292
+ }
3293
+ async function handleBoundaries(config, state, opts) {
3294
+ const shouldInfer = await clack10.confirm({
3295
+ message: "Infer boundary rules from current import patterns?",
3296
+ initialValue: false
3297
+ });
3298
+ assertNotCancelled(shouldInfer);
3299
+ state.visited.boundaries = true;
3300
+ if (!shouldInfer) {
3301
+ config.rules.enforceBoundaries = false;
3302
+ return;
3303
+ }
3304
+ const bs = clack10.spinner();
3305
+ bs.start("Building import graph...");
2932
3306
  try {
2933
- const pkgPath = path14.join(projectRoot, "package.json");
2934
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2935
- return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2936
- } catch {
2937
- return false;
3307
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3308
+ const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
3309
+ const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
3310
+ const inferred = inferBoundaries(graph);
3311
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
3312
+ if (denyCount > 0) {
3313
+ config.boundaries = inferred;
3314
+ config.rules.enforceBoundaries = true;
3315
+ const pkgCount = Object.keys(inferred.deny).length;
3316
+ bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3317
+ } else {
3318
+ bs.stop("No boundary rules inferred");
3319
+ }
3320
+ } catch (err) {
3321
+ bs.stop("Failed to build import graph");
3322
+ clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
2938
3323
  }
2939
3324
  }
2940
-
2941
- // src/utils/filter-confidence.ts
2942
- function filterHighConfidence(conventions, meta) {
2943
- if (!meta) return conventions;
2944
- const filtered = {};
2945
- for (const [key, value] of Object.entries(conventions)) {
2946
- if (value === void 0) continue;
2947
- const convMeta = meta[key];
2948
- if (!convMeta || convMeta.confidence === "high") {
2949
- filtered[key] = value;
2950
- }
3325
+ async function handleIntegrations(state, opts) {
3326
+ const result = await promptIntegrationsDeferred(
3327
+ state.hookManager,
3328
+ opts.tools,
3329
+ opts.tools.packageManager,
3330
+ opts.tools.isWorkspace,
3331
+ opts.projectRoot
3332
+ );
3333
+ state.visited.integrations = true;
3334
+ state.integrations = result.choice;
3335
+ state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
3336
+ if (result.lefthookInstall) {
3337
+ state.deferredInstalls.push(result.lefthookInstall);
2951
3338
  }
2952
- return filtered;
2953
3339
  }
2954
3340
 
2955
3341
  // src/utils/update-gitignore.ts
2956
- var fs15 = __toESM(require("fs"), 1);
2957
- var path15 = __toESM(require("path"), 1);
3342
+ var fs16 = __toESM(require("fs"), 1);
3343
+ var path16 = __toESM(require("path"), 1);
2958
3344
  function updateGitignore(projectRoot) {
2959
- const gitignorePath = path15.join(projectRoot, ".gitignore");
3345
+ const gitignorePath = path16.join(projectRoot, ".gitignore");
2960
3346
  let content = "";
2961
- if (fs15.existsSync(gitignorePath)) {
2962
- content = fs15.readFileSync(gitignorePath, "utf-8");
3347
+ if (fs16.existsSync(gitignorePath)) {
3348
+ content = fs16.readFileSync(gitignorePath, "utf-8");
2963
3349
  }
2964
3350
  if (!content.includes(".viberails/scan-result.json")) {
2965
3351
  const block = "\n# viberails\n.viberails/scan-result.json\n";
2966
3352
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
2967
3353
  `;
2968
- fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
3354
+ fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
2969
3355
  }
2970
3356
  }
2971
3357
 
2972
3358
  // src/commands/init-hooks.ts
2973
- var fs17 = __toESM(require("fs"), 1);
2974
- var path17 = __toESM(require("path"), 1);
3359
+ var fs18 = __toESM(require("fs"), 1);
3360
+ var path18 = __toESM(require("path"), 1);
2975
3361
  var import_chalk10 = __toESM(require("chalk"), 1);
2976
3362
  var import_yaml = require("yaml");
2977
3363
 
2978
3364
  // src/commands/resolve-typecheck.ts
2979
- var fs16 = __toESM(require("fs"), 1);
2980
- var path16 = __toESM(require("path"), 1);
3365
+ var fs17 = __toESM(require("fs"), 1);
3366
+ var path17 = __toESM(require("path"), 1);
2981
3367
  function hasTurboTask(projectRoot, taskName) {
2982
- const turboPath = path16.join(projectRoot, "turbo.json");
2983
- if (!fs16.existsSync(turboPath)) return false;
3368
+ const turboPath = path17.join(projectRoot, "turbo.json");
3369
+ if (!fs17.existsSync(turboPath)) return false;
2984
3370
  try {
2985
- const turbo = JSON.parse(fs16.readFileSync(turboPath, "utf-8"));
3371
+ const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
2986
3372
  const tasks = turbo.tasks ?? turbo.pipeline ?? {};
2987
3373
  return taskName in tasks;
2988
3374
  } catch {
@@ -2993,10 +3379,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
2993
3379
  if (hasTurboTask(projectRoot, "typecheck")) {
2994
3380
  return { command: "npx turbo typecheck", label: "turbo typecheck" };
2995
3381
  }
2996
- const pkgJsonPath = path16.join(projectRoot, "package.json");
2997
- if (fs16.existsSync(pkgJsonPath)) {
3382
+ const pkgJsonPath = path17.join(projectRoot, "package.json");
3383
+ if (fs17.existsSync(pkgJsonPath)) {
2998
3384
  try {
2999
- const pkg = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
3385
+ const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
3000
3386
  if (pkg.scripts?.typecheck) {
3001
3387
  const pm = packageManager ?? "npm";
3002
3388
  return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
@@ -3004,7 +3390,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3004
3390
  } catch {
3005
3391
  }
3006
3392
  }
3007
- if (fs16.existsSync(path16.join(projectRoot, "tsconfig.json"))) {
3393
+ if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
3008
3394
  return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
3009
3395
  }
3010
3396
  return {
@@ -3014,23 +3400,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3014
3400
 
3015
3401
  // src/commands/init-hooks.ts
3016
3402
  function setupPreCommitHook(projectRoot) {
3017
- const lefthookPath = path17.join(projectRoot, "lefthook.yml");
3018
- if (fs17.existsSync(lefthookPath)) {
3403
+ const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3404
+ if (fs18.existsSync(lefthookPath)) {
3019
3405
  addLefthookPreCommit(lefthookPath);
3020
3406
  console.log(` ${import_chalk10.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3021
3407
  return "lefthook.yml";
3022
3408
  }
3023
- const huskyDir = path17.join(projectRoot, ".husky");
3024
- if (fs17.existsSync(huskyDir)) {
3409
+ const huskyDir = path18.join(projectRoot, ".husky");
3410
+ if (fs18.existsSync(huskyDir)) {
3025
3411
  writeHuskyPreCommit(huskyDir);
3026
3412
  console.log(` ${import_chalk10.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3027
3413
  return ".husky/pre-commit";
3028
3414
  }
3029
- const gitDir = path17.join(projectRoot, ".git");
3030
- if (fs17.existsSync(gitDir)) {
3031
- const hooksDir = path17.join(gitDir, "hooks");
3032
- if (!fs17.existsSync(hooksDir)) {
3033
- fs17.mkdirSync(hooksDir, { recursive: true });
3415
+ const gitDir = path18.join(projectRoot, ".git");
3416
+ if (fs18.existsSync(gitDir)) {
3417
+ const hooksDir = path18.join(gitDir, "hooks");
3418
+ if (!fs18.existsSync(hooksDir)) {
3419
+ fs18.mkdirSync(hooksDir, { recursive: true });
3034
3420
  }
3035
3421
  writeGitHookPreCommit(hooksDir);
3036
3422
  console.log(` ${import_chalk10.default.green("\u2713")} .git/hooks/pre-commit`);
@@ -3039,11 +3425,11 @@ function setupPreCommitHook(projectRoot) {
3039
3425
  return void 0;
3040
3426
  }
3041
3427
  function writeGitHookPreCommit(hooksDir) {
3042
- const hookPath = path17.join(hooksDir, "pre-commit");
3043
- if (fs17.existsSync(hookPath)) {
3044
- const existing = fs17.readFileSync(hookPath, "utf-8");
3428
+ const hookPath = path18.join(hooksDir, "pre-commit");
3429
+ if (fs18.existsSync(hookPath)) {
3430
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3045
3431
  if (existing.includes("viberails")) return;
3046
- fs17.writeFileSync(
3432
+ fs18.writeFileSync(
3047
3433
  hookPath,
3048
3434
  `${existing.trimEnd()}
3049
3435
 
@@ -3060,10 +3446,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
3060
3446
  "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
3061
3447
  ""
3062
3448
  ].join("\n");
3063
- fs17.writeFileSync(hookPath, script, { mode: 493 });
3449
+ fs18.writeFileSync(hookPath, script, { mode: 493 });
3064
3450
  }
3065
3451
  function addLefthookPreCommit(lefthookPath) {
3066
- const content = fs17.readFileSync(lefthookPath, "utf-8");
3452
+ const content = fs18.readFileSync(lefthookPath, "utf-8");
3067
3453
  if (content.includes("viberails")) return;
3068
3454
  const doc = (0, import_yaml.parse)(content) ?? {};
3069
3455
  if (!doc["pre-commit"]) {
@@ -3075,23 +3461,23 @@ function addLefthookPreCommit(lefthookPath) {
3075
3461
  doc["pre-commit"].commands.viberails = {
3076
3462
  run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
3077
3463
  };
3078
- fs17.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3464
+ fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3079
3465
  }
3080
3466
  function detectHookManager(projectRoot) {
3081
- if (fs17.existsSync(path17.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3082
- if (fs17.existsSync(path17.join(projectRoot, ".husky"))) return "Husky";
3467
+ if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3468
+ if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
3083
3469
  return void 0;
3084
3470
  }
3085
3471
  function setupClaudeCodeHook(projectRoot) {
3086
- const claudeDir = path17.join(projectRoot, ".claude");
3087
- if (!fs17.existsSync(claudeDir)) {
3088
- fs17.mkdirSync(claudeDir, { recursive: true });
3472
+ const claudeDir = path18.join(projectRoot, ".claude");
3473
+ if (!fs18.existsSync(claudeDir)) {
3474
+ fs18.mkdirSync(claudeDir, { recursive: true });
3089
3475
  }
3090
- const settingsPath = path17.join(claudeDir, "settings.json");
3476
+ const settingsPath = path18.join(claudeDir, "settings.json");
3091
3477
  let settings = {};
3092
- if (fs17.existsSync(settingsPath)) {
3478
+ if (fs18.existsSync(settingsPath)) {
3093
3479
  try {
3094
- settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
3480
+ settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
3095
3481
  } catch {
3096
3482
  console.warn(
3097
3483
  ` ${import_chalk10.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
@@ -3117,30 +3503,30 @@ function setupClaudeCodeHook(projectRoot) {
3117
3503
  }
3118
3504
  ];
3119
3505
  settings.hooks = hooks;
3120
- fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3506
+ fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3121
3507
  `);
3122
3508
  console.log(` ${import_chalk10.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3123
3509
  }
3124
3510
  function setupClaudeMdReference(projectRoot) {
3125
- const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
3511
+ const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
3126
3512
  let content = "";
3127
- if (fs17.existsSync(claudeMdPath)) {
3128
- content = fs17.readFileSync(claudeMdPath, "utf-8");
3513
+ if (fs18.existsSync(claudeMdPath)) {
3514
+ content = fs18.readFileSync(claudeMdPath, "utf-8");
3129
3515
  }
3130
3516
  if (content.includes("@.viberails/context.md")) return;
3131
3517
  const ref = "\n@.viberails/context.md\n";
3132
3518
  const prefix = content.length === 0 ? "" : content.trimEnd();
3133
- fs17.writeFileSync(claudeMdPath, prefix + ref);
3519
+ fs18.writeFileSync(claudeMdPath, prefix + ref);
3134
3520
  console.log(` ${import_chalk10.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3135
3521
  }
3136
3522
  function setupGithubAction(projectRoot, packageManager, options) {
3137
- const workflowDir = path17.join(projectRoot, ".github", "workflows");
3138
- const workflowPath = path17.join(workflowDir, "viberails.yml");
3139
- if (fs17.existsSync(workflowPath)) {
3140
- const existing = fs17.readFileSync(workflowPath, "utf-8");
3523
+ const workflowDir = path18.join(projectRoot, ".github", "workflows");
3524
+ const workflowPath = path18.join(workflowDir, "viberails.yml");
3525
+ if (fs18.existsSync(workflowPath)) {
3526
+ const existing = fs18.readFileSync(workflowPath, "utf-8");
3141
3527
  if (existing.includes("viberails")) return void 0;
3142
3528
  }
3143
- fs17.mkdirSync(workflowDir, { recursive: true });
3529
+ fs18.mkdirSync(workflowDir, { recursive: true });
3144
3530
  const pm = packageManager || "npm";
3145
3531
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
3146
3532
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -3194,74 +3580,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
3194
3580
  ""
3195
3581
  );
3196
3582
  const content = lines.filter((l) => l !== void 0).join("\n");
3197
- fs17.writeFileSync(workflowPath, content);
3583
+ fs18.writeFileSync(workflowPath, content);
3198
3584
  return ".github/workflows/viberails.yml";
3199
3585
  }
3200
3586
  function writeHuskyPreCommit(huskyDir) {
3201
- const hookPath = path17.join(huskyDir, "pre-commit");
3587
+ const hookPath = path18.join(huskyDir, "pre-commit");
3202
3588
  const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
3203
- if (fs17.existsSync(hookPath)) {
3204
- const existing = fs17.readFileSync(hookPath, "utf-8");
3589
+ if (fs18.existsSync(hookPath)) {
3590
+ const existing = fs18.readFileSync(hookPath, "utf-8");
3205
3591
  if (!existing.includes("viberails")) {
3206
- fs17.writeFileSync(hookPath, `${existing.trimEnd()}
3592
+ fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3207
3593
  ${cmd}
3208
3594
  `);
3209
3595
  }
3210
3596
  return;
3211
3597
  }
3212
- fs17.writeFileSync(hookPath, `#!/bin/sh
3598
+ fs18.writeFileSync(hookPath, `#!/bin/sh
3213
3599
  ${cmd}
3214
3600
  `, { mode: 493 });
3215
3601
  }
3216
3602
 
3217
3603
  // src/commands/init-hooks-extra.ts
3218
- var fs18 = __toESM(require("fs"), 1);
3219
- var path18 = __toESM(require("path"), 1);
3604
+ var fs19 = __toESM(require("fs"), 1);
3605
+ var path19 = __toESM(require("path"), 1);
3220
3606
  var import_chalk11 = __toESM(require("chalk"), 1);
3221
3607
  var import_yaml2 = require("yaml");
3222
3608
  function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
3223
- const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3224
- if (fs18.existsSync(lefthookPath)) {
3225
- const content = fs18.readFileSync(lefthookPath, "utf-8");
3609
+ const lefthookPath = path19.join(projectRoot, "lefthook.yml");
3610
+ if (fs19.existsSync(lefthookPath)) {
3611
+ const content = fs19.readFileSync(lefthookPath, "utf-8");
3226
3612
  if (content.includes(marker)) return void 0;
3227
3613
  const doc = (0, import_yaml2.parse)(content) ?? {};
3228
3614
  if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
3229
3615
  if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
3230
3616
  doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
3231
- fs18.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3617
+ fs19.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
3232
3618
  return "lefthook.yml";
3233
3619
  }
3234
- const huskyDir = path18.join(projectRoot, ".husky");
3235
- if (fs18.existsSync(huskyDir)) {
3236
- const hookPath = path18.join(huskyDir, "pre-commit");
3237
- if (fs18.existsSync(hookPath)) {
3238
- const existing = fs18.readFileSync(hookPath, "utf-8");
3620
+ const huskyDir = path19.join(projectRoot, ".husky");
3621
+ if (fs19.existsSync(huskyDir)) {
3622
+ const hookPath = path19.join(huskyDir, "pre-commit");
3623
+ if (fs19.existsSync(hookPath)) {
3624
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3239
3625
  if (existing.includes(marker)) return void 0;
3240
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3626
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3241
3627
  ${command}
3242
3628
  `);
3243
3629
  } else {
3244
- fs18.writeFileSync(hookPath, `#!/bin/sh
3630
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3245
3631
  ${command}
3246
3632
  `, { mode: 493 });
3247
3633
  }
3248
3634
  return ".husky/pre-commit";
3249
3635
  }
3250
- const gitDir = path18.join(projectRoot, ".git");
3251
- if (fs18.existsSync(gitDir)) {
3252
- const hooksDir = path18.join(gitDir, "hooks");
3253
- if (!fs18.existsSync(hooksDir)) fs18.mkdirSync(hooksDir, { recursive: true });
3254
- const hookPath = path18.join(hooksDir, "pre-commit");
3255
- if (fs18.existsSync(hookPath)) {
3256
- const existing = fs18.readFileSync(hookPath, "utf-8");
3636
+ const gitDir = path19.join(projectRoot, ".git");
3637
+ if (fs19.existsSync(gitDir)) {
3638
+ const hooksDir = path19.join(gitDir, "hooks");
3639
+ if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
3640
+ const hookPath = path19.join(hooksDir, "pre-commit");
3641
+ if (fs19.existsSync(hookPath)) {
3642
+ const existing = fs19.readFileSync(hookPath, "utf-8");
3257
3643
  if (existing.includes(marker)) return void 0;
3258
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3644
+ fs19.writeFileSync(hookPath, `${existing.trimEnd()}
3259
3645
 
3260
3646
  # ${name}
3261
3647
  ${command}
3262
3648
  `);
3263
3649
  } else {
3264
- fs18.writeFileSync(hookPath, `#!/bin/sh
3650
+ fs19.writeFileSync(hookPath, `#!/bin/sh
3265
3651
  # Generated by viberails
3266
3652
 
3267
3653
  # ${name}
@@ -3287,7 +3673,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
3287
3673
  return target;
3288
3674
  }
3289
3675
  function setupLintHook(projectRoot, linter) {
3290
- const isLefthook = fs18.existsSync(path18.join(projectRoot, "lefthook.yml"));
3676
+ const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
3291
3677
  const linterName = linter === "biome" ? "Biome" : "ESLint";
3292
3678
  let command;
3293
3679
  let lefthookExtra;
@@ -3311,6 +3697,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3311
3697
  const created = [];
3312
3698
  if (integrations.preCommitHook) {
3313
3699
  const t = setupPreCommitHook(projectRoot);
3700
+ if (t && opts.lefthookExpected && !t.includes("lefthook")) {
3701
+ console.log(` ${import_chalk11.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3702
+ }
3314
3703
  created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
3315
3704
  }
3316
3705
  if (integrations.typecheckHook) {
@@ -3339,34 +3728,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3339
3728
  return created;
3340
3729
  }
3341
3730
 
3342
- // src/commands/init.ts
3343
- var CONFIG_FILE5 = "viberails.config.json";
3344
- function getExemptedPackages(config) {
3345
- return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3346
- }
3347
- async function initCommand(options, cwd) {
3348
- const projectRoot = findProjectRoot(cwd ?? process.cwd());
3349
- if (!projectRoot) {
3350
- throw new Error(
3351
- "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
3352
- );
3353
- }
3354
- const configPath = path19.join(projectRoot, CONFIG_FILE5);
3355
- if (fs19.existsSync(configPath) && !options.force) {
3356
- if (!options.yes) {
3357
- return initInteractive(projectRoot, configPath, options);
3731
+ // src/commands/init-non-interactive.ts
3732
+ var fs20 = __toESM(require("fs"), 1);
3733
+ var path20 = __toESM(require("path"), 1);
3734
+ var clack11 = __toESM(require("@clack/prompts"), 1);
3735
+ var import_config8 = require("@viberails/config");
3736
+ var import_scanner2 = require("@viberails/scanner");
3737
+ var import_chalk12 = __toESM(require("chalk"), 1);
3738
+
3739
+ // src/utils/filter-confidence.ts
3740
+ function filterHighConfidence(conventions, meta) {
3741
+ if (!meta) return conventions;
3742
+ const filtered = {};
3743
+ for (const [key, value] of Object.entries(conventions)) {
3744
+ if (value === void 0) continue;
3745
+ const convMeta = meta[key];
3746
+ if (!convMeta || convMeta.confidence === "high") {
3747
+ filtered[key] = value;
3358
3748
  }
3359
- console.log(
3360
- `${import_chalk12.default.yellow("!")} viberails is already initialized.
3361
- Run ${import_chalk12.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk12.default.cyan("viberails sync")} to update generated files, or ${import_chalk12.default.cyan("viberails init --force")} to replace it.`
3362
- );
3363
- return;
3364
3749
  }
3365
- if (options.yes) return initNonInteractive(projectRoot, configPath);
3366
- await initInteractive(projectRoot, configPath, options);
3750
+ return filtered;
3751
+ }
3752
+
3753
+ // src/commands/init-non-interactive.ts
3754
+ function getExemptedPackages(config) {
3755
+ return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3367
3756
  }
3368
3757
  async function initNonInteractive(projectRoot, configPath) {
3369
- const s = clack8.spinner();
3758
+ const s = clack11.spinner();
3370
3759
  s.start("Scanning project...");
3371
3760
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
3372
3761
  const config = (0, import_config8.generateConfig)(scanResult);
@@ -3385,7 +3774,7 @@ async function initNonInteractive(projectRoot, configPath) {
3385
3774
  );
3386
3775
  }
3387
3776
  if (config.packages.length > 1) {
3388
- const bs = clack8.spinner();
3777
+ const bs = clack11.spinner();
3389
3778
  bs.start("Building import graph...");
3390
3779
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3391
3780
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3401,7 +3790,7 @@ async function initNonInteractive(projectRoot, configPath) {
3401
3790
  }
3402
3791
  }
3403
3792
  const compacted = (0, import_config8.compactConfig)(config);
3404
- fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3793
+ fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3405
3794
  `);
3406
3795
  writeGeneratedFiles(projectRoot, config, scanResult);
3407
3796
  updateGitignore(projectRoot);
@@ -3420,7 +3809,7 @@ async function initNonInteractive(projectRoot, configPath) {
3420
3809
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3421
3810
  const ok = import_chalk12.default.green("\u2713");
3422
3811
  const created = [
3423
- `${ok} ${path19.basename(configPath)}`,
3812
+ `${ok} ${path20.basename(configPath)}`,
3424
3813
  `${ok} .viberails/context.md`,
3425
3814
  `${ok} .viberails/scan-result.json`,
3426
3815
  `${ok} .claude/settings.json \u2014 added viberails hook`,
@@ -3434,13 +3823,36 @@ async function initNonInteractive(projectRoot, configPath) {
3434
3823
  Created:
3435
3824
  ${created.map((f) => ` ${f}`).join("\n")}`);
3436
3825
  }
3826
+
3827
+ // src/commands/init.ts
3828
+ var CONFIG_FILE5 = "viberails.config.json";
3829
+ async function initCommand(options, cwd) {
3830
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
3831
+ if (!projectRoot) {
3832
+ throw new Error(
3833
+ "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
3834
+ );
3835
+ }
3836
+ const configPath = path21.join(projectRoot, CONFIG_FILE5);
3837
+ if (fs21.existsSync(configPath) && !options.force) {
3838
+ if (!options.yes) {
3839
+ return initInteractive(projectRoot, configPath, options);
3840
+ }
3841
+ console.log(
3842
+ `${import_chalk13.default.yellow("!")} viberails is already initialized.
3843
+ Run ${import_chalk13.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk13.default.cyan("viberails sync")} to update generated files, or ${import_chalk13.default.cyan("viberails init --force")} to replace it.`
3844
+ );
3845
+ return;
3846
+ }
3847
+ if (options.yes) return initNonInteractive(projectRoot, configPath);
3848
+ await initInteractive(projectRoot, configPath, options);
3849
+ }
3437
3850
  async function initInteractive(projectRoot, configPath, options) {
3438
- clack8.intro("viberails");
3439
- const replacingExistingConfig = fs19.existsSync(configPath);
3440
- if (fs19.existsSync(configPath) && !options.force) {
3441
- const action = await promptExistingConfigAction(path19.basename(configPath));
3851
+ clack12.intro("viberails");
3852
+ if (fs21.existsSync(configPath) && !options.force) {
3853
+ const action = await promptExistingConfigAction(path21.basename(configPath));
3442
3854
  if (action === "cancel") {
3443
- clack8.outro("Aborted. No files were written.");
3855
+ clack12.outro("Aborted. No files were written.");
3444
3856
  return;
3445
3857
  }
3446
3858
  if (action === "edit") {
@@ -3449,136 +3861,93 @@ async function initInteractive(projectRoot, configPath, options) {
3449
3861
  }
3450
3862
  options.force = true;
3451
3863
  }
3452
- if (fs19.existsSync(configPath) && options.force) {
3864
+ if (fs21.existsSync(configPath) && options.force) {
3453
3865
  const replace = await confirmDangerous(
3454
- `${path19.basename(configPath)} already exists and will be replaced. Continue?`
3866
+ `${path21.basename(configPath)} already exists and will be replaced. Continue?`
3455
3867
  );
3456
3868
  if (!replace) {
3457
- clack8.outro("Aborted. No files were written.");
3869
+ clack12.outro("Aborted. No files were written.");
3458
3870
  return;
3459
3871
  }
3460
3872
  }
3461
- const s = clack8.spinner();
3873
+ const s = clack12.spinner();
3462
3874
  s.start("Scanning project...");
3463
- const scanResult = await (0, import_scanner2.scan)(projectRoot);
3464
- const config = (0, import_config8.generateConfig)(scanResult);
3875
+ const scanResult = await (0, import_scanner3.scan)(projectRoot);
3876
+ const config = (0, import_config9.generateConfig)(scanResult);
3465
3877
  s.stop("Scan complete");
3466
3878
  if (scanResult.statistics.totalFiles === 0) {
3467
- clack8.log.warn(
3879
+ clack12.log.warn(
3468
3880
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3469
3881
  );
3470
3882
  }
3471
- const exemptedPkgs = getExemptedPackages(config);
3472
- let decision;
3473
- while (true) {
3474
- displayInitOverview(scanResult, config, exemptedPkgs);
3475
- const nextDecision = await promptInitDecision();
3476
- if (nextDecision === "review") {
3477
- clack8.note(formatScanResultsText(scanResult), "Detected details");
3478
- continue;
3479
- }
3480
- decision = nextDecision;
3481
- break;
3482
- }
3483
- if (decision === "customize") {
3484
- const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
3485
- const overrides = await promptRuleMenu({
3486
- maxFileLines: config.rules.maxFileLines,
3487
- testCoverage: config.rules.testCoverage,
3488
- enforceMissingTests: config.rules.enforceMissingTests,
3489
- enforceNaming: config.rules.enforceNaming,
3490
- fileNamingValue: rootPkg.conventions?.fileNaming,
3491
- coverageSummaryPath: "coverage/coverage-summary.json",
3492
- coverageCommand: config.defaults?.coverage?.command,
3493
- packageOverrides: config.packages
3494
- });
3495
- applyRuleOverrides(config, overrides);
3496
- }
3497
- if (config.packages.length > 1) {
3498
- clack8.note(
3499
- "Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
3500
- "Boundaries"
3883
+ const hasTestRunner = !!scanResult.stack.testRunner;
3884
+ if (!hasTestRunner) {
3885
+ clack12.log.info(
3886
+ "No test runner detected. Coverage checks are inactive until a test runner is installed.\nInstall a test runner (e.g. vitest) and re-run viberails init."
3501
3887
  );
3502
- const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
3503
- if (shouldInfer) {
3504
- const bs = clack8.spinner();
3505
- bs.start("Building import graph...");
3506
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3507
- const packages = resolveWorkspacePackages(projectRoot, config.packages);
3508
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
3509
- const inferred = inferBoundaries(graph);
3510
- const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
3511
- if (denyCount > 0) {
3512
- config.boundaries = inferred;
3513
- config.rules.enforceBoundaries = true;
3514
- const pkgCount = Object.keys(inferred.deny).length;
3515
- bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3516
- } else {
3517
- bs.stop("No boundary rules inferred");
3518
- }
3519
- }
3520
3888
  }
3521
3889
  const hookManager = detectHookManager(projectRoot);
3522
3890
  const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
3523
- const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
3524
- if (hasMissingPrereqs) {
3525
- clack8.log.info("Some dependencies are needed for full functionality.");
3526
- }
3527
- const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
3528
- if (prereqResult.disableCoverage) {
3529
- config.rules.testCoverage = 0;
3530
- }
3531
3891
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
3532
- const integrations = await promptIntegrations(projectRoot, hookManager, {
3533
- isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
3534
- linter: rootPkgStack?.linter?.split("@")[0],
3535
- packageManager: rootPkgStack?.packageManager?.split("@")[0],
3536
- isWorkspace: config.packages.length > 1
3537
- });
3538
- displaySetupPlan(config, integrations, {
3539
- replacingExistingConfig,
3540
- configFile: path19.basename(configPath)
3892
+ const state = await promptMainMenu(config, scanResult, {
3893
+ hasTestRunner,
3894
+ hookManager,
3895
+ coveragePrereqs,
3896
+ projectRoot,
3897
+ tools: {
3898
+ isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
3899
+ linter: rootPkgStack?.linter?.split("@")[0],
3900
+ packageManager: rootPkgStack?.packageManager?.split("@")[0],
3901
+ isWorkspace: config.packages.length > 1
3902
+ }
3541
3903
  });
3542
3904
  const shouldWrite = await confirm3("Apply this setup?");
3543
3905
  if (!shouldWrite) {
3544
- clack8.outro("Aborted. No files were written.");
3906
+ clack12.outro("Aborted. No files were written.");
3545
3907
  return;
3546
3908
  }
3547
- const ws = clack8.spinner();
3909
+ if (state.deferredInstalls.length > 0) {
3910
+ await executeDeferredInstalls(projectRoot, state.deferredInstalls);
3911
+ }
3912
+ const ws = clack12.spinner();
3548
3913
  ws.start("Writing configuration...");
3549
- const compacted = (0, import_config8.compactConfig)(config);
3550
- fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3914
+ const compacted = (0, import_config9.compactConfig)(config);
3915
+ fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3551
3916
  `);
3552
3917
  writeGeneratedFiles(projectRoot, config, scanResult);
3553
3918
  updateGitignore(projectRoot);
3554
3919
  ws.stop("Configuration written");
3555
- const ok = import_chalk12.default.green("\u2713");
3556
- clack8.log.step(`${ok} ${path19.basename(configPath)}`);
3557
- clack8.log.step(`${ok} .viberails/context.md`);
3558
- clack8.log.step(`${ok} .viberails/scan-result.json`);
3559
- setupSelectedIntegrations(projectRoot, integrations, {
3560
- linter: rootPkgStack?.linter?.split("@")[0],
3561
- packageManager: rootPkgStack?.packageManager?.split("@")[0]
3562
- });
3563
- clack8.outro(
3920
+ const ok = import_chalk13.default.green("\u2713");
3921
+ clack12.log.step(`${ok} ${path21.basename(configPath)}`);
3922
+ clack12.log.step(`${ok} .viberails/context.md`);
3923
+ clack12.log.step(`${ok} .viberails/scan-result.json`);
3924
+ if (state.visited.integrations && state.integrations) {
3925
+ const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
3926
+ setupSelectedIntegrations(projectRoot, state.integrations, {
3927
+ linter: rootPkgStack?.linter?.split("@")[0],
3928
+ packageManager: rootPkgStack?.packageManager?.split("@")[0],
3929
+ lefthookExpected
3930
+ });
3931
+ }
3932
+ clack12.outro(
3564
3933
  `Done! Next: review viberails.config.json, then run viberails check
3565
- ${import_chalk12.default.dim("Tip: use")} ${import_chalk12.default.cyan("viberails check --enforce")} ${import_chalk12.default.dim("in CI to block PRs on violations.")}`
3934
+ ${import_chalk13.default.dim("Tip: use")} ${import_chalk13.default.cyan("viberails check --enforce")} ${import_chalk13.default.dim("in CI to block PRs on violations.")}`
3566
3935
  );
3567
3936
  }
3568
3937
 
3569
3938
  // src/commands/sync.ts
3570
- var fs20 = __toESM(require("fs"), 1);
3571
- var path20 = __toESM(require("path"), 1);
3572
- var clack9 = __toESM(require("@clack/prompts"), 1);
3573
- var import_config10 = require("@viberails/config");
3574
- var import_scanner3 = require("@viberails/scanner");
3575
- var import_chalk13 = __toESM(require("chalk"), 1);
3939
+ var fs22 = __toESM(require("fs"), 1);
3940
+ var path22 = __toESM(require("path"), 1);
3941
+ var clack13 = __toESM(require("@clack/prompts"), 1);
3942
+ var import_config11 = require("@viberails/config");
3943
+ var import_scanner4 = require("@viberails/scanner");
3944
+ var import_chalk14 = __toESM(require("chalk"), 1);
3576
3945
  var CONFIG_FILE6 = "viberails.config.json";
3577
3946
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
3578
3947
  function loadPreviousStats(projectRoot) {
3579
- const scanResultPath = path20.join(projectRoot, SCAN_RESULT_FILE2);
3948
+ const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
3580
3949
  try {
3581
- const raw = fs20.readFileSync(scanResultPath, "utf-8");
3950
+ const raw = fs22.readFileSync(scanResultPath, "utf-8");
3582
3951
  const parsed = JSON.parse(raw);
3583
3952
  if (parsed?.statistics?.totalFiles !== void 0) {
3584
3953
  return parsed.statistics;
@@ -3595,17 +3964,17 @@ async function syncCommand(options, cwd) {
3595
3964
  "No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
3596
3965
  );
3597
3966
  }
3598
- const configPath = path20.join(projectRoot, CONFIG_FILE6);
3599
- const existing = await (0, import_config10.loadConfig)(configPath);
3967
+ const configPath = path22.join(projectRoot, CONFIG_FILE6);
3968
+ const existing = await (0, import_config11.loadConfig)(configPath);
3600
3969
  const previousStats = loadPreviousStats(projectRoot);
3601
- const s = clack9.spinner();
3970
+ const s = clack13.spinner();
3602
3971
  s.start("Scanning project...");
3603
- const scanResult = await (0, import_scanner3.scan)(projectRoot);
3972
+ const scanResult = await (0, import_scanner4.scan)(projectRoot);
3604
3973
  s.stop("Scan complete");
3605
- const merged = (0, import_config10.mergeConfig)(existing, scanResult);
3606
- const compacted = (0, import_config10.compactConfig)(merged);
3974
+ const merged = (0, import_config11.mergeConfig)(existing, scanResult);
3975
+ const compacted = (0, import_config11.compactConfig)(merged);
3607
3976
  const compactedJson = JSON.stringify(compacted, null, 2);
3608
- const rawDisk = fs20.readFileSync(configPath, "utf-8").trim();
3977
+ const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
3609
3978
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3610
3979
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3611
3980
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -3613,19 +3982,19 @@ async function syncCommand(options, cwd) {
3613
3982
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
3614
3983
  if (changes.length > 0 || statsDelta) {
3615
3984
  console.log(`
3616
- ${import_chalk13.default.bold("Changes:")}`);
3985
+ ${import_chalk14.default.bold("Changes:")}`);
3617
3986
  for (const change of changes) {
3618
- const icon = change.type === "removed" ? import_chalk13.default.red("-") : import_chalk13.default.green("+");
3987
+ const icon = change.type === "removed" ? import_chalk14.default.red("-") : import_chalk14.default.green("+");
3619
3988
  console.log(` ${icon} ${change.description}`);
3620
3989
  }
3621
3990
  if (statsDelta) {
3622
- console.log(` ${import_chalk13.default.dim(statsDelta)}`);
3991
+ console.log(` ${import_chalk14.default.dim(statsDelta)}`);
3623
3992
  }
3624
3993
  }
3625
3994
  if (options?.interactive) {
3626
- clack9.intro("viberails sync (interactive)");
3627
- clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
3628
- const decision = await clack9.select({
3995
+ clack13.intro("viberails sync (interactive)");
3996
+ clack13.note(formatRulesText(merged).join("\n"), "Rules after sync");
3997
+ const decision = await clack13.select({
3629
3998
  message: "How would you like to proceed?",
3630
3999
  options: [
3631
4000
  { value: "accept", label: "Accept changes" },
@@ -3635,47 +4004,51 @@ ${import_chalk13.default.bold("Changes:")}`);
3635
4004
  });
3636
4005
  assertNotCancelled(decision);
3637
4006
  if (decision === "cancel") {
3638
- clack9.outro("Sync cancelled. No files were written.");
4007
+ clack13.outro("Sync cancelled. No files were written.");
3639
4008
  return;
3640
4009
  }
3641
4010
  if (decision === "customize") {
3642
4011
  const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
3643
4012
  const overrides = await promptRuleMenu({
3644
4013
  maxFileLines: merged.rules.maxFileLines,
4014
+ maxTestFileLines: merged.rules.maxTestFileLines,
3645
4015
  testCoverage: merged.rules.testCoverage,
3646
4016
  enforceMissingTests: merged.rules.enforceMissingTests,
3647
4017
  enforceNaming: merged.rules.enforceNaming,
3648
4018
  fileNamingValue: rootPkg.conventions?.fileNaming,
4019
+ componentNaming: rootPkg.conventions?.componentNaming,
4020
+ hookNaming: rootPkg.conventions?.hookNaming,
4021
+ importAlias: rootPkg.conventions?.importAlias,
3649
4022
  coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3650
4023
  coverageCommand: merged.defaults?.coverage?.command,
3651
4024
  packageOverrides: merged.packages
3652
4025
  });
3653
4026
  applyRuleOverrides(merged, overrides);
3654
- const recompacted = (0, import_config10.compactConfig)(merged);
3655
- fs20.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4027
+ const recompacted = (0, import_config11.compactConfig)(merged);
4028
+ fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3656
4029
  `);
3657
4030
  writeGeneratedFiles(projectRoot, merged, scanResult);
3658
- clack9.log.success("Updated config with your customizations.");
3659
- clack9.outro("Done! Run viberails check to verify.");
4031
+ clack13.log.success("Updated config with your customizations.");
4032
+ clack13.outro("Done! Run viberails check to verify.");
3660
4033
  return;
3661
4034
  }
3662
4035
  }
3663
- fs20.writeFileSync(configPath, `${compactedJson}
4036
+ fs22.writeFileSync(configPath, `${compactedJson}
3664
4037
  `);
3665
4038
  writeGeneratedFiles(projectRoot, merged, scanResult);
3666
4039
  console.log(`
3667
- ${import_chalk13.default.bold("Synced:")}`);
4040
+ ${import_chalk14.default.bold("Synced:")}`);
3668
4041
  if (configChanged) {
3669
- console.log(` ${import_chalk13.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4042
+ console.log(` ${import_chalk14.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
3670
4043
  } else {
3671
- console.log(` ${import_chalk13.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4044
+ console.log(` ${import_chalk14.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
3672
4045
  }
3673
- console.log(` ${import_chalk13.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3674
- console.log(` ${import_chalk13.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
4046
+ console.log(` ${import_chalk14.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4047
+ console.log(` ${import_chalk14.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3675
4048
  }
3676
4049
 
3677
4050
  // src/index.ts
3678
- var VERSION = "0.6.4";
4051
+ var VERSION = "0.6.6";
3679
4052
  var program = new import_commander.Command();
3680
4053
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
3681
4054
  program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
@@ -3683,7 +4056,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
3683
4056
  await initCommand(options);
3684
4057
  } catch (err) {
3685
4058
  const message = err instanceof Error ? err.message : String(err);
3686
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4059
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3687
4060
  process.exit(1);
3688
4061
  }
3689
4062
  });
@@ -3692,7 +4065,7 @@ program.command("sync").description("Re-scan and update generated files").option
3692
4065
  await syncCommand(options);
3693
4066
  } catch (err) {
3694
4067
  const message = err instanceof Error ? err.message : String(err);
3695
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4068
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3696
4069
  process.exit(1);
3697
4070
  }
3698
4071
  });
@@ -3701,7 +4074,7 @@ program.command("config").description("Interactively edit existing config rules"
3701
4074
  await configCommand(options);
3702
4075
  } catch (err) {
3703
4076
  const message = err instanceof Error ? err.message : String(err);
3704
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4077
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3705
4078
  process.exit(1);
3706
4079
  }
3707
4080
  });
@@ -3722,7 +4095,7 @@ program.command("check").description("Check files against enforced rules").optio
3722
4095
  process.exit(exitCode);
3723
4096
  } catch (err) {
3724
4097
  const message = err instanceof Error ? err.message : String(err);
3725
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4098
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3726
4099
  process.exit(1);
3727
4100
  }
3728
4101
  }
@@ -3733,7 +4106,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
3733
4106
  process.exit(exitCode);
3734
4107
  } catch (err) {
3735
4108
  const message = err instanceof Error ? err.message : String(err);
3736
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4109
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3737
4110
  process.exit(1);
3738
4111
  }
3739
4112
  });
@@ -3742,7 +4115,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
3742
4115
  await boundariesCommand(options);
3743
4116
  } catch (err) {
3744
4117
  const message = err instanceof Error ? err.message : String(err);
3745
- console.error(`${import_chalk14.default.red("Error:")} ${message}`);
4118
+ console.error(`${import_chalk15.default.red("Error:")} ${message}`);
3746
4119
  process.exit(1);
3747
4120
  }
3748
4121
  });