viberails 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ assertNotCancelled,
4
+ confirm,
5
+ confirmDangerous,
6
+ getRootPackage,
7
+ promptExistingConfigAction,
8
+ promptInitDecision,
9
+ promptIntegrations,
10
+ promptRuleMenu,
11
+ spawnAsync
12
+ } from "./chunk-XQKOK3FU.js";
2
13
 
3
14
  // src/index.ts
4
- import chalk14 from "chalk";
15
+ import chalk16 from "chalk";
5
16
  import { Command } from "commander";
6
17
 
7
18
  // src/commands/boundaries.ts
@@ -27,530 +38,6 @@ function findProjectRoot(startDir) {
27
38
  }
28
39
  }
29
40
 
30
- // src/utils/prompt.ts
31
- import * as clack5 from "@clack/prompts";
32
-
33
- // src/utils/prompt-integrations.ts
34
- import { spawnSync } from "child_process";
35
- import * as clack from "@clack/prompts";
36
- async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
37
- const choice = await clack.select({
38
- message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
39
- options: [
40
- {
41
- value: "install",
42
- label: "Yes, install Lefthook",
43
- hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
44
- },
45
- {
46
- value: "skip",
47
- label: "No, skip",
48
- hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
49
- }
50
- ]
51
- });
52
- assertNotCancelled(choice);
53
- if (choice !== "install") return void 0;
54
- const pm = packageManager || "npm";
55
- const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
56
- const s = clack.spinner();
57
- s.start("Installing Lefthook...");
58
- const result = spawnSync(installCmd, {
59
- cwd: projectRoot,
60
- shell: true,
61
- encoding: "utf-8",
62
- stdio: "pipe"
63
- });
64
- if (result.status === 0) {
65
- const fs21 = await import("fs");
66
- const path21 = await import("path");
67
- const lefthookPath = path21.join(projectRoot, "lefthook.yml");
68
- if (!fs21.existsSync(lefthookPath)) {
69
- fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
70
- }
71
- s.stop("Installed Lefthook");
72
- return "Lefthook";
73
- }
74
- s.stop("Failed to install Lefthook");
75
- clack.log.warn(`Install manually: ${installCmd}`);
76
- return void 0;
77
- }
78
- async function promptIntegrations(projectRoot, hookManager, tools) {
79
- let resolvedHookManager = hookManager;
80
- if (!resolvedHookManager) {
81
- resolvedHookManager = await promptHookManagerInstall(
82
- projectRoot,
83
- tools?.packageManager ?? "npm",
84
- tools?.isWorkspace
85
- );
86
- }
87
- const isBareHook = !resolvedHookManager;
88
- const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
89
- const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
90
- const options = [
91
- {
92
- value: "preCommit",
93
- label: hookLabel,
94
- hint: hookHint
95
- }
96
- ];
97
- if (tools?.isTypeScript) {
98
- options.push({
99
- value: "typecheck",
100
- label: "Typecheck (tsc --noEmit)",
101
- hint: "pre-commit hook + CI check"
102
- });
103
- }
104
- if (tools?.linter) {
105
- const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
106
- options.push({
107
- value: "lint",
108
- label: `Lint check (${linterName})`,
109
- hint: "pre-commit hook + CI check"
110
- });
111
- }
112
- options.push(
113
- {
114
- value: "claude",
115
- label: "Claude Code hook",
116
- hint: "checks files when Claude edits them"
117
- },
118
- {
119
- value: "claudeMd",
120
- label: "CLAUDE.md reference",
121
- hint: "appends @.viberails/context.md so Claude loads rules automatically"
122
- },
123
- {
124
- value: "githubAction",
125
- label: "GitHub Actions workflow",
126
- hint: "blocks PRs that fail viberails check"
127
- }
128
- );
129
- const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
130
- const result = await clack.multiselect({
131
- message: "Set up integrations?",
132
- options,
133
- initialValues,
134
- required: false
135
- });
136
- assertNotCancelled(result);
137
- return {
138
- preCommitHook: result.includes("preCommit"),
139
- claudeCodeHook: result.includes("claude"),
140
- claudeMdRef: result.includes("claudeMd"),
141
- githubAction: result.includes("githubAction"),
142
- typecheckHook: result.includes("typecheck"),
143
- lintHook: result.includes("lint")
144
- };
145
- }
146
-
147
- // src/utils/prompt-rules.ts
148
- import * as clack4 from "@clack/prompts";
149
-
150
- // src/utils/prompt-menu-handlers.ts
151
- import * as clack3 from "@clack/prompts";
152
-
153
- // src/utils/prompt-package-overrides.ts
154
- import * as clack2 from "@clack/prompts";
155
- function normalizePackageOverrides(packages) {
156
- for (const pkg of packages) {
157
- if (pkg.rules && Object.keys(pkg.rules).length === 0) {
158
- delete pkg.rules;
159
- }
160
- if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
161
- delete pkg.coverage;
162
- }
163
- }
164
- return packages;
165
- }
166
- function packageCoverageHint(pkg, defaults) {
167
- const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
168
- const isExempt = coverage === 0;
169
- const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
170
- const defaultCommand = defaults.coverageCommand ?? "";
171
- const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
172
- const tags = [];
173
- const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
174
- const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
175
- tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
176
- if (hasSummaryOverride) tags.push("summary override");
177
- if (hasCommandOverride) tags.push("command override");
178
- return tags.join(", ");
179
- }
180
- async function promptPackageCoverageOverrides(packages, defaults) {
181
- const editablePackages = packages.filter((pkg) => pkg.path !== ".");
182
- if (editablePackages.length === 0) return packages;
183
- while (true) {
184
- const selectedPath = await clack2.select({
185
- message: "Select package to edit coverage overrides",
186
- options: [
187
- ...editablePackages.map((pkg) => ({
188
- value: pkg.path,
189
- label: `${pkg.path} (${pkg.name})`,
190
- hint: packageCoverageHint(pkg, defaults)
191
- })),
192
- { value: "__done__", label: "Done" }
193
- ]
194
- });
195
- assertNotCancelled(selectedPath);
196
- if (selectedPath === "__done__") break;
197
- const target = editablePackages.find((pkg) => pkg.path === selectedPath);
198
- if (!target) continue;
199
- while (true) {
200
- const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
201
- const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
202
- const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
203
- const choice = await clack2.select({
204
- message: `Edit coverage overrides for ${target.path}`,
205
- options: [
206
- { value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
207
- { value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
208
- { value: "command", label: "coverage.command", hint: effectiveCommand },
209
- { value: "reset", label: "Reset this package to inherit defaults" },
210
- { value: "back", label: "Back to package list" }
211
- ]
212
- });
213
- assertNotCancelled(choice);
214
- if (choice === "back") break;
215
- if (choice === "testCoverage") {
216
- const result = await clack2.text({
217
- message: "Package testCoverage (0 to exempt package)?",
218
- initialValue: String(effectiveCoverage),
219
- validate: (v) => {
220
- if (typeof v !== "string") return "Enter a number between 0 and 100";
221
- const n = Number.parseInt(v, 10);
222
- if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
223
- }
224
- });
225
- assertNotCancelled(result);
226
- const nextCoverage = Number.parseInt(result, 10);
227
- if (nextCoverage === defaults.testCoverage) {
228
- if (target.rules) {
229
- delete target.rules.testCoverage;
230
- }
231
- } else {
232
- target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
233
- }
234
- }
235
- if (choice === "summaryPath") {
236
- const result = await clack2.text({
237
- message: "Package coverage.summaryPath (blank to inherit default)?",
238
- initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
239
- placeholder: defaults.coverageSummaryPath
240
- });
241
- assertNotCancelled(result);
242
- const value = result.trim();
243
- if (value.length === 0 || value === defaults.coverageSummaryPath) {
244
- if (target.coverage) {
245
- delete target.coverage.summaryPath;
246
- }
247
- } else {
248
- target.coverage = { ...target.coverage ?? {}, summaryPath: value };
249
- }
250
- }
251
- if (choice === "command") {
252
- const result = await clack2.text({
253
- message: "Package coverage.command (blank to inherit default/auto)?",
254
- initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
255
- placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
256
- });
257
- assertNotCancelled(result);
258
- const value = result.trim();
259
- const defaultCommand = defaults.coverageCommand ?? "";
260
- if (value.length === 0 || value === defaultCommand) {
261
- if (target.coverage) {
262
- delete target.coverage.command;
263
- }
264
- } else {
265
- target.coverage = { ...target.coverage ?? {}, command: value };
266
- }
267
- }
268
- if (choice === "reset") {
269
- if (target.rules) {
270
- delete target.rules.testCoverage;
271
- }
272
- delete target.coverage;
273
- }
274
- normalizePackageOverrides(editablePackages);
275
- }
276
- }
277
- return normalizePackageOverrides(packages);
278
- }
279
-
280
- // src/utils/prompt-menu-handlers.ts
281
- function getPackageDiffs(pkg, root) {
282
- const diffs = [];
283
- const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
284
- for (const key of convKeys) {
285
- if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
286
- diffs.push(`${key}: ${pkg.conventions[key]}`);
287
- }
288
- }
289
- const stackKeys = [
290
- "framework",
291
- "language",
292
- "styling",
293
- "backend",
294
- "orm",
295
- "linter",
296
- "formatter",
297
- "testRunner",
298
- "packageManager"
299
- ];
300
- for (const key of stackKeys) {
301
- if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
302
- diffs.push(`${key}: ${pkg.stack[key]}`);
303
- }
304
- }
305
- if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
306
- diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
307
- }
308
- if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
309
- diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
310
- }
311
- if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
312
- diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
313
- }
314
- if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
315
- diffs.push("coverage.command: (override)");
316
- }
317
- return diffs;
318
- }
319
- function buildMenuOptions(state, packageCount) {
320
- const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
321
- const options = [
322
- { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
323
- { value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
324
- ];
325
- if (state.fileNamingValue) {
326
- options.push({
327
- value: "fileNaming",
328
- label: "File naming convention",
329
- hint: state.fileNamingValue
330
- });
331
- }
332
- const isMonorepo = packageCount > 0;
333
- const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
334
- const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
335
- options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
336
- options.push({
337
- value: "enforceMissingTests",
338
- label: "Enforce missing tests",
339
- hint: state.enforceMissingTests ? "yes" : "no"
340
- });
341
- if (state.testCoverage > 0) {
342
- options.push(
343
- {
344
- value: "coverageSummaryPath",
345
- label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
346
- hint: state.coverageSummaryPath
347
- },
348
- {
349
- value: "coverageCommand",
350
- label: isMonorepo ? "Default coverage command" : "Coverage command",
351
- hint: state.coverageCommand ?? "auto-detect from package.json test runner"
352
- }
353
- );
354
- if (isMonorepo) {
355
- options.push({
356
- value: "packageOverrides",
357
- label: "Per-package coverage overrides",
358
- hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
359
- });
360
- }
361
- }
362
- options.push(
363
- { value: "reset", label: "Reset all to detected defaults" },
364
- { value: "done", label: "Done" }
365
- );
366
- return options;
367
- }
368
- function clonePackages(packages) {
369
- return packages?.map((pkg) => ({
370
- ...pkg,
371
- stack: pkg.stack ? { ...pkg.stack } : void 0,
372
- structure: pkg.structure ? { ...pkg.structure } : void 0,
373
- conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
374
- rules: pkg.rules ? { ...pkg.rules } : void 0,
375
- coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
376
- ignore: pkg.ignore ? [...pkg.ignore] : void 0,
377
- boundaries: pkg.boundaries ? {
378
- deny: [...pkg.boundaries.deny],
379
- ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
380
- } : void 0
381
- }));
382
- }
383
- async function handleMenuChoice(choice, state, defaults, root) {
384
- if (choice === "reset") {
385
- state.maxFileLines = defaults.maxFileLines;
386
- state.testCoverage = defaults.testCoverage;
387
- state.enforceMissingTests = defaults.enforceMissingTests;
388
- state.enforceNaming = defaults.enforceNaming;
389
- state.fileNamingValue = defaults.fileNamingValue;
390
- state.coverageSummaryPath = defaults.coverageSummaryPath;
391
- state.coverageCommand = defaults.coverageCommand;
392
- state.packageOverrides = clonePackages(defaults.packageOverrides);
393
- clack3.log.info("Reset all rules to detected defaults.");
394
- return;
395
- }
396
- if (choice === "packageOverrides") {
397
- if (state.packageOverrides) {
398
- const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
399
- state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
400
- testCoverage: state.testCoverage,
401
- coverageSummaryPath: state.coverageSummaryPath,
402
- coverageCommand: state.coverageCommand
403
- });
404
- const lines = packageDiffs.map((entry) => `${entry.pkg.path}
405
- ${entry.diffs.join(", ")}`);
406
- if (lines.length > 0) {
407
- clack3.note(lines.join("\n\n"), "Existing package differences");
408
- }
409
- }
410
- return;
411
- }
412
- if (choice === "maxFileLines") {
413
- const result = await clack3.text({
414
- message: "Maximum lines per source file?",
415
- initialValue: String(state.maxFileLines),
416
- validate: (v) => {
417
- if (typeof v !== "string") return "Enter a positive number";
418
- const n = Number.parseInt(v, 10);
419
- if (Number.isNaN(n) || n < 1) return "Enter a positive number";
420
- }
421
- });
422
- assertNotCancelled(result);
423
- state.maxFileLines = Number.parseInt(result, 10);
424
- }
425
- if (choice === "enforceMissingTests") {
426
- const result = await clack3.confirm({
427
- message: "Require every source file to have a corresponding test file?",
428
- initialValue: state.enforceMissingTests
429
- });
430
- assertNotCancelled(result);
431
- state.enforceMissingTests = result;
432
- }
433
- if (choice === "testCoverage") {
434
- const result = await clack3.text({
435
- message: "Test coverage target (0 disables coverage checks)?",
436
- initialValue: String(state.testCoverage),
437
- validate: (v) => {
438
- if (typeof v !== "string") return "Enter a number between 0 and 100";
439
- const n = Number.parseInt(v, 10);
440
- if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
441
- }
442
- });
443
- assertNotCancelled(result);
444
- state.testCoverage = Number.parseInt(result, 10);
445
- }
446
- if (choice === "coverageSummaryPath") {
447
- const result = await clack3.text({
448
- message: "Coverage summary path (relative to package root)?",
449
- initialValue: state.coverageSummaryPath,
450
- validate: (v) => {
451
- if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
452
- }
453
- });
454
- assertNotCancelled(result);
455
- state.coverageSummaryPath = result.trim();
456
- }
457
- if (choice === "coverageCommand") {
458
- const result = await clack3.text({
459
- message: "Coverage command (blank to auto-detect from package.json)?",
460
- initialValue: state.coverageCommand ?? "",
461
- placeholder: "(auto-detect from package.json test runner)"
462
- });
463
- assertNotCancelled(result);
464
- const trimmed = result.trim();
465
- state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
466
- }
467
- if (choice === "enforceNaming") {
468
- const result = await clack3.confirm({
469
- message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
470
- initialValue: state.enforceNaming
471
- });
472
- assertNotCancelled(result);
473
- state.enforceNaming = result;
474
- }
475
- if (choice === "fileNaming") {
476
- const selected = await clack3.select({
477
- message: "Which file naming convention should be enforced?",
478
- options: [
479
- { value: "kebab-case", label: "kebab-case" },
480
- { value: "camelCase", label: "camelCase" },
481
- { value: "PascalCase", label: "PascalCase" },
482
- { value: "snake_case", label: "snake_case" }
483
- ],
484
- initialValue: state.fileNamingValue
485
- });
486
- assertNotCancelled(selected);
487
- state.fileNamingValue = selected;
488
- }
489
- }
490
-
491
- // src/utils/prompt-rules.ts
492
- function getRootPackage(packages) {
493
- return packages.find((pkg) => pkg.path === ".") ?? packages[0];
494
- }
495
- async function promptRuleMenu(defaults) {
496
- const state = {
497
- ...defaults,
498
- packageOverrides: clonePackages(defaults.packageOverrides)
499
- };
500
- const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
501
- const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
502
- while (true) {
503
- const options = buildMenuOptions(state, packageCount);
504
- const choice = await clack4.select({ message: "Customize rules", options });
505
- assertNotCancelled(choice);
506
- if (choice === "done") break;
507
- await handleMenuChoice(choice, state, defaults, root);
508
- }
509
- return {
510
- maxFileLines: state.maxFileLines,
511
- testCoverage: state.testCoverage,
512
- enforceMissingTests: state.enforceMissingTests,
513
- enforceNaming: state.enforceNaming,
514
- fileNamingValue: state.fileNamingValue,
515
- coverageSummaryPath: state.coverageSummaryPath,
516
- coverageCommand: state.coverageCommand,
517
- packageOverrides: state.packageOverrides
518
- };
519
- }
520
-
521
- // src/utils/prompt.ts
522
- function assertNotCancelled(value) {
523
- if (clack5.isCancel(value)) {
524
- clack5.cancel("Setup cancelled.");
525
- process.exit(0);
526
- }
527
- }
528
- async function confirm3(message) {
529
- const result = await clack5.confirm({ message, initialValue: true });
530
- assertNotCancelled(result);
531
- return result;
532
- }
533
- async function confirmDangerous(message) {
534
- const result = await clack5.confirm({ message, initialValue: false });
535
- assertNotCancelled(result);
536
- return result;
537
- }
538
- async function promptInitDecision() {
539
- const result = await clack5.select({
540
- message: "Accept these rules?",
541
- options: [
542
- {
543
- value: "accept",
544
- label: "Yes, looks good",
545
- hint: "warns on violation; use --enforce in CI to block"
546
- },
547
- { value: "customize", label: "Let me customize rules" }
548
- ]
549
- });
550
- assertNotCancelled(result);
551
- return result;
552
- }
553
-
554
41
  // src/utils/resolve-workspace-packages.ts
555
42
  import * as fs2 from "fs";
556
43
  import * as path2 from "path";
@@ -655,7 +142,7 @@ ${chalk.bold("Inferred boundary rules:")}
655
142
  console.log(`
656
143
  ${totalRules} denied`);
657
144
  console.log("");
658
- const shouldSave = await confirm3("Save to viberails.config.json?");
145
+ const shouldSave = await confirm("Save to viberails.config.json?");
659
146
  if (shouldSave) {
660
147
  config.boundaries = inferred;
661
148
  config.rules.enforceBoundaries = true;
@@ -742,7 +229,7 @@ function resolveIgnoreForFile(relPath, config) {
742
229
  }
743
230
 
744
231
  // src/commands/check-coverage.ts
745
- import { spawnSync as spawnSync2 } from "child_process";
232
+ import { spawnSync } from "child_process";
746
233
  import * as fs4 from "fs";
747
234
  import * as path4 from "path";
748
235
  import { inferCoverageCommand } from "@viberails/config";
@@ -785,7 +272,7 @@ function readCoveragePercentage(summaryPath) {
785
272
  }
786
273
  }
787
274
  function runCoverageCommand(pkgRoot, command) {
788
- const result = spawnSync2(command, {
275
+ const result = spawnSync(command, {
789
276
  cwd: pkgRoot,
790
277
  shell: true,
791
278
  encoding: "utf-8",
@@ -1167,13 +654,13 @@ function checkMissingTests(projectRoot, config, severity) {
1167
654
  const testSuffix = testPattern.replace("*", "");
1168
655
  const sourceFiles = collectSourceFiles(srcPath, projectRoot);
1169
656
  for (const relFile of sourceFiles) {
1170
- const basename9 = path6.basename(relFile);
1171
- if (basename9.includes(".test.") || basename9.includes(".spec.") || basename9.startsWith("index.") || basename9.endsWith(".d.ts")) {
657
+ const basename10 = path6.basename(relFile);
658
+ if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
1172
659
  continue;
1173
660
  }
1174
- const ext = path6.extname(basename9);
661
+ const ext = path6.extname(basename10);
1175
662
  if (!SOURCE_EXTS2.has(ext)) continue;
1176
- const stem = basename9.slice(0, -ext.length);
663
+ const stem = basename10.slice(0, -ext.length);
1177
664
  const expectedTestFile = `${stem}${testSuffix}`;
1178
665
  const dir = path6.dirname(path6.join(projectRoot, relFile));
1179
666
  const colocatedTest = path6.join(dir, expectedTestFile);
@@ -1254,9 +741,9 @@ async function checkCommand(options, cwd) {
1254
741
  }
1255
742
  const violations = [];
1256
743
  const severity = options.enforce ? "error" : "warn";
1257
- const log7 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
744
+ const log5 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
1258
745
  };
1259
- log7(" Checking files...");
746
+ log5(" Checking files...");
1260
747
  for (const file of filesToCheck) {
1261
748
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1262
749
  const relPath = path7.relative(projectRoot, absPath);
@@ -1289,9 +776,9 @@ async function checkCommand(options, cwd) {
1289
776
  }
1290
777
  }
1291
778
  }
1292
- log7(" done\n");
779
+ log5(" done\n");
1293
780
  if (!options.files) {
1294
- log7(" Checking missing tests...");
781
+ log5(" Checking missing tests...");
1295
782
  const testViolations = checkMissingTests(projectRoot, config, severity);
1296
783
  if (options.staged) {
1297
784
  const stagedSet = new Set(filesToCheck);
@@ -1304,14 +791,14 @@ async function checkCommand(options, cwd) {
1304
791
  } else {
1305
792
  violations.push(...testViolations);
1306
793
  }
1307
- log7(" done\n");
794
+ log5(" done\n");
1308
795
  }
1309
796
  if (!options.files && !options.staged && !options.diffBase) {
1310
- log7(" Running test coverage...\n");
797
+ log5(" Running test coverage...\n");
1311
798
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1312
799
  staged: options.staged,
1313
800
  enforce: options.enforce,
1314
- onProgress: (pkg) => log7(` Coverage: ${pkg}...
801
+ onProgress: (pkg) => log5(` Coverage: ${pkg}...
1315
802
  `)
1316
803
  });
1317
804
  violations.push(...coverageViolations);
@@ -1336,7 +823,7 @@ async function checkCommand(options, cwd) {
1336
823
  severity
1337
824
  });
1338
825
  }
1339
- log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
826
+ log5(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1340
827
  `);
1341
828
  }
1342
829
  if (options.format === "json") {
@@ -1412,7 +899,7 @@ async function hookCheckCommand(cwd) {
1412
899
  // src/commands/config.ts
1413
900
  import * as fs10 from "fs";
1414
901
  import * as path9 from "path";
1415
- import * as clack6 from "@clack/prompts";
902
+ import * as clack from "@clack/prompts";
1416
903
  import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
1417
904
  import { scan } from "@viberails/scanner";
1418
905
  import chalk6 from "chalk";
@@ -1732,58 +1219,6 @@ function displayRulesPreview(config) {
1732
1219
  );
1733
1220
  console.log("");
1734
1221
  }
1735
- function displayInitSummary(config, exemptedPackages) {
1736
- const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1737
- const isMonorepo = config.packages.length > 1;
1738
- const ok = chalk5.green("\u2713");
1739
- const off = chalk5.dim("\u25CB");
1740
- console.log("");
1741
- console.log(` ${chalk5.bold("Rules to apply:")}`);
1742
- console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
1743
- const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
1744
- if (config.rules.enforceNaming && fileNaming) {
1745
- console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
1746
- } else {
1747
- console.log(` ${off} File naming: ${chalk5.dim("not enforced")}`);
1748
- }
1749
- const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
1750
- if (config.rules.enforceMissingTests && testPattern) {
1751
- console.log(` ${ok} Missing tests: ${chalk5.cyan(`enforced (${testPattern})`)}`);
1752
- } else if (config.rules.enforceMissingTests) {
1753
- console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
1754
- } else {
1755
- console.log(` ${off} Missing tests: ${chalk5.dim("not enforced")}`);
1756
- }
1757
- if (config.rules.testCoverage > 0) {
1758
- if (isMonorepo) {
1759
- const withCoverage = config.packages.filter(
1760
- (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
1761
- );
1762
- console.log(
1763
- ` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)} default ${chalk5.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
1764
- );
1765
- } else {
1766
- console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
1767
- }
1768
- } else {
1769
- console.log(` ${off} Coverage: ${chalk5.dim("disabled")}`);
1770
- }
1771
- if (exemptedPackages.length > 0) {
1772
- console.log(
1773
- ` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
1774
- );
1775
- }
1776
- if (isMonorepo) {
1777
- console.log(
1778
- `
1779
- ${chalk5.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
1780
- );
1781
- } else {
1782
- console.log(`
1783
- ${chalk5.dim("warns on violation \xB7 use --enforce in CI to block")}`);
1784
- }
1785
- console.log("");
1786
- }
1787
1222
 
1788
1223
  // src/display-text.ts
1789
1224
  function plainConfidenceLabel(convention) {
@@ -1902,7 +1337,9 @@ function formatScanResultsText(scanResult) {
1902
1337
  // src/utils/apply-rule-overrides.ts
1903
1338
  function applyRuleOverrides(config, overrides) {
1904
1339
  if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
1340
+ const rootPkg = getRootPackage(config.packages);
1905
1341
  config.rules.maxFileLines = overrides.maxFileLines;
1342
+ config.rules.maxTestFileLines = overrides.maxTestFileLines;
1906
1343
  config.rules.testCoverage = overrides.testCoverage;
1907
1344
  config.rules.enforceMissingTests = overrides.enforceMissingTests;
1908
1345
  config.rules.enforceNaming = overrides.enforceNaming;
@@ -1916,7 +1353,6 @@ function applyRuleOverrides(config, overrides) {
1916
1353
  }
1917
1354
  }
1918
1355
  if (overrides.fileNamingValue) {
1919
- const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1920
1356
  const oldNaming = rootPkg.conventions?.fileNaming;
1921
1357
  rootPkg.conventions = rootPkg.conventions ?? {};
1922
1358
  rootPkg.conventions.fileNaming = overrides.fileNamingValue;
@@ -1928,6 +1364,18 @@ function applyRuleOverrides(config, overrides) {
1928
1364
  }
1929
1365
  }
1930
1366
  }
1367
+ if (rootPkg) {
1368
+ rootPkg.conventions = rootPkg.conventions ?? {};
1369
+ if (overrides.componentNaming !== void 0) {
1370
+ rootPkg.conventions.componentNaming = overrides.componentNaming || void 0;
1371
+ }
1372
+ if (overrides.hookNaming !== void 0) {
1373
+ rootPkg.conventions.hookNaming = overrides.hookNaming || void 0;
1374
+ }
1375
+ if (overrides.importAlias !== void 0) {
1376
+ rootPkg.conventions.importAlias = overrides.importAlias || void 0;
1377
+ }
1378
+ }
1931
1379
  }
1932
1380
 
1933
1381
  // src/utils/diff-configs.ts
@@ -2092,29 +1540,35 @@ async function configCommand(options, cwd) {
2092
1540
  }
2093
1541
  const configPath = path9.join(projectRoot, CONFIG_FILE3);
2094
1542
  if (!fs10.existsSync(configPath)) {
2095
- console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails init")} first.`);
1543
+ console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails")} first.`);
2096
1544
  return;
2097
1545
  }
2098
- clack6.intro("viberails config");
1546
+ if (!options.suppressIntro) {
1547
+ clack.intro("viberails config");
1548
+ }
2099
1549
  const config = await loadConfig3(configPath);
2100
1550
  let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2101
- clack6.note(formatRulesText(config).join("\n"), "Current rules");
1551
+ clack.note(formatRulesText(config).join("\n"), "Current rules");
2102
1552
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2103
1553
  const overrides = await promptRuleMenu({
2104
1554
  maxFileLines: config.rules.maxFileLines,
1555
+ maxTestFileLines: config.rules.maxTestFileLines,
2105
1556
  testCoverage: config.rules.testCoverage,
2106
1557
  enforceMissingTests: config.rules.enforceMissingTests,
2107
1558
  enforceNaming: config.rules.enforceNaming,
2108
1559
  fileNamingValue: rootPkg.conventions?.fileNaming,
1560
+ componentNaming: rootPkg.conventions?.componentNaming,
1561
+ hookNaming: rootPkg.conventions?.hookNaming,
1562
+ importAlias: rootPkg.conventions?.importAlias,
2109
1563
  coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
2110
1564
  coverageCommand: config.defaults?.coverage?.command,
2111
1565
  packageOverrides: config.packages
2112
1566
  });
2113
1567
  applyRuleOverrides(config, overrides);
2114
1568
  if (options.rescan && config.packages.length > 1) {
2115
- const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
1569
+ const shouldInfer = await confirm("Re-infer boundary rules from import patterns?");
2116
1570
  if (shouldInfer) {
2117
- const bs = clack6.spinner();
1571
+ const bs = clack.spinner();
2118
1572
  bs.start("Building import graph...");
2119
1573
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2120
1574
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -2130,31 +1584,31 @@ async function configCommand(options, cwd) {
2130
1584
  }
2131
1585
  }
2132
1586
  }
2133
- const shouldWrite = await confirm3("Save updated configuration?");
1587
+ const shouldWrite = await confirm("Save updated configuration?");
2134
1588
  if (!shouldWrite) {
2135
- clack6.outro("No changes written.");
1589
+ clack.outro("No changes written.");
2136
1590
  return;
2137
1591
  }
2138
1592
  const compacted = compactConfig2(config);
2139
1593
  fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2140
1594
  `);
2141
1595
  if (!scanResult) {
2142
- const s = clack6.spinner();
1596
+ const s = clack.spinner();
2143
1597
  s.start("Scanning for context generation...");
2144
1598
  scanResult = await scan(projectRoot);
2145
1599
  s.stop("Scan complete");
2146
1600
  }
2147
1601
  writeGeneratedFiles(projectRoot, config, scanResult);
2148
- clack6.log.success(
1602
+ clack.log.success(
2149
1603
  `Updated:
2150
1604
  ${CONFIG_FILE3}
2151
1605
  .viberails/context.md
2152
1606
  .viberails/scan-result.json`
2153
1607
  );
2154
- clack6.outro("Done! Run viberails check to verify.");
1608
+ clack.outro("Done! Run viberails check to verify.");
2155
1609
  }
2156
1610
  async function rescanAndMerge(projectRoot, config) {
2157
- const s = clack6.spinner();
1611
+ const s = clack.spinner();
2158
1612
  s.start("Re-scanning project...");
2159
1613
  const scanResult = await scan(projectRoot);
2160
1614
  const merged = mergeConfig(config, scanResult);
@@ -2165,9 +1619,9 @@ async function rescanAndMerge(projectRoot, config) {
2165
1619
  const icon = c.type === "removed" ? "-" : "+";
2166
1620
  return `${icon} ${c.description}`;
2167
1621
  }).join("\n");
2168
- clack6.note(changeLines, "Changes detected");
1622
+ clack.note(changeLines, "Changes detected");
2169
1623
  } else {
2170
- clack6.log.info("No new changes detected from scan.");
1624
+ clack.log.info("No new changes detected from scan.");
2171
1625
  }
2172
1626
  Object.assign(config, merged);
2173
1627
  return scanResult;
@@ -2486,10 +1940,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
2486
1940
  const pkg = resolvePackageForFile(sourceRelPath, config);
2487
1941
  const testPattern = pkg?.structure?.testPattern;
2488
1942
  if (!testPattern) return null;
2489
- const basename9 = path12.basename(sourceRelPath);
2490
- const ext = path12.extname(basename9);
1943
+ const basename10 = path12.basename(sourceRelPath);
1944
+ const ext = path12.extname(basename10);
2491
1945
  if (!ext) return null;
2492
- const stem = basename9.slice(0, -ext.length);
1946
+ const stem = basename10.slice(0, -ext.length);
2493
1947
  const testSuffix = testPattern.replace("*", "");
2494
1948
  const testFilename = `${stem}${testSuffix}`;
2495
1949
  const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
@@ -2661,19 +2115,159 @@ ${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
2661
2115
  }
2662
2116
 
2663
2117
  // src/commands/init.ts
2664
- import * as fs19 from "fs";
2665
- import * as path19 from "path";
2666
- import * as clack8 from "@clack/prompts";
2667
- import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
2668
- import { scan as scan2 } from "@viberails/scanner";
2669
- import chalk12 from "chalk";
2118
+ import * as fs20 from "fs";
2119
+ import * as path20 from "path";
2120
+ import * as clack4 from "@clack/prompts";
2121
+ import { compactConfig as compactConfig4, generateConfig as generateConfig2 } from "@viberails/config";
2122
+ import { scan as scan3 } from "@viberails/scanner";
2123
+ import chalk14 from "chalk";
2124
+
2125
+ // src/display-init.ts
2126
+ import { FRAMEWORK_NAMES as FRAMEWORK_NAMES5, STYLING_NAMES as STYLING_NAMES5 } from "@viberails/types";
2127
+ import chalk9 from "chalk";
2128
+ var INIT_OVERVIEW_NAMES = {
2129
+ typescript: "TypeScript",
2130
+ javascript: "JavaScript",
2131
+ eslint: "ESLint",
2132
+ prettier: "Prettier",
2133
+ jest: "Jest",
2134
+ vitest: "Vitest",
2135
+ biome: "Biome"
2136
+ };
2137
+ function formatDetectedOverview(scanResult) {
2138
+ const { stack } = scanResult;
2139
+ const primaryParts = [];
2140
+ const secondaryParts = [];
2141
+ const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
2142
+ if (scanResult.packages.length > 1) {
2143
+ primaryParts.push("monorepo");
2144
+ primaryParts.push(`${scanResult.packages.length} packages`);
2145
+ } else if (stack.framework) {
2146
+ primaryParts.push(formatItem(stack.framework, FRAMEWORK_NAMES5));
2147
+ } else {
2148
+ primaryParts.push("single package");
2149
+ }
2150
+ primaryParts.push(formatOverviewItem(stack.language));
2151
+ if (stack.styling) {
2152
+ primaryParts.push(formatOverviewItem(stack.styling, STYLING_NAMES5));
2153
+ }
2154
+ if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
2155
+ if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
2156
+ if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
2157
+ if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
2158
+ const primary = primaryParts.map((part) => chalk9.cyan(part)).join(chalk9.dim(" \xB7 "));
2159
+ const secondary = secondaryParts.join(chalk9.dim(" \xB7 "));
2160
+ return secondary ? `${primary}
2161
+ ${chalk9.dim(secondary)}` : primary;
2162
+ }
2163
+ function displayInitOverview(scanResult, config, exemptedPackages) {
2164
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
2165
+ const isMonorepo = config.packages.length > 1;
2166
+ const ok = chalk9.green("\u2713");
2167
+ const info = chalk9.yellow("~");
2168
+ console.log("");
2169
+ console.log(` ${chalk9.bold("Ready to initialize:")}`);
2170
+ console.log(` ${formatDetectedOverview(scanResult)}`);
2171
+ console.log("");
2172
+ console.log(` ${chalk9.bold("Rules to apply:")}`);
2173
+ console.log(` ${ok} Max file size: ${chalk9.cyan(`${config.rules.maxFileLines} lines`)}`);
2174
+ const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
2175
+ if (config.rules.enforceNaming && fileNaming) {
2176
+ console.log(` ${ok} File naming: ${chalk9.cyan(fileNaming)}`);
2177
+ } else {
2178
+ console.log(` ${info} File naming: ${chalk9.dim("not enforced")}`);
2179
+ }
2180
+ const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
2181
+ if (config.rules.enforceMissingTests && testPattern) {
2182
+ console.log(` ${ok} Missing tests: ${chalk9.cyan(`enforced (${testPattern})`)}`);
2183
+ } else if (config.rules.enforceMissingTests) {
2184
+ console.log(` ${ok} Missing tests: ${chalk9.cyan("enforced")}`);
2185
+ } else {
2186
+ console.log(` ${info} Missing tests: ${chalk9.dim("not enforced")}`);
2187
+ }
2188
+ if (config.rules.testCoverage > 0) {
2189
+ if (isMonorepo) {
2190
+ const withCoverage = config.packages.filter(
2191
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
2192
+ );
2193
+ console.log(
2194
+ ` ${ok} Coverage: ${chalk9.cyan(`${config.rules.testCoverage}%`)} default ${chalk9.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
2195
+ );
2196
+ } else {
2197
+ console.log(` ${ok} Coverage: ${chalk9.cyan(`${config.rules.testCoverage}%`)}`);
2198
+ }
2199
+ } else {
2200
+ console.log(` ${info} Coverage: ${chalk9.dim("disabled")}`);
2201
+ }
2202
+ if (exemptedPackages.length > 0) {
2203
+ console.log(
2204
+ ` ${chalk9.dim(" exempted:")} ${chalk9.dim(exemptedPackages.join(", "))} ${chalk9.dim("(types-only)")}`
2205
+ );
2206
+ }
2207
+ console.log("");
2208
+ console.log(` ${chalk9.bold("Also available:")}`);
2209
+ if (isMonorepo) {
2210
+ console.log(` ${info} Infer boundaries from current imports`);
2211
+ }
2212
+ console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
2213
+ console.log(
2214
+ `
2215
+ ${chalk9.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
2216
+ );
2217
+ console.log("");
2218
+ }
2219
+ function summarizeSelectedIntegrations(integrations, opts) {
2220
+ const lines = [];
2221
+ if (opts.hasBoundaries) {
2222
+ lines.push("\u2713 Boundary rules: inferred from current imports");
2223
+ } else {
2224
+ lines.push("~ Boundary rules: not enabled");
2225
+ }
2226
+ if (opts.hasCoverage) {
2227
+ lines.push("\u2713 Coverage checks: enabled");
2228
+ } else {
2229
+ lines.push("~ Coverage checks: disabled");
2230
+ }
2231
+ const selectedIntegrations = [
2232
+ integrations.preCommitHook ? "pre-commit hook" : void 0,
2233
+ integrations.typecheckHook ? "typecheck" : void 0,
2234
+ integrations.lintHook ? "lint check" : void 0,
2235
+ integrations.claudeCodeHook ? "Claude Code hook" : void 0,
2236
+ integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
2237
+ integrations.githubAction ? "GitHub Actions workflow" : void 0
2238
+ ].filter(Boolean);
2239
+ if (selectedIntegrations.length > 0) {
2240
+ lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
2241
+ } else {
2242
+ lines.push("~ Integrations: none selected");
2243
+ }
2244
+ return lines;
2245
+ }
2246
+ function displaySetupPlan(config, integrations, opts = {}) {
2247
+ const configFile = opts.configFile ?? "viberails.config.json";
2248
+ const lines = summarizeSelectedIntegrations(integrations, {
2249
+ hasBoundaries: config.rules.enforceBoundaries,
2250
+ hasCoverage: config.rules.testCoverage > 0
2251
+ });
2252
+ console.log("");
2253
+ console.log(` ${chalk9.bold("Ready to write:")}`);
2254
+ console.log(
2255
+ ` ${opts.replacingExistingConfig ? chalk9.yellow("!") : chalk9.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? chalk9.dim(" (replacing existing config)") : ""}`
2256
+ );
2257
+ console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
2258
+ console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
2259
+ for (const line of lines) {
2260
+ const icon = line.startsWith("\u2713") ? chalk9.green("\u2713") : chalk9.yellow("~");
2261
+ console.log(` ${icon} ${line.slice(2)}`);
2262
+ }
2263
+ console.log("");
2264
+ }
2670
2265
 
2671
2266
  // src/utils/check-prerequisites.ts
2672
- import { spawnSync as spawnSync3 } from "child_process";
2673
2267
  import * as fs14 from "fs";
2674
2268
  import * as path14 from "path";
2675
- import * as clack7 from "@clack/prompts";
2676
- import chalk9 from "chalk";
2269
+ import * as clack2 from "@clack/prompts";
2270
+ import chalk10 from "chalk";
2677
2271
  function checkCoveragePrereqs(projectRoot, scanResult) {
2678
2272
  const pm = scanResult.stack.packageManager.name;
2679
2273
  const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
@@ -2704,9 +2298,9 @@ function displayMissingPrereqs(prereqs) {
2704
2298
  const missing = prereqs.filter((p) => !p.installed);
2705
2299
  for (const m of missing) {
2706
2300
  const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
2707
- console.log(` ${chalk9.yellow("!")} ${m.label} not installed${suffix}`);
2301
+ console.log(` ${chalk10.yellow("!")} ${m.label} not installed${suffix}`);
2708
2302
  if (m.installCommand) {
2709
- console.log(` Install: ${chalk9.cyan(m.installCommand)}`);
2303
+ console.log(` Install: ${chalk10.cyan(m.installCommand)}`);
2710
2304
  }
2711
2305
  }
2712
2306
  }
@@ -2718,24 +2312,24 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2718
2312
  const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
2719
2313
  return `\u2717 ${p.label} \u2014 ${detail}`;
2720
2314
  }).join("\n");
2721
- clack7.note(prereqLines, "Coverage prerequisites");
2315
+ clack2.note(prereqLines, "Coverage support");
2722
2316
  let disableCoverage = false;
2723
2317
  for (const m of missing) {
2724
2318
  if (!m.installCommand) continue;
2725
2319
  const pkgCount = m.affectedPackages?.length;
2726
2320
  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.`;
2727
- const choice = await clack7.select({
2321
+ const choice = await clack2.select({
2728
2322
  message,
2729
2323
  options: [
2730
2324
  {
2731
2325
  value: "install",
2732
- label: `Yes, install now`,
2326
+ label: "Install now",
2733
2327
  hint: m.installCommand
2734
2328
  },
2735
2329
  {
2736
2330
  value: "disable",
2737
- label: "No, disable coverage percentage checks",
2738
- hint: "missing-test checks still active"
2331
+ label: "Disable coverage checks",
2332
+ hint: "missing-test checks still stay active"
2739
2333
  },
2740
2334
  {
2741
2335
  value: "skip",
@@ -2746,28 +2340,23 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2746
2340
  });
2747
2341
  assertNotCancelled(choice);
2748
2342
  if (choice === "install") {
2749
- const is = clack7.spinner();
2343
+ const is = clack2.spinner();
2750
2344
  is.start(`Installing ${m.label}...`);
2751
- const result = spawnSync3(m.installCommand, {
2752
- cwd: projectRoot,
2753
- shell: true,
2754
- encoding: "utf-8",
2755
- stdio: "pipe"
2756
- });
2345
+ const result = await spawnAsync(m.installCommand, projectRoot);
2757
2346
  if (result.status === 0) {
2758
2347
  is.stop(`Installed ${m.label}`);
2759
2348
  } else {
2760
2349
  is.stop(`Failed to install ${m.label}`);
2761
- clack7.log.warn(
2350
+ clack2.log.warn(
2762
2351
  `Install manually: ${m.installCommand}
2763
2352
  Coverage percentage checks will not work until the dependency is installed.`
2764
2353
  );
2765
2354
  }
2766
2355
  } else if (choice === "disable") {
2767
2356
  disableCoverage = true;
2768
- clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2357
+ clack2.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2769
2358
  } else {
2770
- clack7.log.info(
2359
+ clack2.log.info(
2771
2360
  `Coverage percentage checks will fail until ${m.label} is installed.
2772
2361
  Install later: ${m.installCommand}`
2773
2362
  );
@@ -2785,20 +2374,6 @@ function hasDependency(projectRoot, name) {
2785
2374
  }
2786
2375
  }
2787
2376
 
2788
- // src/utils/filter-confidence.ts
2789
- function filterHighConfidence(conventions, meta) {
2790
- if (!meta) return conventions;
2791
- const filtered = {};
2792
- for (const [key, value] of Object.entries(conventions)) {
2793
- if (value === void 0) continue;
2794
- const convMeta = meta[key];
2795
- if (!convMeta || convMeta.confidence === "high") {
2796
- filtered[key] = value;
2797
- }
2798
- }
2799
- return filtered;
2800
- }
2801
-
2802
2377
  // src/utils/update-gitignore.ts
2803
2378
  import * as fs15 from "fs";
2804
2379
  import * as path15 from "path";
@@ -2819,7 +2394,7 @@ function updateGitignore(projectRoot) {
2819
2394
  // src/commands/init-hooks.ts
2820
2395
  import * as fs17 from "fs";
2821
2396
  import * as path17 from "path";
2822
- import chalk10 from "chalk";
2397
+ import chalk11 from "chalk";
2823
2398
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2824
2399
 
2825
2400
  // src/commands/resolve-typecheck.ts
@@ -2864,13 +2439,13 @@ function setupPreCommitHook(projectRoot) {
2864
2439
  const lefthookPath = path17.join(projectRoot, "lefthook.yml");
2865
2440
  if (fs17.existsSync(lefthookPath)) {
2866
2441
  addLefthookPreCommit(lefthookPath);
2867
- console.log(` ${chalk10.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2442
+ console.log(` ${chalk11.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2868
2443
  return "lefthook.yml";
2869
2444
  }
2870
2445
  const huskyDir = path17.join(projectRoot, ".husky");
2871
2446
  if (fs17.existsSync(huskyDir)) {
2872
2447
  writeHuskyPreCommit(huskyDir);
2873
- console.log(` ${chalk10.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2448
+ console.log(` ${chalk11.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2874
2449
  return ".husky/pre-commit";
2875
2450
  }
2876
2451
  const gitDir = path17.join(projectRoot, ".git");
@@ -2880,7 +2455,7 @@ function setupPreCommitHook(projectRoot) {
2880
2455
  fs17.mkdirSync(hooksDir, { recursive: true });
2881
2456
  }
2882
2457
  writeGitHookPreCommit(hooksDir);
2883
- console.log(` ${chalk10.green("\u2713")} .git/hooks/pre-commit`);
2458
+ console.log(` ${chalk11.green("\u2713")} .git/hooks/pre-commit`);
2884
2459
  return ".git/hooks/pre-commit";
2885
2460
  }
2886
2461
  return void 0;
@@ -2941,9 +2516,9 @@ function setupClaudeCodeHook(projectRoot) {
2941
2516
  settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
2942
2517
  } catch {
2943
2518
  console.warn(
2944
- ` ${chalk10.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2519
+ ` ${chalk11.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2945
2520
  );
2946
- console.warn(` Fix the JSON manually, then re-run ${chalk10.cyan("viberails init --force")}`);
2521
+ console.warn(` Fix the JSON manually, then re-run ${chalk11.cyan("viberails init --force")}`);
2947
2522
  return;
2948
2523
  }
2949
2524
  }
@@ -2966,7 +2541,7 @@ function setupClaudeCodeHook(projectRoot) {
2966
2541
  settings.hooks = hooks;
2967
2542
  fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2968
2543
  `);
2969
- console.log(` ${chalk10.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2544
+ console.log(` ${chalk11.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2970
2545
  }
2971
2546
  function setupClaudeMdReference(projectRoot) {
2972
2547
  const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
@@ -2978,7 +2553,7 @@ function setupClaudeMdReference(projectRoot) {
2978
2553
  const ref = "\n@.viberails/context.md\n";
2979
2554
  const prefix = content.length === 0 ? "" : content.trimEnd();
2980
2555
  fs17.writeFileSync(claudeMdPath, prefix + ref);
2981
- console.log(` ${chalk10.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2556
+ console.log(` ${chalk11.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2982
2557
  }
2983
2558
  function setupGithubAction(projectRoot, packageManager, options) {
2984
2559
  const workflowDir = path17.join(projectRoot, ".github", "workflows");
@@ -3064,7 +2639,7 @@ ${cmd}
3064
2639
  // src/commands/init-hooks-extra.ts
3065
2640
  import * as fs18 from "fs";
3066
2641
  import * as path18 from "path";
3067
- import chalk11 from "chalk";
2642
+ import chalk12 from "chalk";
3068
2643
  import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
3069
2644
  function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
3070
2645
  const lefthookPath = path18.join(projectRoot, "lefthook.yml");
@@ -3124,12 +2699,12 @@ ${command}
3124
2699
  function setupTypecheckHook(projectRoot, packageManager) {
3125
2700
  const resolved = resolveTypecheckCommand(projectRoot, packageManager);
3126
2701
  if (!resolved.command) {
3127
- console.log(` ${chalk11.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
2702
+ console.log(` ${chalk12.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3128
2703
  return void 0;
3129
2704
  }
3130
2705
  const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
3131
2706
  if (target) {
3132
- console.log(` ${chalk11.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
2707
+ console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3133
2708
  }
3134
2709
  return target;
3135
2710
  }
@@ -3150,7 +2725,7 @@ function setupLintHook(projectRoot, linter) {
3150
2725
  }
3151
2726
  const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
3152
2727
  if (target) {
3153
- console.log(` ${chalk11.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2728
+ console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3154
2729
  }
3155
2730
  return target;
3156
2731
  }
@@ -3186,31 +2761,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3186
2761
  return created;
3187
2762
  }
3188
2763
 
3189
- // src/commands/init.ts
3190
- var CONFIG_FILE5 = "viberails.config.json";
2764
+ // src/commands/init-non-interactive.ts
2765
+ import * as fs19 from "fs";
2766
+ import * as path19 from "path";
2767
+ import * as clack3 from "@clack/prompts";
2768
+ import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
2769
+ import { scan as scan2 } from "@viberails/scanner";
2770
+ import chalk13 from "chalk";
2771
+
2772
+ // src/utils/filter-confidence.ts
2773
+ function filterHighConfidence(conventions, meta) {
2774
+ if (!meta) return conventions;
2775
+ const filtered = {};
2776
+ for (const [key, value] of Object.entries(conventions)) {
2777
+ if (value === void 0) continue;
2778
+ const convMeta = meta[key];
2779
+ if (!convMeta || convMeta.confidence === "high") {
2780
+ filtered[key] = value;
2781
+ }
2782
+ }
2783
+ return filtered;
2784
+ }
2785
+
2786
+ // src/commands/init-non-interactive.ts
3191
2787
  function getExemptedPackages(config) {
3192
2788
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3193
2789
  }
3194
- async function initCommand(options, cwd) {
3195
- const projectRoot = findProjectRoot(cwd ?? process.cwd());
3196
- if (!projectRoot) {
3197
- throw new Error(
3198
- "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
3199
- );
3200
- }
3201
- const configPath = path19.join(projectRoot, CONFIG_FILE5);
3202
- if (fs19.existsSync(configPath) && !options.force) {
3203
- console.log(
3204
- `${chalk12.yellow("!")} viberails is already initialized.
3205
- Run ${chalk12.cyan("viberails config")} to edit rules, ${chalk12.cyan("viberails sync")} to update, or ${chalk12.cyan("viberails init --force")} to start fresh.`
3206
- );
3207
- return;
3208
- }
3209
- if (options.yes) return initNonInteractive(projectRoot, configPath);
3210
- await initInteractive(projectRoot, configPath, options);
3211
- }
3212
2790
  async function initNonInteractive(projectRoot, configPath) {
3213
- const s = clack8.spinner();
2791
+ const s = clack3.spinner();
3214
2792
  s.start("Scanning project...");
3215
2793
  const scanResult = await scan2(projectRoot);
3216
2794
  const config = generateConfig(scanResult);
@@ -3225,11 +2803,11 @@ async function initNonInteractive(projectRoot, configPath) {
3225
2803
  const exempted = getExemptedPackages(config);
3226
2804
  if (exempted.length > 0) {
3227
2805
  console.log(
3228
- ` ${chalk12.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk12.dim("(types-only)")}`
2806
+ ` ${chalk13.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk13.dim("(types-only)")}`
3229
2807
  );
3230
2808
  }
3231
2809
  if (config.packages.length > 1) {
3232
- const bs = clack8.spinner();
2810
+ const bs = clack3.spinner();
3233
2811
  bs.start("Building import graph...");
3234
2812
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3235
2813
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3262,14 +2840,14 @@ async function initNonInteractive(projectRoot, configPath) {
3262
2840
  const hookManager = detectHookManager(projectRoot);
3263
2841
  const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
3264
2842
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3265
- const ok = chalk12.green("\u2713");
2843
+ const ok = chalk13.green("\u2713");
3266
2844
  const created = [
3267
2845
  `${ok} ${path19.basename(configPath)}`,
3268
2846
  `${ok} .viberails/context.md`,
3269
2847
  `${ok} .viberails/scan-result.json`,
3270
2848
  `${ok} .claude/settings.json \u2014 added viberails hook`,
3271
2849
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
3272
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk12.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
2850
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk13.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3273
2851
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
3274
2852
  ].filter(Boolean);
3275
2853
  if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
@@ -3278,39 +2856,93 @@ async function initNonInteractive(projectRoot, configPath) {
3278
2856
  Created:
3279
2857
  ${created.map((f) => ` ${f}`).join("\n")}`);
3280
2858
  }
2859
+
2860
+ // src/commands/init.ts
2861
+ var CONFIG_FILE5 = "viberails.config.json";
2862
+ function getExemptedPackages2(config) {
2863
+ return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
2864
+ }
2865
+ async function initCommand(options, cwd) {
2866
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
2867
+ if (!projectRoot) {
2868
+ throw new Error(
2869
+ "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
2870
+ );
2871
+ }
2872
+ const configPath = path20.join(projectRoot, CONFIG_FILE5);
2873
+ if (fs20.existsSync(configPath) && !options.force) {
2874
+ if (!options.yes) {
2875
+ return initInteractive(projectRoot, configPath, options);
2876
+ }
2877
+ console.log(
2878
+ `${chalk14.yellow("!")} viberails is already initialized.
2879
+ Run ${chalk14.cyan("viberails")} to review or edit the existing setup, ${chalk14.cyan("viberails sync")} to update generated files, or ${chalk14.cyan("viberails init --force")} to replace it.`
2880
+ );
2881
+ return;
2882
+ }
2883
+ if (options.yes) return initNonInteractive(projectRoot, configPath);
2884
+ await initInteractive(projectRoot, configPath, options);
2885
+ }
3281
2886
  async function initInteractive(projectRoot, configPath, options) {
3282
- clack8.intro("viberails");
3283
- if (fs19.existsSync(configPath) && options.force) {
2887
+ clack4.intro("viberails");
2888
+ const replacingExistingConfig = fs20.existsSync(configPath);
2889
+ if (fs20.existsSync(configPath) && !options.force) {
2890
+ const action = await promptExistingConfigAction(path20.basename(configPath));
2891
+ if (action === "cancel") {
2892
+ clack4.outro("Aborted. No files were written.");
2893
+ return;
2894
+ }
2895
+ if (action === "edit") {
2896
+ await configCommand({ suppressIntro: true }, projectRoot);
2897
+ return;
2898
+ }
2899
+ options.force = true;
2900
+ }
2901
+ if (fs20.existsSync(configPath) && options.force) {
3284
2902
  const replace = await confirmDangerous(
3285
- `${path19.basename(configPath)} already exists and will be replaced. Continue?`
2903
+ `${path20.basename(configPath)} already exists and will be replaced. Continue?`
3286
2904
  );
3287
2905
  if (!replace) {
3288
- clack8.outro("Aborted. No files were written.");
2906
+ clack4.outro("Aborted. No files were written.");
3289
2907
  return;
3290
2908
  }
3291
2909
  }
3292
- const s = clack8.spinner();
2910
+ const s = clack4.spinner();
3293
2911
  s.start("Scanning project...");
3294
- const scanResult = await scan2(projectRoot);
3295
- const config = generateConfig(scanResult);
2912
+ const scanResult = await scan3(projectRoot);
2913
+ const config = generateConfig2(scanResult);
3296
2914
  s.stop("Scan complete");
3297
2915
  if (scanResult.statistics.totalFiles === 0) {
3298
- clack8.log.warn(
2916
+ clack4.log.warn(
3299
2917
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3300
2918
  );
3301
2919
  }
3302
- clack8.note(formatScanResultsText(scanResult), "Scan results");
3303
- const exemptedPkgs = getExemptedPackages(config);
3304
- displayInitSummary(config, exemptedPkgs);
3305
- const decision = await promptInitDecision();
2920
+ const exemptedPkgs = getExemptedPackages2(config);
2921
+ let decision;
2922
+ while (true) {
2923
+ displayInitOverview(scanResult, config, exemptedPkgs);
2924
+ const nextDecision = await promptInitDecision();
2925
+ if (nextDecision === "review") {
2926
+ clack4.note(formatScanResultsText(scanResult), "Detected details");
2927
+ continue;
2928
+ }
2929
+ decision = nextDecision;
2930
+ break;
2931
+ }
3306
2932
  if (decision === "customize") {
2933
+ const { resolveNamingDefault } = await import("./prompt-naming-default-AH54HEBC.js");
2934
+ await resolveNamingDefault(config, scanResult);
3307
2935
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
3308
2936
  const overrides = await promptRuleMenu({
3309
2937
  maxFileLines: config.rules.maxFileLines,
2938
+ maxTestFileLines: config.rules.maxTestFileLines,
3310
2939
  testCoverage: config.rules.testCoverage,
3311
2940
  enforceMissingTests: config.rules.enforceMissingTests,
3312
2941
  enforceNaming: config.rules.enforceNaming,
3313
2942
  fileNamingValue: rootPkg.conventions?.fileNaming,
2943
+ componentNaming: rootPkg.conventions?.componentNaming,
2944
+ hookNaming: rootPkg.conventions?.hookNaming,
2945
+ importAlias: rootPkg.conventions?.importAlias,
3314
2946
  coverageSummaryPath: "coverage/coverage-summary.json",
3315
2947
  coverageCommand: config.defaults?.coverage?.command,
3316
2948
  packageOverrides: config.packages
@@ -3318,13 +2950,13 @@ async function initInteractive(projectRoot, configPath, options) {
3318
2950
  applyRuleOverrides(config, overrides);
3319
2951
  }
3320
2952
  if (config.packages.length > 1) {
3321
- clack8.note(
3322
- "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
2953
+ clack4.note(
2954
+ "Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
3323
2955
  "Boundaries"
3324
2956
  );
3325
- const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
2957
+ const shouldInfer = await confirm("Infer boundary rules from current import patterns?");
3326
2958
  if (shouldInfer) {
3327
- const bs = clack8.spinner();
2959
+ const bs = clack4.spinner();
3328
2960
  bs.start("Building import graph...");
3329
2961
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3330
2962
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3345,7 +2977,7 @@ async function initInteractive(projectRoot, configPath, options) {
3345
2977
  const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
3346
2978
  const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
3347
2979
  if (hasMissingPrereqs) {
3348
- clack8.log.info("Some dependencies are needed for full functionality.");
2980
+ clack4.log.info("Some dependencies are needed for full functionality.");
3349
2981
  }
3350
2982
  const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
3351
2983
  if (prereqResult.disableCoverage) {
@@ -3358,46 +2990,50 @@ async function initInteractive(projectRoot, configPath, options) {
3358
2990
  packageManager: rootPkgStack?.packageManager?.split("@")[0],
3359
2991
  isWorkspace: config.packages.length > 1
3360
2992
  });
3361
- const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
2993
+ displaySetupPlan(config, integrations, {
2994
+ replacingExistingConfig,
2995
+ configFile: path20.basename(configPath)
2996
+ });
2997
+ const shouldWrite = await confirm("Apply this setup?");
3362
2998
  if (!shouldWrite) {
3363
- clack8.outro("Aborted. No files were written.");
2999
+ clack4.outro("Aborted. No files were written.");
3364
3000
  return;
3365
3001
  }
3366
- const ws = clack8.spinner();
3002
+ const ws = clack4.spinner();
3367
3003
  ws.start("Writing configuration...");
3368
- const compacted = compactConfig3(config);
3369
- fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3004
+ const compacted = compactConfig4(config);
3005
+ fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3370
3006
  `);
3371
3007
  writeGeneratedFiles(projectRoot, config, scanResult);
3372
3008
  updateGitignore(projectRoot);
3373
3009
  ws.stop("Configuration written");
3374
- const ok = chalk12.green("\u2713");
3375
- clack8.log.step(`${ok} ${path19.basename(configPath)}`);
3376
- clack8.log.step(`${ok} .viberails/context.md`);
3377
- clack8.log.step(`${ok} .viberails/scan-result.json`);
3010
+ const ok = chalk14.green("\u2713");
3011
+ clack4.log.step(`${ok} ${path20.basename(configPath)}`);
3012
+ clack4.log.step(`${ok} .viberails/context.md`);
3013
+ clack4.log.step(`${ok} .viberails/scan-result.json`);
3378
3014
  setupSelectedIntegrations(projectRoot, integrations, {
3379
3015
  linter: rootPkgStack?.linter?.split("@")[0],
3380
3016
  packageManager: rootPkgStack?.packageManager?.split("@")[0]
3381
3017
  });
3382
- clack8.outro(
3018
+ clack4.outro(
3383
3019
  `Done! Next: review viberails.config.json, then run viberails check
3384
- ${chalk12.dim("Tip: use")} ${chalk12.cyan("viberails check --enforce")} ${chalk12.dim("in CI to block PRs on violations.")}`
3020
+ ${chalk14.dim("Tip: use")} ${chalk14.cyan("viberails check --enforce")} ${chalk14.dim("in CI to block PRs on violations.")}`
3385
3021
  );
3386
3022
  }
3387
3023
 
3388
3024
  // src/commands/sync.ts
3389
- import * as fs20 from "fs";
3390
- import * as path20 from "path";
3391
- import * as clack9 from "@clack/prompts";
3392
- import { compactConfig as compactConfig4, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
3393
- import { scan as scan3 } from "@viberails/scanner";
3394
- import chalk13 from "chalk";
3025
+ import * as fs21 from "fs";
3026
+ import * as path21 from "path";
3027
+ import * as clack5 from "@clack/prompts";
3028
+ import { compactConfig as compactConfig5, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
3029
+ import { scan as scan4 } from "@viberails/scanner";
3030
+ import chalk15 from "chalk";
3395
3031
  var CONFIG_FILE6 = "viberails.config.json";
3396
3032
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
3397
3033
  function loadPreviousStats(projectRoot) {
3398
- const scanResultPath = path20.join(projectRoot, SCAN_RESULT_FILE2);
3034
+ const scanResultPath = path21.join(projectRoot, SCAN_RESULT_FILE2);
3399
3035
  try {
3400
- const raw = fs20.readFileSync(scanResultPath, "utf-8");
3036
+ const raw = fs21.readFileSync(scanResultPath, "utf-8");
3401
3037
  const parsed = JSON.parse(raw);
3402
3038
  if (parsed?.statistics?.totalFiles !== void 0) {
3403
3039
  return parsed.statistics;
@@ -3414,17 +3050,17 @@ async function syncCommand(options, cwd) {
3414
3050
  "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"
3415
3051
  );
3416
3052
  }
3417
- const configPath = path20.join(projectRoot, CONFIG_FILE6);
3053
+ const configPath = path21.join(projectRoot, CONFIG_FILE6);
3418
3054
  const existing = await loadConfig5(configPath);
3419
3055
  const previousStats = loadPreviousStats(projectRoot);
3420
- const s = clack9.spinner();
3056
+ const s = clack5.spinner();
3421
3057
  s.start("Scanning project...");
3422
- const scanResult = await scan3(projectRoot);
3058
+ const scanResult = await scan4(projectRoot);
3423
3059
  s.stop("Scan complete");
3424
3060
  const merged = mergeConfig2(existing, scanResult);
3425
- const compacted = compactConfig4(merged);
3061
+ const compacted = compactConfig5(merged);
3426
3062
  const compactedJson = JSON.stringify(compacted, null, 2);
3427
- const rawDisk = fs20.readFileSync(configPath, "utf-8").trim();
3063
+ const rawDisk = fs21.readFileSync(configPath, "utf-8").trim();
3428
3064
  const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3429
3065
  const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3430
3066
  const configChanged = diskWithoutSync !== mergedWithoutSync;
@@ -3432,19 +3068,19 @@ async function syncCommand(options, cwd) {
3432
3068
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
3433
3069
  if (changes.length > 0 || statsDelta) {
3434
3070
  console.log(`
3435
- ${chalk13.bold("Changes:")}`);
3071
+ ${chalk15.bold("Changes:")}`);
3436
3072
  for (const change of changes) {
3437
- const icon = change.type === "removed" ? chalk13.red("-") : chalk13.green("+");
3073
+ const icon = change.type === "removed" ? chalk15.red("-") : chalk15.green("+");
3438
3074
  console.log(` ${icon} ${change.description}`);
3439
3075
  }
3440
3076
  if (statsDelta) {
3441
- console.log(` ${chalk13.dim(statsDelta)}`);
3077
+ console.log(` ${chalk15.dim(statsDelta)}`);
3442
3078
  }
3443
3079
  }
3444
3080
  if (options?.interactive) {
3445
- clack9.intro("viberails sync (interactive)");
3446
- clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
3447
- const decision = await clack9.select({
3081
+ clack5.intro("viberails sync (interactive)");
3082
+ clack5.note(formatRulesText(merged).join("\n"), "Rules after sync");
3083
+ const decision = await clack5.select({
3448
3084
  message: "How would you like to proceed?",
3449
3085
  options: [
3450
3086
  { value: "accept", label: "Accept changes" },
@@ -3454,47 +3090,51 @@ ${chalk13.bold("Changes:")}`);
3454
3090
  });
3455
3091
  assertNotCancelled(decision);
3456
3092
  if (decision === "cancel") {
3457
- clack9.outro("Sync cancelled. No files were written.");
3093
+ clack5.outro("Sync cancelled. No files were written.");
3458
3094
  return;
3459
3095
  }
3460
3096
  if (decision === "customize") {
3461
3097
  const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
3462
3098
  const overrides = await promptRuleMenu({
3463
3099
  maxFileLines: merged.rules.maxFileLines,
3100
+ maxTestFileLines: merged.rules.maxTestFileLines,
3464
3101
  testCoverage: merged.rules.testCoverage,
3465
3102
  enforceMissingTests: merged.rules.enforceMissingTests,
3466
3103
  enforceNaming: merged.rules.enforceNaming,
3467
3104
  fileNamingValue: rootPkg.conventions?.fileNaming,
3105
+ componentNaming: rootPkg.conventions?.componentNaming,
3106
+ hookNaming: rootPkg.conventions?.hookNaming,
3107
+ importAlias: rootPkg.conventions?.importAlias,
3468
3108
  coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3469
3109
  coverageCommand: merged.defaults?.coverage?.command,
3470
3110
  packageOverrides: merged.packages
3471
3111
  });
3472
3112
  applyRuleOverrides(merged, overrides);
3473
- const recompacted = compactConfig4(merged);
3474
- fs20.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3113
+ const recompacted = compactConfig5(merged);
3114
+ fs21.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3475
3115
  `);
3476
3116
  writeGeneratedFiles(projectRoot, merged, scanResult);
3477
- clack9.log.success("Updated config with your customizations.");
3478
- clack9.outro("Done! Run viberails check to verify.");
3117
+ clack5.log.success("Updated config with your customizations.");
3118
+ clack5.outro("Done! Run viberails check to verify.");
3479
3119
  return;
3480
3120
  }
3481
3121
  }
3482
- fs20.writeFileSync(configPath, `${compactedJson}
3122
+ fs21.writeFileSync(configPath, `${compactedJson}
3483
3123
  `);
3484
3124
  writeGeneratedFiles(projectRoot, merged, scanResult);
3485
3125
  console.log(`
3486
- ${chalk13.bold("Synced:")}`);
3126
+ ${chalk15.bold("Synced:")}`);
3487
3127
  if (configChanged) {
3488
- console.log(` ${chalk13.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
3128
+ console.log(` ${chalk15.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
3489
3129
  } else {
3490
- console.log(` ${chalk13.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
3130
+ console.log(` ${chalk15.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
3491
3131
  }
3492
- console.log(` ${chalk13.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3493
- console.log(` ${chalk13.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3132
+ console.log(` ${chalk15.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3133
+ console.log(` ${chalk15.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3494
3134
  }
3495
3135
 
3496
3136
  // src/index.ts
3497
- var VERSION = "0.6.3";
3137
+ var VERSION = "0.6.5";
3498
3138
  var program = new Command();
3499
3139
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
3500
3140
  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) => {
@@ -3502,7 +3142,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
3502
3142
  await initCommand(options);
3503
3143
  } catch (err) {
3504
3144
  const message = err instanceof Error ? err.message : String(err);
3505
- console.error(`${chalk14.red("Error:")} ${message}`);
3145
+ console.error(`${chalk16.red("Error:")} ${message}`);
3506
3146
  process.exit(1);
3507
3147
  }
3508
3148
  });
@@ -3511,7 +3151,7 @@ program.command("sync").description("Re-scan and update generated files").option
3511
3151
  await syncCommand(options);
3512
3152
  } catch (err) {
3513
3153
  const message = err instanceof Error ? err.message : String(err);
3514
- console.error(`${chalk14.red("Error:")} ${message}`);
3154
+ console.error(`${chalk16.red("Error:")} ${message}`);
3515
3155
  process.exit(1);
3516
3156
  }
3517
3157
  });
@@ -3520,7 +3160,7 @@ program.command("config").description("Interactively edit existing config rules"
3520
3160
  await configCommand(options);
3521
3161
  } catch (err) {
3522
3162
  const message = err instanceof Error ? err.message : String(err);
3523
- console.error(`${chalk14.red("Error:")} ${message}`);
3163
+ console.error(`${chalk16.red("Error:")} ${message}`);
3524
3164
  process.exit(1);
3525
3165
  }
3526
3166
  });
@@ -3541,7 +3181,7 @@ program.command("check").description("Check files against enforced rules").optio
3541
3181
  process.exit(exitCode);
3542
3182
  } catch (err) {
3543
3183
  const message = err instanceof Error ? err.message : String(err);
3544
- console.error(`${chalk14.red("Error:")} ${message}`);
3184
+ console.error(`${chalk16.red("Error:")} ${message}`);
3545
3185
  process.exit(1);
3546
3186
  }
3547
3187
  }
@@ -3552,7 +3192,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
3552
3192
  process.exit(exitCode);
3553
3193
  } catch (err) {
3554
3194
  const message = err instanceof Error ? err.message : String(err);
3555
- console.error(`${chalk14.red("Error:")} ${message}`);
3195
+ console.error(`${chalk16.red("Error:")} ${message}`);
3556
3196
  process.exit(1);
3557
3197
  }
3558
3198
  });
@@ -3561,7 +3201,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
3561
3201
  await boundariesCommand(options);
3562
3202
  } catch (err) {
3563
3203
  const message = err instanceof Error ? err.message : String(err);
3564
- console.error(`${chalk14.red("Error:")} ${message}`);
3204
+ console.error(`${chalk16.red("Error:")} ${message}`);
3565
3205
  process.exit(1);
3566
3206
  }
3567
3207
  });