viberails 0.4.0 → 0.5.1

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_chalk10 = __toESM(require("chalk"), 1);
37
+ var import_chalk13 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -61,183 +61,537 @@ function findProjectRoot(startDir) {
61
61
  }
62
62
 
63
63
  // src/utils/prompt.ts
64
+ var clack5 = __toESM(require("@clack/prompts"), 1);
65
+
66
+ // src/utils/prompt-integrations.ts
67
+ var import_node_child_process = require("child_process");
64
68
  var clack = __toESM(require("@clack/prompts"), 1);
65
- function assertNotCancelled(value) {
66
- if (clack.isCancel(value)) {
67
- clack.cancel("Setup cancelled.");
68
- process.exit(0);
69
- }
70
- }
71
- async function confirm2(message) {
72
- const result = await clack.confirm({ message, initialValue: true });
73
- assertNotCancelled(result);
74
- return result;
75
- }
76
- async function confirmDangerous(message) {
77
- const result = await clack.confirm({ message, initialValue: false });
78
- assertNotCancelled(result);
79
- return result;
80
- }
81
- async function promptInitDecision() {
82
- const result = await clack.select({
83
- message: "Accept these settings?",
69
+ async function promptHookManagerInstall(projectRoot, packageManager) {
70
+ const choice = await clack.select({
71
+ message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
84
72
  options: [
85
- { value: "accept", label: "Yes, looks good", hint: "recommended" },
86
- { value: "customize", label: "Let me customize" }
73
+ {
74
+ value: "install",
75
+ label: "Yes, install Lefthook",
76
+ hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
77
+ },
78
+ {
79
+ value: "skip",
80
+ label: "No, skip",
81
+ hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
82
+ }
87
83
  ]
88
84
  });
85
+ assertNotCancelled(choice);
86
+ if (choice !== "install") return void 0;
87
+ const pm = packageManager || "npm";
88
+ const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? "pnpm add -D lefthook" : "npm install -D lefthook";
89
+ const s = clack.spinner();
90
+ s.start("Installing Lefthook...");
91
+ const result = (0, import_node_child_process.spawnSync)(installCmd, {
92
+ cwd: projectRoot,
93
+ shell: true,
94
+ encoding: "utf-8",
95
+ stdio: "pipe"
96
+ });
97
+ if (result.status === 0) {
98
+ const fs20 = await import("fs");
99
+ const path20 = await import("path");
100
+ const lefthookPath = path20.join(projectRoot, "lefthook.yml");
101
+ if (!fs20.existsSync(lefthookPath)) {
102
+ fs20.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
103
+ }
104
+ s.stop("Installed Lefthook");
105
+ return "Lefthook";
106
+ }
107
+ s.stop("Failed to install Lefthook");
108
+ clack.log.warn(`Install manually: ${installCmd}`);
109
+ return void 0;
110
+ }
111
+ async function promptIntegrations(projectRoot, hookManager, tools) {
112
+ let resolvedHookManager = hookManager;
113
+ if (!resolvedHookManager) {
114
+ resolvedHookManager = await promptHookManagerInstall(
115
+ projectRoot,
116
+ tools?.packageManager ?? "npm"
117
+ );
118
+ }
119
+ const isBareHook = !resolvedHookManager;
120
+ const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
121
+ const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
122
+ const options = [
123
+ {
124
+ value: "preCommit",
125
+ label: hookLabel,
126
+ hint: hookHint
127
+ }
128
+ ];
129
+ if (tools?.isTypeScript) {
130
+ options.push({
131
+ value: "typecheck",
132
+ label: "Typecheck (tsc --noEmit)",
133
+ hint: "catches type errors before commit"
134
+ });
135
+ }
136
+ if (tools?.linter) {
137
+ const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
138
+ options.push({
139
+ value: "lint",
140
+ label: `Lint check (${linterName})`,
141
+ hint: "runs linter on staged files before commit"
142
+ });
143
+ }
144
+ options.push(
145
+ {
146
+ value: "claude",
147
+ label: "Claude Code hook",
148
+ hint: "checks files when Claude edits them"
149
+ },
150
+ {
151
+ value: "claudeMd",
152
+ label: "CLAUDE.md reference",
153
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
154
+ },
155
+ {
156
+ value: "githubAction",
157
+ label: "GitHub Actions workflow",
158
+ hint: "blocks PRs that fail viberails check"
159
+ }
160
+ );
161
+ const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
162
+ const result = await clack.multiselect({
163
+ message: "Set up integrations?",
164
+ options,
165
+ initialValues,
166
+ required: false
167
+ });
89
168
  assertNotCancelled(result);
90
- return result;
169
+ return {
170
+ preCommitHook: result.includes("preCommit"),
171
+ claudeCodeHook: result.includes("claude"),
172
+ claudeMdRef: result.includes("claudeMd"),
173
+ githubAction: result.includes("githubAction"),
174
+ typecheckHook: result.includes("typecheck"),
175
+ lintHook: result.includes("lint")
176
+ };
91
177
  }
92
- async function promptRuleMenu(defaults) {
93
- const state = { ...defaults };
94
- while (true) {
95
- const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
96
- const enforcementHint = state.enforcement === "warn" ? "warn \u2014 violations shown but commits allowed" : "enforce \u2014 commits blocked on violation";
97
- const options = [
98
- { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
99
- {
100
- value: "requireTests",
101
- label: "Require test files",
102
- hint: state.requireTests ? "yes" : "no"
103
- },
104
- { value: "enforceNaming", label: "Enforce file naming", hint: namingHint },
105
- { value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
106
- ];
107
- if (state.packageOverrides && state.packageOverrides.length > 0) {
108
- const count = state.packageOverrides.length;
109
- options.push({
110
- value: "packageOverrides",
111
- label: "Per-package overrides",
112
- hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
113
- });
178
+
179
+ // src/utils/prompt-rules.ts
180
+ var clack4 = __toESM(require("@clack/prompts"), 1);
181
+
182
+ // src/utils/prompt-menu-handlers.ts
183
+ var clack3 = __toESM(require("@clack/prompts"), 1);
184
+
185
+ // src/utils/prompt-package-overrides.ts
186
+ var clack2 = __toESM(require("@clack/prompts"), 1);
187
+ function normalizePackageOverrides(packages) {
188
+ for (const pkg of packages) {
189
+ if (pkg.rules && Object.keys(pkg.rules).length === 0) {
190
+ delete pkg.rules;
114
191
  }
115
- options.push({ value: "done", label: "Done" });
116
- const choice = await clack.select({
117
- message: "Customize rules",
118
- options
192
+ if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
193
+ delete pkg.coverage;
194
+ }
195
+ }
196
+ return packages;
197
+ }
198
+ function packageCoverageHint(pkg, defaults) {
199
+ const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
200
+ const isExempt = coverage === 0;
201
+ const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
202
+ const defaultCommand = defaults.coverageCommand ?? "";
203
+ const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
204
+ const tags = [];
205
+ const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
206
+ const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
207
+ tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
208
+ if (hasSummaryOverride) tags.push("summary override");
209
+ if (hasCommandOverride) tags.push("command override");
210
+ return tags.join(", ");
211
+ }
212
+ async function promptPackageCoverageOverrides(packages, defaults) {
213
+ const editablePackages = packages.filter((pkg) => pkg.path !== ".");
214
+ if (editablePackages.length === 0) return packages;
215
+ while (true) {
216
+ const selectedPath = await clack2.select({
217
+ message: "Select package to edit coverage overrides",
218
+ options: [
219
+ ...editablePackages.map((pkg) => ({
220
+ value: pkg.path,
221
+ label: `${pkg.path} (${pkg.name})`,
222
+ hint: packageCoverageHint(pkg, defaults)
223
+ })),
224
+ { value: "__done__", label: "Done" }
225
+ ]
119
226
  });
120
- assertNotCancelled(choice);
121
- if (choice === "done") break;
122
- if (choice === "packageOverrides" && state.packageOverrides) {
123
- const lines = state.packageOverrides.map((pkg) => {
124
- const diffs = [];
125
- if (pkg.conventions) {
126
- for (const [key, val] of Object.entries(pkg.conventions)) {
127
- const v = typeof val === "string" ? val : val?.value;
128
- if (v) diffs.push(`${key}: ${v}`);
227
+ assertNotCancelled(selectedPath);
228
+ if (selectedPath === "__done__") break;
229
+ const target = editablePackages.find((pkg) => pkg.path === selectedPath);
230
+ if (!target) continue;
231
+ while (true) {
232
+ const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
233
+ const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
234
+ const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
235
+ const choice = await clack2.select({
236
+ message: `Edit coverage overrides for ${target.path}`,
237
+ options: [
238
+ { value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
239
+ { value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
240
+ { value: "command", label: "coverage.command", hint: effectiveCommand },
241
+ { value: "reset", label: "Reset this package to inherit defaults" },
242
+ { value: "back", label: "Back to package list" }
243
+ ]
244
+ });
245
+ assertNotCancelled(choice);
246
+ if (choice === "back") break;
247
+ if (choice === "testCoverage") {
248
+ const result = await clack2.text({
249
+ message: "Package testCoverage (0 to exempt package)?",
250
+ initialValue: String(effectiveCoverage),
251
+ validate: (v) => {
252
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
253
+ const n = Number.parseInt(v, 10);
254
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
129
255
  }
256
+ });
257
+ assertNotCancelled(result);
258
+ const nextCoverage = Number.parseInt(result, 10);
259
+ if (nextCoverage === defaults.testCoverage) {
260
+ if (target.rules) {
261
+ delete target.rules.testCoverage;
262
+ }
263
+ } else {
264
+ target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
130
265
  }
131
- if (pkg.stack) {
132
- for (const [key, val] of Object.entries(pkg.stack)) {
133
- if (val) diffs.push(`${key}: ${val}`);
266
+ }
267
+ if (choice === "summaryPath") {
268
+ const result = await clack2.text({
269
+ message: "Package coverage.summaryPath (blank to inherit default)?",
270
+ initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
271
+ placeholder: defaults.coverageSummaryPath
272
+ });
273
+ assertNotCancelled(result);
274
+ const value = result.trim();
275
+ if (value.length === 0 || value === defaults.coverageSummaryPath) {
276
+ if (target.coverage) {
277
+ delete target.coverage.summaryPath;
134
278
  }
279
+ } else {
280
+ target.coverage = { ...target.coverage ?? {}, summaryPath: value };
135
281
  }
136
- return `${pkg.path}
137
- ${diffs.join(", ") || "minor differences"}`;
138
- });
139
- clack.note(
140
- `${lines.join("\n\n")}
141
-
142
- Edit the "packages" section in viberails.config.json to adjust.`,
143
- "Per-package overrides"
144
- );
145
- continue;
146
- }
147
- if (choice === "maxFileLines") {
148
- const result = await clack.text({
149
- message: "Maximum lines per source file?",
150
- initialValue: String(state.maxFileLines),
151
- validate: (v) => {
152
- const n = Number.parseInt(v, 10);
153
- if (Number.isNaN(n) || n < 1) return "Enter a positive number";
282
+ }
283
+ if (choice === "command") {
284
+ const result = await clack2.text({
285
+ message: "Package coverage.command (blank to inherit default/auto)?",
286
+ initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
287
+ placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
288
+ });
289
+ assertNotCancelled(result);
290
+ const value = result.trim();
291
+ const defaultCommand = defaults.coverageCommand ?? "";
292
+ if (value.length === 0 || value === defaultCommand) {
293
+ if (target.coverage) {
294
+ delete target.coverage.command;
295
+ }
296
+ } else {
297
+ target.coverage = { ...target.coverage ?? {}, command: value };
154
298
  }
155
- });
156
- assertNotCancelled(result);
157
- state.maxFileLines = Number.parseInt(result, 10);
299
+ }
300
+ if (choice === "reset") {
301
+ if (target.rules) {
302
+ delete target.rules.testCoverage;
303
+ }
304
+ delete target.coverage;
305
+ }
306
+ normalizePackageOverrides(editablePackages);
158
307
  }
159
- if (choice === "requireTests") {
160
- const result = await clack.confirm({
161
- message: "Require matching test files for source files?",
162
- initialValue: state.requireTests
163
- });
164
- assertNotCancelled(result);
165
- state.requireTests = result;
308
+ }
309
+ return normalizePackageOverrides(packages);
310
+ }
311
+
312
+ // src/utils/prompt-menu-handlers.ts
313
+ function getPackageDiffs(pkg, root) {
314
+ const diffs = [];
315
+ const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
316
+ for (const key of convKeys) {
317
+ if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
318
+ diffs.push(`${key}: ${pkg.conventions[key]}`);
319
+ }
320
+ }
321
+ const stackKeys = [
322
+ "framework",
323
+ "language",
324
+ "styling",
325
+ "backend",
326
+ "orm",
327
+ "linter",
328
+ "formatter",
329
+ "testRunner",
330
+ "packageManager"
331
+ ];
332
+ for (const key of stackKeys) {
333
+ if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
334
+ diffs.push(`${key}: ${pkg.stack[key]}`);
166
335
  }
167
- if (choice === "enforceNaming") {
168
- const result = await clack.confirm({
169
- message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
170
- initialValue: state.enforceNaming
336
+ }
337
+ if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
338
+ diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
339
+ }
340
+ if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
341
+ diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
342
+ }
343
+ if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
344
+ diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
345
+ }
346
+ if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
347
+ diffs.push("coverage.command: (override)");
348
+ }
349
+ return diffs;
350
+ }
351
+ function buildMenuOptions(state, packageCount) {
352
+ const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
353
+ const options = [
354
+ { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
355
+ { value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
356
+ ];
357
+ if (state.fileNamingValue) {
358
+ options.push({
359
+ value: "fileNaming",
360
+ label: "File naming convention",
361
+ hint: state.fileNamingValue
362
+ });
363
+ }
364
+ options.push({
365
+ value: "testCoverage",
366
+ label: "Test coverage target",
367
+ hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
368
+ });
369
+ options.push({
370
+ value: "enforceMissingTests",
371
+ label: "Enforce missing tests",
372
+ hint: state.enforceMissingTests ? "yes" : "no"
373
+ });
374
+ if (state.testCoverage > 0) {
375
+ options.push(
376
+ {
377
+ value: "coverageSummaryPath",
378
+ label: "Coverage summary path",
379
+ hint: state.coverageSummaryPath
380
+ },
381
+ {
382
+ value: "coverageCommand",
383
+ label: "Coverage command",
384
+ hint: state.coverageCommand ?? "auto-detect from package.json test runner"
385
+ }
386
+ );
387
+ if (packageCount > 0) {
388
+ options.push({
389
+ value: "packageOverrides",
390
+ label: "Per-package coverage overrides",
391
+ hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
171
392
  });
172
- assertNotCancelled(result);
173
- state.enforceNaming = result;
174
393
  }
175
- if (choice === "enforcement") {
176
- const result = await clack.select({
177
- message: "Enforcement mode",
178
- options: [
179
- {
180
- value: "warn",
181
- label: "warn",
182
- hint: "show violations but don't block commits (recommended)"
183
- },
184
- {
185
- value: "enforce",
186
- label: "enforce",
187
- hint: "block commits with violations"
188
- }
189
- ],
190
- initialValue: state.enforcement
394
+ }
395
+ options.push(
396
+ { value: "reset", label: "Reset all to detected defaults" },
397
+ { value: "done", label: "Done" }
398
+ );
399
+ return options;
400
+ }
401
+ function clonePackages(packages) {
402
+ return packages?.map((pkg) => ({
403
+ ...pkg,
404
+ stack: pkg.stack ? { ...pkg.stack } : void 0,
405
+ structure: pkg.structure ? { ...pkg.structure } : void 0,
406
+ conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
407
+ rules: pkg.rules ? { ...pkg.rules } : void 0,
408
+ coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
409
+ ignore: pkg.ignore ? [...pkg.ignore] : void 0,
410
+ boundaries: pkg.boundaries ? {
411
+ deny: [...pkg.boundaries.deny],
412
+ ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
413
+ } : void 0
414
+ }));
415
+ }
416
+ async function handleMenuChoice(choice, state, defaults, root) {
417
+ if (choice === "reset") {
418
+ state.maxFileLines = defaults.maxFileLines;
419
+ state.testCoverage = defaults.testCoverage;
420
+ state.enforceMissingTests = defaults.enforceMissingTests;
421
+ state.enforceNaming = defaults.enforceNaming;
422
+ state.fileNamingValue = defaults.fileNamingValue;
423
+ state.coverageSummaryPath = defaults.coverageSummaryPath;
424
+ state.coverageCommand = defaults.coverageCommand;
425
+ state.packageOverrides = clonePackages(defaults.packageOverrides);
426
+ clack3.log.info("Reset all rules to detected defaults.");
427
+ return;
428
+ }
429
+ if (choice === "packageOverrides") {
430
+ if (state.packageOverrides) {
431
+ const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
432
+ state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
433
+ testCoverage: state.testCoverage,
434
+ coverageSummaryPath: state.coverageSummaryPath,
435
+ coverageCommand: state.coverageCommand
191
436
  });
192
- assertNotCancelled(result);
193
- state.enforcement = result;
437
+ const lines = packageDiffs.map((entry) => `${entry.pkg.path}
438
+ ${entry.diffs.join(", ")}`);
439
+ if (lines.length > 0) {
440
+ clack3.note(lines.join("\n\n"), "Existing package differences");
441
+ }
194
442
  }
443
+ return;
444
+ }
445
+ if (choice === "maxFileLines") {
446
+ const result = await clack3.text({
447
+ message: "Maximum lines per source file?",
448
+ initialValue: String(state.maxFileLines),
449
+ validate: (v) => {
450
+ if (typeof v !== "string") return "Enter a positive number";
451
+ const n = Number.parseInt(v, 10);
452
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
453
+ }
454
+ });
455
+ assertNotCancelled(result);
456
+ state.maxFileLines = Number.parseInt(result, 10);
457
+ }
458
+ if (choice === "enforceMissingTests") {
459
+ const result = await clack3.confirm({
460
+ message: "Require every source file to have a corresponding test file?",
461
+ initialValue: state.enforceMissingTests
462
+ });
463
+ assertNotCancelled(result);
464
+ state.enforceMissingTests = result;
465
+ }
466
+ if (choice === "testCoverage") {
467
+ const result = await clack3.text({
468
+ message: "Test coverage target (0 disables coverage checks)?",
469
+ initialValue: String(state.testCoverage),
470
+ validate: (v) => {
471
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
472
+ const n = Number.parseInt(v, 10);
473
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
474
+ }
475
+ });
476
+ assertNotCancelled(result);
477
+ state.testCoverage = Number.parseInt(result, 10);
478
+ }
479
+ if (choice === "coverageSummaryPath") {
480
+ const result = await clack3.text({
481
+ message: "Coverage summary path (relative to package root)?",
482
+ initialValue: state.coverageSummaryPath,
483
+ validate: (v) => {
484
+ if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
485
+ }
486
+ });
487
+ assertNotCancelled(result);
488
+ state.coverageSummaryPath = result.trim();
489
+ }
490
+ if (choice === "coverageCommand") {
491
+ const result = await clack3.text({
492
+ message: "Coverage command (blank to auto-detect from package.json)?",
493
+ initialValue: state.coverageCommand ?? "",
494
+ placeholder: "(auto-detect from package.json test runner)"
495
+ });
496
+ assertNotCancelled(result);
497
+ const trimmed = result.trim();
498
+ state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
499
+ }
500
+ if (choice === "enforceNaming") {
501
+ const result = await clack3.confirm({
502
+ message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
503
+ initialValue: state.enforceNaming
504
+ });
505
+ assertNotCancelled(result);
506
+ state.enforceNaming = result;
507
+ }
508
+ if (choice === "fileNaming") {
509
+ const selected = await clack3.select({
510
+ message: "Which file naming convention should be enforced?",
511
+ options: [
512
+ { value: "kebab-case", label: "kebab-case" },
513
+ { value: "camelCase", label: "camelCase" },
514
+ { value: "PascalCase", label: "PascalCase" },
515
+ { value: "snake_case", label: "snake_case" }
516
+ ],
517
+ initialValue: state.fileNamingValue
518
+ });
519
+ assertNotCancelled(selected);
520
+ state.fileNamingValue = selected;
521
+ }
522
+ }
523
+
524
+ // src/utils/prompt-rules.ts
525
+ function getRootPackage(packages) {
526
+ return packages.find((pkg) => pkg.path === ".") ?? packages[0];
527
+ }
528
+ async function promptRuleMenu(defaults) {
529
+ const state = {
530
+ ...defaults,
531
+ packageOverrides: clonePackages(defaults.packageOverrides)
532
+ };
533
+ const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
534
+ const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
535
+ while (true) {
536
+ const options = buildMenuOptions(state, packageCount);
537
+ const choice = await clack4.select({ message: "Customize rules", options });
538
+ assertNotCancelled(choice);
539
+ if (choice === "done") break;
540
+ await handleMenuChoice(choice, state, defaults, root);
195
541
  }
196
542
  return {
197
543
  maxFileLines: state.maxFileLines,
198
- requireTests: state.requireTests,
544
+ testCoverage: state.testCoverage,
545
+ enforceMissingTests: state.enforceMissingTests,
199
546
  enforceNaming: state.enforceNaming,
200
- enforcement: state.enforcement
547
+ fileNamingValue: state.fileNamingValue,
548
+ coverageSummaryPath: state.coverageSummaryPath,
549
+ coverageCommand: state.coverageCommand,
550
+ packageOverrides: state.packageOverrides
201
551
  };
202
552
  }
203
- async function promptIntegrations(hookManager) {
204
- const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
205
- const result = await clack.multiselect({
206
- message: "Set up integrations?",
553
+
554
+ // src/utils/prompt.ts
555
+ function assertNotCancelled(value) {
556
+ if (clack5.isCancel(value)) {
557
+ clack5.cancel("Setup cancelled.");
558
+ process.exit(0);
559
+ }
560
+ }
561
+ async function confirm3(message) {
562
+ const result = await clack5.confirm({ message, initialValue: true });
563
+ assertNotCancelled(result);
564
+ return result;
565
+ }
566
+ async function confirmDangerous(message) {
567
+ const result = await clack5.confirm({ message, initialValue: false });
568
+ assertNotCancelled(result);
569
+ return result;
570
+ }
571
+ async function promptInitDecision() {
572
+ const result = await clack5.select({
573
+ message: "Accept these rules?",
207
574
  options: [
208
575
  {
209
- value: "preCommit",
210
- label: hookLabel,
211
- hint: "runs checks when you commit"
576
+ value: "accept",
577
+ label: "Yes, looks good",
578
+ hint: "warns on violation; use --enforce in CI to block"
212
579
  },
213
- {
214
- value: "claude",
215
- label: "Claude Code hook",
216
- hint: "checks files when Claude edits them"
217
- },
218
- {
219
- value: "claudeMd",
220
- label: "CLAUDE.md reference",
221
- hint: "appends @.viberails/context.md so Claude loads rules automatically"
222
- }
223
- ],
224
- initialValues: ["preCommit", "claude", "claudeMd"],
225
- required: false
580
+ { value: "customize", label: "Let me customize rules" }
581
+ ]
226
582
  });
227
583
  assertNotCancelled(result);
228
- return {
229
- preCommitHook: result.includes("preCommit"),
230
- claudeCodeHook: result.includes("claude"),
231
- claudeMdRef: result.includes("claudeMd")
232
- };
584
+ return result;
233
585
  }
234
586
 
235
587
  // src/utils/resolve-workspace-packages.ts
236
588
  var fs2 = __toESM(require("fs"), 1);
237
589
  var path2 = __toESM(require("path"), 1);
238
- function resolveWorkspacePackages(projectRoot, workspace) {
239
- const packages = [];
240
- for (const relativePath of workspace.packages) {
590
+ function resolveWorkspacePackages(projectRoot, packages) {
591
+ const resolved = [];
592
+ for (const pkgConfig of packages) {
593
+ if (pkgConfig.path === ".") continue;
594
+ const relativePath = pkgConfig.path;
241
595
  const absPath = path2.join(projectRoot, relativePath);
242
596
  const pkgJsonPath = path2.join(absPath, "package.json");
243
597
  if (!fs2.existsSync(pkgJsonPath)) continue;
@@ -253,13 +607,13 @@ function resolveWorkspacePackages(projectRoot, workspace) {
253
607
  ...Object.keys(pkg.dependencies ?? {}),
254
608
  ...Object.keys(pkg.devDependencies ?? {})
255
609
  ];
256
- packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
610
+ resolved.push({ name, path: absPath, relativePath, internalDeps: allDeps });
257
611
  }
258
- const packageNames = new Set(packages.map((p) => p.name));
259
- for (const pkg of packages) {
612
+ const packageNames = new Set(resolved.map((p) => p.name));
613
+ for (const pkg of resolved) {
260
614
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
261
615
  }
262
- return packages;
616
+ return resolved;
263
617
  }
264
618
 
265
619
  // src/commands/boundaries.ts
@@ -310,7 +664,7 @@ Enforcement: ${config.rules.enforceBoundaries ? import_chalk.default.green("on")
310
664
  async function inferAndDisplay(projectRoot, config, configPath) {
311
665
  console.log(import_chalk.default.dim("Analyzing imports..."));
312
666
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
313
- const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
667
+ const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
314
668
  const graph = await buildImportGraph(projectRoot, {
315
669
  packages,
316
670
  ignore: config.ignore
@@ -334,11 +688,11 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
334
688
  console.log(`
335
689
  ${totalRules} denied`);
336
690
  console.log("");
337
- const shouldSave = await confirm2("Save to viberails.config.json?");
691
+ const shouldSave = await confirm3("Save to viberails.config.json?");
338
692
  if (shouldSave) {
339
693
  config.boundaries = inferred;
340
694
  config.rules.enforceBoundaries = true;
341
- fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
695
+ fs3.writeFileSync(configPath, `${JSON.stringify((0, import_config.compactConfig)(config), null, 2)}
342
696
  `);
343
697
  console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
344
698
  }
@@ -346,7 +700,7 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
346
700
  async function showGraph(projectRoot, config) {
347
701
  console.log(import_chalk.default.dim("Building import graph..."));
348
702
  const { buildImportGraph } = await import("@viberails/graph");
349
- const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
703
+ const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
350
704
  const graph = await buildImportGraph(projectRoot, {
351
705
  packages,
352
706
  ignore: config.ignore
@@ -374,42 +728,194 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
374
728
  }
375
729
 
376
730
  // src/commands/check.ts
377
- var fs6 = __toESM(require("fs"), 1);
378
- var path6 = __toESM(require("path"), 1);
379
- var import_config2 = require("@viberails/config");
731
+ var fs7 = __toESM(require("fs"), 1);
732
+ var path7 = __toESM(require("path"), 1);
733
+ var import_config5 = require("@viberails/config");
380
734
  var import_chalk2 = __toESM(require("chalk"), 1);
381
735
 
382
736
  // src/commands/check-config.ts
737
+ var import_config2 = require("@viberails/config");
383
738
  function resolveConfigForFile(relPath, config) {
384
- if (!config.packages || config.packages.length === 0) {
385
- return { rules: config.rules, conventions: config.conventions };
386
- }
387
739
  const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
388
740
  for (const pkg of sortedPackages) {
741
+ if (pkg.path === ".") continue;
389
742
  if (relPath.startsWith(`${pkg.path}/`) || relPath === pkg.path) {
390
743
  return {
391
744
  rules: { ...config.rules, ...pkg.rules },
392
- conventions: { ...config.conventions, ...pkg.conventions }
745
+ conventions: pkg.conventions ?? {},
746
+ coverage: {
747
+ ...config.defaults?.coverage ?? {},
748
+ ...pkg.coverage ?? {}
749
+ }
393
750
  };
394
751
  }
395
752
  }
396
- return { rules: config.rules, conventions: config.conventions };
753
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
754
+ return {
755
+ rules: { ...config.rules, ...root.rules },
756
+ conventions: root.conventions ?? {},
757
+ coverage: {
758
+ ...config.defaults?.coverage ?? {},
759
+ ...root.coverage ?? {}
760
+ }
761
+ };
762
+ }
763
+ function getEffectiveIgnore(config) {
764
+ return [...import_config2.BUILTIN_IGNORE, ...config.ignore ?? []];
397
765
  }
398
766
  function resolveIgnoreForFile(relPath, config) {
399
- const globalIgnore = config.ignore;
400
- if (!config.packages) return globalIgnore;
401
- for (const pkg of config.packages) {
402
- if (pkg.ignore && relPath.startsWith(`${pkg.path}/`)) {
403
- return [...globalIgnore, ...pkg.ignore];
404
- }
767
+ const base = getEffectiveIgnore(config);
768
+ const root = config.packages.find((p) => p.path === ".");
769
+ const withRoot = root?.ignore ? [...base, ...root.ignore] : base;
770
+ const matched = [...config.packages].filter((p) => p.path !== ".").sort((a, b) => b.path.length - a.path.length).find((p) => relPath.startsWith(`${p.path}/`) || relPath === p.path);
771
+ if (matched?.ignore) {
772
+ return [...withRoot, ...matched.ignore];
405
773
  }
406
- return globalIgnore;
774
+ return withRoot;
407
775
  }
408
776
 
409
- // src/commands/check-files.ts
410
- var import_node_child_process = require("child_process");
777
+ // src/commands/check-coverage.ts
778
+ var import_node_child_process2 = require("child_process");
411
779
  var fs4 = __toESM(require("fs"), 1);
412
780
  var path4 = __toESM(require("path"), 1);
781
+ var import_config3 = require("@viberails/config");
782
+ var DEFAULT_SUMMARY_PATH = "coverage/coverage-summary.json";
783
+ function packageRoot(projectRoot, pkg) {
784
+ return pkg.path === "." ? projectRoot : path4.join(projectRoot, pkg.path);
785
+ }
786
+ function resolveForPackage(config, pkg) {
787
+ return {
788
+ pkg,
789
+ rules: { ...config.rules, ...pkg.rules },
790
+ coverage: {
791
+ ...config.defaults?.coverage ?? {},
792
+ ...pkg.coverage ?? {}
793
+ }
794
+ };
795
+ }
796
+ function resolveCoveragePackages(projectRoot, config, filesToCheck, staged) {
797
+ if (!staged) {
798
+ return config.packages.map((pkg) => resolveForPackage(config, pkg));
799
+ }
800
+ const matched = /* @__PURE__ */ new Map();
801
+ for (const raw of filesToCheck) {
802
+ const relPath = path4.isAbsolute(raw) ? path4.relative(projectRoot, raw) : raw;
803
+ const sorted = [...config.packages].filter((pkg2) => pkg2.path !== ".").sort((a, b) => b.path.length - a.path.length);
804
+ const pkg = sorted.find(
805
+ (candidate) => relPath.startsWith(`${candidate.path}/`) || relPath === candidate.path
806
+ ) ?? config.packages.find((candidate) => candidate.path === ".") ?? config.packages[0];
807
+ matched.set(pkg.path, resolveForPackage(config, pkg));
808
+ }
809
+ return [...matched.values()];
810
+ }
811
+ function readCoveragePercentage(summaryPath) {
812
+ try {
813
+ const parsed = JSON.parse(fs4.readFileSync(summaryPath, "utf-8"));
814
+ const pct = parsed.total?.lines?.pct;
815
+ return typeof pct === "number" ? pct : void 0;
816
+ } catch {
817
+ return void 0;
818
+ }
819
+ }
820
+ function runCoverageCommand(pkgRoot, command) {
821
+ const result = (0, import_node_child_process2.spawnSync)(command, {
822
+ cwd: pkgRoot,
823
+ shell: true,
824
+ encoding: "utf-8",
825
+ stdio: "pipe"
826
+ });
827
+ if (result.status === 0) return { ok: true };
828
+ const stderr = result.stderr?.trim() ?? "";
829
+ const stdout = result.stdout?.trim() ?? "";
830
+ const raw = stderr || stdout || `exit code ${result.status ?? 1}`;
831
+ if (raw.includes("coverage-v8") || raw.includes("coverage-istanbul") || raw.includes("MISSING DEP")) {
832
+ return {
833
+ ok: false,
834
+ detail: "Missing coverage provider. Install with: npm install -D @vitest/coverage-v8"
835
+ };
836
+ }
837
+ const detail = raw.replace(/\x1B\[[0-9;]*m/g, "");
838
+ return { ok: false, detail };
839
+ }
840
+ function violationFilePath(projectRoot, pkgRoot, summaryPath) {
841
+ return path4.relative(projectRoot, path4.join(pkgRoot, summaryPath));
842
+ }
843
+ function pushViolation(violations, file, message, severity) {
844
+ violations.push({
845
+ file,
846
+ rule: "test-coverage",
847
+ message,
848
+ severity
849
+ });
850
+ }
851
+ function checkCoverage(projectRoot, config, filesToCheck, options) {
852
+ const severity = options.enforce ? "error" : "warn";
853
+ const targets = resolveCoveragePackages(
854
+ projectRoot,
855
+ config,
856
+ filesToCheck,
857
+ options.staged === true
858
+ );
859
+ const violations = [];
860
+ for (const target of targets) {
861
+ if (target.rules.testCoverage <= 0) continue;
862
+ if (!target.pkg.stack?.testRunner) continue;
863
+ const pkgRoot = packageRoot(projectRoot, target.pkg);
864
+ const summaryPath = target.coverage.summaryPath ?? DEFAULT_SUMMARY_PATH;
865
+ const summaryAbs = path4.join(pkgRoot, summaryPath);
866
+ const summaryRel = violationFilePath(projectRoot, pkgRoot, summaryPath);
867
+ let pct = readCoveragePercentage(summaryAbs);
868
+ if (pct === void 0 && !options.staged) {
869
+ const command = target.coverage.command ?? (0, import_config3.inferCoverageCommand)(target.pkg.stack.testRunner);
870
+ if (!command) {
871
+ const pkgLabel = target.pkg.path === "." ? "root package" : target.pkg.path;
872
+ pushViolation(
873
+ violations,
874
+ summaryRel,
875
+ `No coverage summary found for "${pkgLabel}". Run your test suite with coverage enabled, or set defaults.coverage.command in viberails.config.json.`,
876
+ severity
877
+ );
878
+ continue;
879
+ }
880
+ options.onProgress?.(target.pkg.path === "." ? "root" : target.pkg.path);
881
+ const run = runCoverageCommand(pkgRoot, command);
882
+ if (!run.ok) {
883
+ pushViolation(
884
+ violations,
885
+ summaryRel,
886
+ `Failed to run coverage command: ${run.detail}.`,
887
+ severity
888
+ );
889
+ continue;
890
+ }
891
+ pct = readCoveragePercentage(summaryAbs);
892
+ }
893
+ if (pct === void 0) {
894
+ pushViolation(
895
+ violations,
896
+ summaryRel,
897
+ `Coverage summary not found or invalid at \`${summaryPath}\`.`,
898
+ severity
899
+ );
900
+ continue;
901
+ }
902
+ if (pct < target.rules.testCoverage) {
903
+ pushViolation(
904
+ violations,
905
+ summaryRel,
906
+ `Line coverage ${pct.toFixed(1)}% is below required ${target.rules.testCoverage}%.`,
907
+ severity
908
+ );
909
+ }
910
+ }
911
+ return violations;
912
+ }
913
+
914
+ // src/commands/check-files.ts
915
+ var import_node_child_process3 = require("child_process");
916
+ var fs5 = __toESM(require("fs"), 1);
917
+ var path5 = __toESM(require("path"), 1);
918
+ var import_config4 = require("@viberails/config");
413
919
  var import_picomatch = __toESM(require("picomatch"), 1);
414
920
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
415
921
  "node_modules",
@@ -452,7 +958,7 @@ function isIgnored(relPath, ignorePatterns) {
452
958
  }
453
959
  function countFileLines(filePath) {
454
960
  try {
455
- const content = fs4.readFileSync(filePath, "utf-8");
961
+ const content = fs5.readFileSync(filePath, "utf-8");
456
962
  if (content.length === 0) return 0;
457
963
  let count = 1;
458
964
  for (let i = 0; i < content.length; i++) {
@@ -464,14 +970,14 @@ function countFileLines(filePath) {
464
970
  }
465
971
  }
466
972
  function checkNaming(relPath, conventions) {
467
- const filename = path4.basename(relPath);
468
- const ext = path4.extname(filename);
973
+ const filename = path5.basename(relPath);
974
+ const ext = path5.extname(filename);
469
975
  if (!SOURCE_EXTS.has(ext)) return void 0;
470
976
  if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".") || filename.startsWith("_") || filename.startsWith("+") || filename.startsWith("$") || filename.startsWith("[")) {
471
977
  return void 0;
472
978
  }
473
979
  const bare = filename.slice(0, filename.indexOf("."));
474
- const convention = typeof conventions.fileNaming === "string" ? conventions.fileNaming : conventions.fileNaming?.value;
980
+ const convention = conventions.fileNaming;
475
981
  if (!convention) return void 0;
476
982
  const pattern = NAMING_PATTERNS[convention];
477
983
  if (!pattern || pattern.test(bare)) return void 0;
@@ -479,35 +985,57 @@ function checkNaming(relPath, conventions) {
479
985
  }
480
986
  function getStagedFiles(projectRoot) {
481
987
  try {
482
- const output = (0, import_node_child_process.execSync)("git diff --cached --name-only --diff-filter=ACM", {
988
+ const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACM", {
483
989
  cwd: projectRoot,
484
- encoding: "utf-8"
990
+ encoding: "utf-8",
991
+ stdio: ["ignore", "pipe", "ignore"]
485
992
  });
486
993
  return output.trim().split("\n").filter(Boolean);
487
994
  } catch {
488
995
  return [];
489
996
  }
490
997
  }
998
+ function getDiffFiles(projectRoot, base) {
999
+ try {
1000
+ const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
1001
+ cwd: projectRoot,
1002
+ encoding: "utf-8",
1003
+ stdio: ["ignore", "pipe", "ignore"]
1004
+ });
1005
+ const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
1006
+ cwd: projectRoot,
1007
+ encoding: "utf-8",
1008
+ stdio: ["ignore", "pipe", "ignore"]
1009
+ });
1010
+ return {
1011
+ all: allOutput.trim().split("\n").filter(Boolean),
1012
+ added: addedOutput.trim().split("\n").filter(Boolean)
1013
+ };
1014
+ } catch {
1015
+ return { all: [], added: [] };
1016
+ }
1017
+ }
491
1018
  function getAllSourceFiles(projectRoot, config) {
1019
+ const effectiveIgnore = [...import_config4.BUILTIN_IGNORE, ...config.ignore ?? []];
492
1020
  const files = [];
493
1021
  const walk = (dir) => {
494
1022
  let entries;
495
1023
  try {
496
- entries = fs4.readdirSync(dir, { withFileTypes: true });
1024
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
497
1025
  } catch {
498
1026
  return;
499
1027
  }
500
1028
  for (const entry of entries) {
501
- const rel = path4.relative(projectRoot, path4.join(dir, entry.name));
1029
+ const rel = path5.relative(projectRoot, path5.join(dir, entry.name));
502
1030
  if (entry.isDirectory()) {
503
1031
  if (ALWAYS_SKIP_DIRS.has(entry.name)) {
504
1032
  continue;
505
1033
  }
506
- if (isIgnored(rel, config.ignore)) continue;
507
- walk(path4.join(dir, entry.name));
1034
+ if (isIgnored(rel, effectiveIgnore)) continue;
1035
+ walk(path5.join(dir, entry.name));
508
1036
  } else if (entry.isFile()) {
509
- const ext = path4.extname(entry.name);
510
- if (SOURCE_EXTS.has(ext) && !isIgnored(rel, config.ignore)) {
1037
+ const ext = path5.extname(entry.name);
1038
+ if (SOURCE_EXTS.has(ext) && !isIgnored(rel, effectiveIgnore)) {
511
1039
  files.push(rel);
512
1040
  }
513
1041
  }
@@ -521,16 +1049,16 @@ function collectSourceFiles(dir, projectRoot) {
521
1049
  const walk = (d) => {
522
1050
  let entries;
523
1051
  try {
524
- entries = fs4.readdirSync(d, { withFileTypes: true });
1052
+ entries = fs5.readdirSync(d, { withFileTypes: true });
525
1053
  } catch {
526
1054
  return;
527
1055
  }
528
1056
  for (const entry of entries) {
529
1057
  if (entry.isDirectory()) {
530
1058
  if (entry.name === "node_modules") continue;
531
- walk(path4.join(d, entry.name));
1059
+ walk(path5.join(d, entry.name));
532
1060
  } else if (entry.isFile()) {
533
- files.push(path4.relative(projectRoot, path4.join(d, entry.name)));
1061
+ files.push(path5.relative(projectRoot, path5.join(d, entry.name)));
534
1062
  }
535
1063
  }
536
1064
  };
@@ -539,8 +1067,8 @@ function collectSourceFiles(dir, projectRoot) {
539
1067
  }
540
1068
 
541
1069
  // src/commands/check-tests.ts
542
- var fs5 = __toESM(require("fs"), 1);
543
- var path5 = __toESM(require("path"), 1);
1070
+ var fs6 = __toESM(require("fs"), 1);
1071
+ var path6 = __toESM(require("path"), 1);
544
1072
  var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
545
1073
  ".ts",
546
1074
  ".tsx",
@@ -554,44 +1082,58 @@ var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
554
1082
  ]);
555
1083
  function checkMissingTests(projectRoot, config, severity) {
556
1084
  const violations = [];
557
- const { testPattern } = config.structure;
558
- if (!testPattern) return violations;
559
- const srcDir = config.structure.srcDir;
560
- if (!srcDir) return violations;
561
- const srcPath = path5.join(projectRoot, srcDir);
562
- if (!fs5.existsSync(srcPath)) return violations;
563
- const testSuffix = testPattern.replace("*", "");
564
- const sourceFiles = collectSourceFiles(srcPath, projectRoot);
565
- for (const relFile of sourceFiles) {
566
- const basename7 = path5.basename(relFile);
567
- if (basename7.includes(".test.") || basename7.includes(".spec.") || basename7.startsWith("index.") || basename7.endsWith(".d.ts")) {
568
- continue;
569
- }
570
- const ext = path5.extname(basename7);
571
- if (!SOURCE_EXTS2.has(ext)) continue;
572
- const stem = basename7.slice(0, basename7.indexOf("."));
573
- const expectedTestFile = `${stem}${testSuffix}`;
574
- const dir = path5.dirname(path5.join(projectRoot, relFile));
575
- const colocatedTest = path5.join(dir, expectedTestFile);
576
- const testsDir = config.structure.tests;
577
- const dedicatedTest = testsDir ? path5.join(projectRoot, testsDir, expectedTestFile) : null;
578
- const hasTest = fs5.existsSync(colocatedTest) || dedicatedTest !== null && fs5.existsSync(dedicatedTest);
579
- if (!hasTest) {
580
- violations.push({
581
- file: relFile,
582
- rule: "missing-test",
583
- message: `No test file found. Expected \`${expectedTestFile}\`.`,
584
- severity
585
- });
1085
+ for (const pkg of config.packages) {
1086
+ const effectiveRules = { ...config.rules, ...pkg.rules };
1087
+ const enforceMissing = effectiveRules.enforceMissingTests ?? effectiveRules.testCoverage > 0;
1088
+ if (!enforceMissing) continue;
1089
+ const testPattern = pkg.structure?.testPattern;
1090
+ const srcDir = pkg.structure?.srcDir;
1091
+ if (!testPattern || !srcDir) continue;
1092
+ const packageRoot2 = pkg.path === "." ? projectRoot : path6.join(projectRoot, pkg.path);
1093
+ const srcPath = path6.join(packageRoot2, srcDir);
1094
+ if (!fs6.existsSync(srcPath)) continue;
1095
+ const testSuffix = testPattern.replace("*", "");
1096
+ const sourceFiles = collectSourceFiles(srcPath, projectRoot);
1097
+ for (const relFile of sourceFiles) {
1098
+ const basename8 = path6.basename(relFile);
1099
+ if (basename8.includes(".test.") || basename8.includes(".spec.") || basename8.startsWith("index.") || basename8.endsWith(".d.ts")) {
1100
+ continue;
1101
+ }
1102
+ const ext = path6.extname(basename8);
1103
+ if (!SOURCE_EXTS2.has(ext)) continue;
1104
+ const stem = basename8.slice(0, -ext.length);
1105
+ const expectedTestFile = `${stem}${testSuffix}`;
1106
+ const dir = path6.dirname(path6.join(projectRoot, relFile));
1107
+ const colocatedTest = path6.join(dir, expectedTestFile);
1108
+ const testsDir = pkg.structure?.tests;
1109
+ const dedicatedTest = testsDir ? path6.join(packageRoot2, testsDir, expectedTestFile) : null;
1110
+ const hasTest = fs6.existsSync(colocatedTest) || dedicatedTest !== null && fs6.existsSync(dedicatedTest);
1111
+ if (!hasTest) {
1112
+ violations.push({
1113
+ file: relFile,
1114
+ rule: "missing-test",
1115
+ message: `No test file found. Expected \`${expectedTestFile}\`.`,
1116
+ severity
1117
+ });
1118
+ }
586
1119
  }
587
1120
  }
588
1121
  return violations;
589
1122
  }
590
-
591
- // src/commands/check.ts
1123
+ function resolvePackageForFile(sourceRelPath, config) {
1124
+ const sorted = [...config.packages].filter((p) => p.path !== ".").sort((a, b) => b.path.length - a.path.length);
1125
+ for (const pkg of sorted) {
1126
+ if (sourceRelPath.startsWith(`${pkg.path}/`) || sourceRelPath === pkg.path) {
1127
+ return pkg;
1128
+ }
1129
+ }
1130
+ return config.packages.find((p) => p.path === ".") ?? config.packages[0];
1131
+ }
1132
+
1133
+ // src/commands/check.ts
592
1134
  var CONFIG_FILE2 = "viberails.config.json";
593
1135
  function isTestFile(relPath) {
594
- const filename = path6.basename(relPath);
1136
+ const filename = path7.basename(relPath);
595
1137
  return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
596
1138
  }
597
1139
  function printGroupedViolations(violations, limit) {
@@ -601,7 +1143,13 @@ function printGroupedViolations(violations, limit) {
601
1143
  existing.push(v);
602
1144
  groups.set(v.rule, existing);
603
1145
  }
604
- const ruleOrder = ["file-size", "file-naming", "missing-test", "boundary-violation"];
1146
+ const ruleOrder = [
1147
+ "file-size",
1148
+ "file-naming",
1149
+ "missing-test",
1150
+ "test-coverage",
1151
+ "boundary-violation"
1152
+ ];
605
1153
  const sortedKeys = [...groups.keys()].sort(
606
1154
  (a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
607
1155
  );
@@ -641,17 +1189,22 @@ async function checkCommand(options, cwd) {
641
1189
  console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
642
1190
  return 1;
643
1191
  }
644
- const configPath = path6.join(projectRoot, CONFIG_FILE2);
645
- if (!fs6.existsSync(configPath)) {
1192
+ const configPath = path7.join(projectRoot, CONFIG_FILE2);
1193
+ if (!fs7.existsSync(configPath)) {
646
1194
  console.error(
647
1195
  `${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
648
1196
  );
649
1197
  return 1;
650
1198
  }
651
- const config = await (0, import_config2.loadConfig)(configPath);
1199
+ const config = await (0, import_config5.loadConfig)(configPath);
652
1200
  let filesToCheck;
1201
+ let diffAddedFiles = null;
653
1202
  if (options.staged) {
654
1203
  filesToCheck = getStagedFiles(projectRoot);
1204
+ } else if (options.diffBase) {
1205
+ const diff = getDiffFiles(projectRoot, options.diffBase);
1206
+ filesToCheck = diff.all.filter((f) => SOURCE_EXTS.has(path7.extname(f)));
1207
+ diffAddedFiles = new Set(diff.added);
655
1208
  } else if (options.files && options.files.length > 0) {
656
1209
  filesToCheck = options.files;
657
1210
  } else {
@@ -659,22 +1212,23 @@ async function checkCommand(options, cwd) {
659
1212
  }
660
1213
  if (filesToCheck.length === 0) {
661
1214
  if (options.format === "json") {
662
- console.log(
663
- JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
664
- );
1215
+ console.log(JSON.stringify({ violations: [], checkedFiles: 0 }));
665
1216
  } else {
666
1217
  console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
667
1218
  }
668
1219
  return 0;
669
1220
  }
670
1221
  const violations = [];
671
- const severity = config.enforcement === "enforce" ? "error" : "warn";
1222
+ const severity = options.enforce ? "error" : "warn";
1223
+ const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(import_chalk2.default.dim(msg)) : () => {
1224
+ };
1225
+ log7(" Checking files...");
672
1226
  for (const file of filesToCheck) {
673
- const absPath = path6.isAbsolute(file) ? file : path6.join(projectRoot, file);
674
- const relPath = path6.relative(projectRoot, absPath);
1227
+ const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1228
+ const relPath = path7.relative(projectRoot, absPath);
675
1229
  const effectiveIgnore = resolveIgnoreForFile(relPath, config);
676
1230
  if (isIgnored(relPath, effectiveIgnore)) continue;
677
- if (!fs6.existsSync(absPath)) continue;
1231
+ if (!fs7.existsSync(absPath)) continue;
678
1232
  const resolved = resolveConfigForFile(relPath, config);
679
1233
  const testFile = isTestFile(relPath);
680
1234
  const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
@@ -701,23 +1255,38 @@ async function checkCommand(options, cwd) {
701
1255
  }
702
1256
  }
703
1257
  }
704
- if (config.rules.requireTests && !options.staged && !options.files) {
1258
+ log7(" done\n");
1259
+ if (!options.staged && !options.files) {
1260
+ log7(" Checking missing tests...");
705
1261
  const testViolations = checkMissingTests(projectRoot, config, severity);
706
- violations.push(...testViolations);
1262
+ violations.push(
1263
+ ...diffAddedFiles ? testViolations.filter((v) => diffAddedFiles.has(v.file)) : testViolations
1264
+ );
1265
+ log7(" done\n");
1266
+ }
1267
+ if (!options.files && !options.staged && !options.diffBase) {
1268
+ log7(" Running test coverage...\n");
1269
+ const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1270
+ staged: options.staged,
1271
+ enforce: options.enforce,
1272
+ onProgress: (pkg) => log7(` Coverage: ${pkg}...
1273
+ `)
1274
+ });
1275
+ violations.push(...coverageViolations);
707
1276
  }
708
1277
  if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
709
1278
  const startTime = Date.now();
710
1279
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
711
- const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
1280
+ const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
712
1281
  const graph = await buildImportGraph(projectRoot, {
713
1282
  packages,
714
1283
  ignore: config.ignore
715
1284
  });
716
1285
  const boundaryViolations = checkBoundaries(graph, config.boundaries);
717
- const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) => path6.resolve(projectRoot, f))) : null;
1286
+ const filterSet = options.staged || options.files || options.diffBase ? new Set(filesToCheck.map((f) => path7.resolve(projectRoot, f))) : null;
718
1287
  for (const bv of boundaryViolations) {
719
1288
  if (filterSet && !filterSet.has(bv.file)) continue;
720
- const relFile = path6.relative(projectRoot, bv.file);
1289
+ const relFile = path7.relative(projectRoot, bv.file);
721
1290
  violations.push({
722
1291
  file: relFile,
723
1292
  rule: "boundary-violation",
@@ -725,20 +1294,17 @@ async function checkCommand(options, cwd) {
725
1294
  severity
726
1295
  });
727
1296
  }
728
- const elapsed = Date.now() - startTime;
729
- if (options.format !== "json") {
730
- console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
731
- }
1297
+ log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1298
+ `);
732
1299
  }
733
1300
  if (options.format === "json") {
734
1301
  console.log(
735
1302
  JSON.stringify({
736
1303
  violations,
737
- checkedFiles: filesToCheck.length,
738
- enforcement: config.enforcement
1304
+ checkedFiles: filesToCheck.length
739
1305
  })
740
1306
  );
741
- return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
1307
+ return options.enforce && violations.length > 0 ? 1 : 0;
742
1308
  }
743
1309
  if (violations.length === 0) {
744
1310
  console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
@@ -748,7 +1314,7 @@ async function checkCommand(options, cwd) {
748
1314
  printGroupedViolations(violations, options.limit);
749
1315
  }
750
1316
  printSummary(violations);
751
- if (config.enforcement === "enforce") {
1317
+ if (options.enforce) {
752
1318
  console.log(import_chalk2.default.red("Fix violations before committing."));
753
1319
  return 1;
754
1320
  }
@@ -756,7 +1322,7 @@ async function checkCommand(options, cwd) {
756
1322
  }
757
1323
 
758
1324
  // src/commands/check-hook.ts
759
- var fs7 = __toESM(require("fs"), 1);
1325
+ var fs8 = __toESM(require("fs"), 1);
760
1326
  function parseHookFilePath(input) {
761
1327
  try {
762
1328
  if (!input.trim()) return void 0;
@@ -768,7 +1334,7 @@ function parseHookFilePath(input) {
768
1334
  }
769
1335
  function readStdin() {
770
1336
  try {
771
- return fs7.readFileSync(0, "utf-8");
1337
+ return fs8.readFileSync(0, "utf-8");
772
1338
  } catch {
773
1339
  return "";
774
1340
  }
@@ -801,484 +1367,133 @@ async function hookCheckCommand(cwd) {
801
1367
  }
802
1368
  }
803
1369
 
804
- // src/commands/fix.ts
1370
+ // src/commands/config.ts
805
1371
  var fs10 = __toESM(require("fs"), 1);
806
- var path10 = __toESM(require("path"), 1);
807
- var import_config3 = require("@viberails/config");
1372
+ var path9 = __toESM(require("path"), 1);
1373
+ var clack6 = __toESM(require("@clack/prompts"), 1);
1374
+ var import_config6 = require("@viberails/config");
1375
+ var import_scanner = require("@viberails/scanner");
1376
+ var import_chalk5 = __toESM(require("chalk"), 1);
1377
+
1378
+ // src/display-text.ts
1379
+ var import_types4 = require("@viberails/types");
1380
+
1381
+ // src/display.ts
1382
+ var import_types3 = require("@viberails/types");
808
1383
  var import_chalk4 = __toESM(require("chalk"), 1);
809
1384
 
810
- // src/commands/fix-helpers.ts
811
- var import_node_child_process2 = require("child_process");
812
- var import_chalk3 = __toESM(require("chalk"), 1);
813
- function printPlan(renames, stubs) {
814
- if (renames.length > 0) {
815
- console.log(import_chalk3.default.bold("\nFile renames:"));
816
- for (const r of renames) {
817
- console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
1385
+ // src/display-helpers.ts
1386
+ var import_types = require("@viberails/types");
1387
+ function groupByRole(directories) {
1388
+ const map = /* @__PURE__ */ new Map();
1389
+ for (const dir of directories) {
1390
+ if (dir.role === "unknown") continue;
1391
+ const existing = map.get(dir.role);
1392
+ if (existing) {
1393
+ existing.dirs.push(dir);
1394
+ } else {
1395
+ map.set(dir.role, { dirs: [dir] });
818
1396
  }
819
1397
  }
820
- if (stubs.length > 0) {
821
- console.log(import_chalk3.default.bold("\nTest stubs to create:"));
822
- for (const s of stubs) {
823
- console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
824
- }
1398
+ const groups = [];
1399
+ for (const [role, { dirs }] of map) {
1400
+ const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1401
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1402
+ groups.push({
1403
+ role,
1404
+ label,
1405
+ dirCount: dirs.length,
1406
+ totalFiles,
1407
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1408
+ });
825
1409
  }
1410
+ return groups;
826
1411
  }
827
- function checkGitDirty(projectRoot) {
828
- try {
829
- const output = (0, import_node_child_process2.execSync)("git status --porcelain", {
830
- cwd: projectRoot,
831
- encoding: "utf-8"
832
- });
833
- return output.trim().length > 0;
834
- } catch {
835
- return false;
1412
+ function formatSummary(stats, packageCount) {
1413
+ const parts = [];
1414
+ if (packageCount && packageCount > 1) {
1415
+ parts.push(`${packageCount} packages`);
836
1416
  }
1417
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1418
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1419
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1420
+ return parts.join(" \xB7 ");
837
1421
  }
838
- function getConventionValue(convention) {
839
- if (typeof convention === "string") return convention;
840
- if (convention && typeof convention === "object" && "value" in convention) {
841
- return convention.value;
1422
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1423
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1424
+ }
1425
+ function formatRoleGroup(group) {
1426
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1427
+ if (group.singlePath) {
1428
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
842
1429
  }
843
- return void 0;
1430
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1431
+ return `${group.label} \u2014 ${dirs} (${files})`;
844
1432
  }
845
1433
 
846
- // src/commands/fix-imports.ts
847
- var path7 = __toESM(require("path"), 1);
848
- function stripExtension(filePath) {
849
- return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
850
- }
851
- function computeNewSpecifier(oldSpecifier, newBare) {
852
- const hasJsExt = oldSpecifier.endsWith(".js");
853
- const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
854
- const dir = base.lastIndexOf("/");
855
- const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
856
- const newSpec = prefix + newBare;
857
- return hasJsExt ? `${newSpec}.js` : newSpec;
1434
+ // src/display-monorepo.ts
1435
+ var import_types2 = require("@viberails/types");
1436
+ var import_chalk3 = __toESM(require("chalk"), 1);
1437
+ function formatPackageSummary(pkg) {
1438
+ const parts = [];
1439
+ if (pkg.stack.framework) {
1440
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1441
+ }
1442
+ if (pkg.stack.styling) {
1443
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1444
+ }
1445
+ const files = `${pkg.statistics.totalFiles} files`;
1446
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1447
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
858
1448
  }
859
- async function updateImportsAfterRenames(renames, projectRoot) {
860
- if (renames.length === 0) return [];
861
- const { Project, SyntaxKind } = await import("ts-morph");
862
- const renameMap = /* @__PURE__ */ new Map();
863
- for (const r of renames) {
864
- const oldStripped = stripExtension(r.oldAbsPath);
865
- const newFilename = path7.basename(r.newPath);
866
- const newName = newFilename.slice(0, newFilename.indexOf("."));
867
- renameMap.set(oldStripped, { newBare: newName });
1449
+ function displayMonorepoResults(scanResult) {
1450
+ const { stack, packages } = scanResult;
1451
+ console.log(`
1452
+ ${import_chalk3.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1453
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.language)}`);
1454
+ if (stack.packageManager) {
1455
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
868
1456
  }
869
- const project = new Project({
870
- tsConfigFilePath: void 0,
871
- skipAddingFilesFromTsConfig: true
872
- });
873
- project.addSourceFilesAtPaths(path7.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
874
- const updates = [];
875
- const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
876
- for (const sourceFile of project.getSourceFiles()) {
877
- const filePath = sourceFile.getFilePath();
878
- const segments = filePath.split(path7.sep);
879
- if (segments.includes("node_modules") || segments.includes("dist")) continue;
880
- const fileDir = path7.dirname(filePath);
881
- for (const decl of sourceFile.getImportDeclarations()) {
882
- const specifier = decl.getModuleSpecifierValue();
883
- if (!specifier.startsWith(".")) continue;
884
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
885
- if (!match) continue;
886
- const newSpec = computeNewSpecifier(specifier, match.newBare);
887
- updates.push({
888
- file: filePath,
889
- oldSpecifier: specifier,
890
- newSpecifier: newSpec,
891
- line: decl.getStartLineNumber()
892
- });
893
- decl.setModuleSpecifier(newSpec);
894
- }
895
- for (const decl of sourceFile.getExportDeclarations()) {
896
- const specifier = decl.getModuleSpecifierValue();
897
- if (!specifier || !specifier.startsWith(".")) continue;
898
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
899
- if (!match) continue;
900
- const newSpec = computeNewSpecifier(specifier, match.newBare);
901
- updates.push({
902
- file: filePath,
903
- oldSpecifier: specifier,
904
- newSpecifier: newSpec,
905
- line: decl.getStartLineNumber()
906
- });
907
- decl.setModuleSpecifier(newSpec);
1457
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1458
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1459
+ } else {
1460
+ if (stack.linter) {
1461
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)}`);
908
1462
  }
909
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
910
- if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
911
- const args = call.getArguments();
912
- if (args.length === 0) continue;
913
- const arg = args[0];
914
- if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
915
- const specifier = arg.getText().slice(1, -1);
916
- if (!specifier.startsWith(".")) continue;
917
- const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
918
- if (!match) continue;
919
- const newSpec = computeNewSpecifier(specifier, match.newBare);
920
- updates.push({
921
- file: filePath,
922
- oldSpecifier: specifier,
923
- newSpecifier: newSpec,
924
- line: call.getStartLineNumber()
925
- });
926
- const quote = arg.getText()[0];
927
- arg.replaceWithText(`${quote}${newSpec}${quote}`);
1463
+ if (stack.formatter) {
1464
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.formatter)}`);
928
1465
  }
929
1466
  }
930
- if (updates.length > 0) {
931
- await project.save();
1467
+ if (stack.testRunner) {
1468
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
932
1469
  }
933
- return updates;
934
- }
935
- function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
936
- const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
937
- const resolved = path7.resolve(fromDir, cleanSpec);
938
- for (const ext of extensions) {
939
- const candidate = resolved + ext;
940
- const stripped = stripExtension(candidate);
941
- const match = renameMap.get(stripped);
942
- if (match) return match;
1470
+ console.log("");
1471
+ for (const pkg of packages) {
1472
+ console.log(formatPackageSummary(pkg));
943
1473
  }
944
- return void 0;
945
- }
946
-
947
- // src/commands/fix-naming.ts
948
- var fs8 = __toESM(require("fs"), 1);
949
- var path8 = __toESM(require("path"), 1);
950
-
951
- // src/commands/convert-name.ts
952
- function splitIntoWords(name) {
953
- const parts = name.split(/[-_]/);
954
- const words = [];
955
- for (const part of parts) {
956
- if (part === "") continue;
957
- let current = "";
958
- for (let i = 0; i < part.length; i++) {
959
- const ch = part[i];
960
- const isUpper = ch >= "A" && ch <= "Z";
961
- if (isUpper && current.length > 0) {
962
- const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
963
- const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
964
- if (!prevIsUpper || nextIsLower) {
965
- words.push(current.toLowerCase());
966
- current = "";
967
- }
1474
+ const packagesWithDirs = packages.filter(
1475
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1476
+ );
1477
+ if (packagesWithDirs.length > 0) {
1478
+ console.log(`
1479
+ ${import_chalk3.default.bold("Structure:")}`);
1480
+ for (const pkg of packagesWithDirs) {
1481
+ const groups = groupByRole(pkg.structure.directories);
1482
+ if (groups.length === 0) continue;
1483
+ console.log(` ${pkg.relativePath}:`);
1484
+ for (const group of groups) {
1485
+ console.log(` ${import_chalk3.default.green("\u2713")} ${formatRoleGroup(group)}`);
968
1486
  }
969
- current += ch;
970
1487
  }
971
- if (current) words.push(current.toLowerCase());
972
1488
  }
973
- return words;
1489
+ displayConventions(scanResult);
1490
+ displaySummarySection(scanResult);
1491
+ console.log("");
974
1492
  }
975
- function convertName(bare, target) {
976
- const words = splitIntoWords(bare);
977
- if (words.length === 0) return bare;
978
- switch (target) {
979
- case "kebab-case":
980
- return words.join("-");
981
- case "camelCase":
982
- return words[0] + words.slice(1).map(capitalize).join("");
983
- case "PascalCase":
984
- return words.map(capitalize).join("");
985
- case "snake_case":
986
- return words.join("_");
987
- default:
988
- return bare;
989
- }
990
- }
991
- function capitalize(word) {
992
- if (word.length === 0) return word;
993
- return word[0].toUpperCase() + word.slice(1);
994
- }
995
-
996
- // src/commands/fix-naming.ts
997
- function computeRename(relPath, targetConvention, projectRoot) {
998
- const filename = path8.basename(relPath);
999
- const dir = path8.dirname(relPath);
1000
- const dotIndex = filename.indexOf(".");
1001
- if (dotIndex === -1) return null;
1002
- const bare = filename.slice(0, dotIndex);
1003
- const suffix = filename.slice(dotIndex);
1004
- const newBare = convertName(bare, targetConvention);
1005
- if (newBare === bare) return null;
1006
- const newFilename = newBare + suffix;
1007
- const newRelPath = path8.join(dir, newFilename);
1008
- const oldAbsPath = path8.join(projectRoot, relPath);
1009
- const newAbsPath = path8.join(projectRoot, newRelPath);
1010
- if (fs8.existsSync(newAbsPath)) return null;
1011
- return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
1012
- }
1013
- function executeRename(rename) {
1014
- if (fs8.existsSync(rename.newAbsPath)) return false;
1015
- fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
1016
- return true;
1017
- }
1018
- function deduplicateRenames(renames) {
1019
- const seen = /* @__PURE__ */ new Set();
1020
- const result = [];
1021
- for (const r of renames) {
1022
- if (seen.has(r.newAbsPath)) continue;
1023
- seen.add(r.newAbsPath);
1024
- result.push(r);
1025
- }
1026
- return result;
1027
- }
1028
-
1029
- // src/commands/fix-tests.ts
1030
- var fs9 = __toESM(require("fs"), 1);
1031
- var path9 = __toESM(require("path"), 1);
1032
- function generateTestStub(sourceRelPath, config, projectRoot) {
1033
- const { testPattern } = config.structure;
1034
- if (!testPattern) return null;
1035
- const basename7 = path9.basename(sourceRelPath);
1036
- const stem = basename7.slice(0, basename7.indexOf("."));
1037
- const testSuffix = testPattern.replace("*", "");
1038
- const testFilename = `${stem}${testSuffix}`;
1039
- const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
1040
- const testAbsPath = path9.join(dir, testFilename);
1041
- if (fs9.existsSync(testAbsPath)) return null;
1042
- return {
1043
- path: path9.relative(projectRoot, testAbsPath),
1044
- absPath: testAbsPath,
1045
- moduleName: stem
1046
- };
1047
- }
1048
- function writeTestStub(stub, config) {
1049
- const runner = config.stack.testRunner === "jest" ? "jest" : "vitest";
1050
- const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
1051
- const content = `${importLine}describe('${stub.moduleName}', () => {
1052
- it.todo('add tests');
1053
- });
1054
- `;
1055
- fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
1056
- fs9.writeFileSync(stub.absPath, content);
1057
- }
1058
-
1059
- // src/commands/fix.ts
1060
- var CONFIG_FILE3 = "viberails.config.json";
1061
- async function fixCommand(options, cwd) {
1062
- const startDir = cwd ?? process.cwd();
1063
- const projectRoot = findProjectRoot(startDir);
1064
- if (!projectRoot) {
1065
- console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
1066
- return 1;
1067
- }
1068
- const configPath = path10.join(projectRoot, CONFIG_FILE3);
1069
- if (!fs10.existsSync(configPath)) {
1070
- console.error(
1071
- `${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
1072
- );
1073
- return 1;
1074
- }
1075
- const config = await (0, import_config3.loadConfig)(configPath);
1076
- if (!options.dryRun) {
1077
- const isDirty = checkGitDirty(projectRoot);
1078
- if (isDirty) {
1079
- console.log(
1080
- import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
1081
- );
1082
- }
1083
- }
1084
- const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
1085
- const shouldFixTests = !options.rule || options.rule.includes("missing-test");
1086
- const allFiles = getAllSourceFiles(projectRoot, config);
1087
- const renames = [];
1088
- if (shouldFixNaming) {
1089
- for (const file of allFiles) {
1090
- const resolved = resolveConfigForFile(file, config);
1091
- if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
1092
- const violation = checkNaming(file, resolved.conventions);
1093
- if (!violation) continue;
1094
- const convention = getConventionValue(resolved.conventions.fileNaming);
1095
- if (!convention) continue;
1096
- const rename = computeRename(file, convention, projectRoot);
1097
- if (rename) renames.push(rename);
1098
- }
1099
- }
1100
- const dedupedRenames = deduplicateRenames(renames);
1101
- const testStubs = [];
1102
- if (shouldFixTests && config.rules.requireTests) {
1103
- const testViolations = checkMissingTests(projectRoot, config, "warn");
1104
- for (const v of testViolations) {
1105
- const stub = generateTestStub(v.file, config, projectRoot);
1106
- if (stub) testStubs.push(stub);
1107
- }
1108
- }
1109
- if (dedupedRenames.length === 0 && testStubs.length === 0) {
1110
- console.log(`${import_chalk4.default.green("\u2713")} No fixable violations found.`);
1111
- return 0;
1112
- }
1113
- printPlan(dedupedRenames, testStubs);
1114
- if (options.dryRun) {
1115
- console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
1116
- return 0;
1117
- }
1118
- if (!options.yes) {
1119
- const confirmed = await confirmDangerous("Apply these fixes?");
1120
- if (!confirmed) {
1121
- console.log("Aborted.");
1122
- return 0;
1123
- }
1124
- }
1125
- let renameCount = 0;
1126
- for (const rename of dedupedRenames) {
1127
- if (executeRename(rename)) {
1128
- renameCount++;
1129
- }
1130
- }
1131
- let importUpdateCount = 0;
1132
- if (renameCount > 0) {
1133
- const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
1134
- const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
1135
- importUpdateCount = updates.length;
1136
- }
1137
- let stubCount = 0;
1138
- for (const stub of testStubs) {
1139
- if (!fs10.existsSync(stub.absPath)) {
1140
- writeTestStub(stub, config);
1141
- stubCount++;
1142
- }
1143
- }
1144
- console.log("");
1145
- if (renameCount > 0) {
1146
- console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
1147
- }
1148
- if (importUpdateCount > 0) {
1149
- console.log(
1150
- `${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
1151
- );
1152
- }
1153
- if (stubCount > 0) {
1154
- console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
1155
- }
1156
- return 0;
1157
- }
1158
-
1159
- // src/commands/init.ts
1160
- var fs13 = __toESM(require("fs"), 1);
1161
- var path13 = __toESM(require("path"), 1);
1162
- var clack2 = __toESM(require("@clack/prompts"), 1);
1163
- var import_config4 = require("@viberails/config");
1164
- var import_scanner = require("@viberails/scanner");
1165
- var import_chalk8 = __toESM(require("chalk"), 1);
1166
-
1167
- // src/display-text.ts
1168
- var import_types4 = require("@viberails/types");
1169
-
1170
- // src/display-helpers.ts
1171
- var import_types = require("@viberails/types");
1172
- function groupByRole(directories) {
1173
- const map = /* @__PURE__ */ new Map();
1174
- for (const dir of directories) {
1175
- if (dir.role === "unknown") continue;
1176
- const existing = map.get(dir.role);
1177
- if (existing) {
1178
- existing.dirs.push(dir);
1179
- } else {
1180
- map.set(dir.role, { dirs: [dir] });
1181
- }
1182
- }
1183
- const groups = [];
1184
- for (const [role, { dirs }] of map) {
1185
- const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1186
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1187
- groups.push({
1188
- role,
1189
- label,
1190
- dirCount: dirs.length,
1191
- totalFiles,
1192
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
1193
- });
1194
- }
1195
- return groups;
1196
- }
1197
- function formatSummary(stats, packageCount) {
1198
- const parts = [];
1199
- if (packageCount && packageCount > 1) {
1200
- parts.push(`${packageCount} packages`);
1201
- }
1202
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1203
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1204
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1205
- return parts.join(" \xB7 ");
1206
- }
1207
- function formatExtensions(filesByExtension, maxEntries = 4) {
1208
- return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1209
- }
1210
- function formatRoleGroup(group) {
1211
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1212
- if (group.singlePath) {
1213
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1214
- }
1215
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1216
- return `${group.label} \u2014 ${dirs} (${files})`;
1217
- }
1218
-
1219
- // src/display.ts
1220
- var import_types3 = require("@viberails/types");
1221
- var import_chalk6 = __toESM(require("chalk"), 1);
1222
-
1223
- // src/display-monorepo.ts
1224
- var import_types2 = require("@viberails/types");
1225
- var import_chalk5 = __toESM(require("chalk"), 1);
1226
- function formatPackageSummary(pkg) {
1227
- const parts = [];
1228
- if (pkg.stack.framework) {
1229
- parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1230
- }
1231
- if (pkg.stack.styling) {
1232
- parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1233
- }
1234
- const files = `${pkg.statistics.totalFiles} files`;
1235
- const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1236
- return ` ${pkg.relativePath} \u2014 ${detail}`;
1237
- }
1238
- function displayMonorepoResults(scanResult) {
1239
- const { stack, packages } = scanResult;
1240
- console.log(`
1241
- ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1242
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
1243
- if (stack.packageManager) {
1244
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1245
- }
1246
- if (stack.linter) {
1247
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
1248
- }
1249
- if (stack.formatter) {
1250
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1251
- }
1252
- if (stack.testRunner) {
1253
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1254
- }
1255
- console.log("");
1256
- for (const pkg of packages) {
1257
- console.log(formatPackageSummary(pkg));
1258
- }
1259
- const packagesWithDirs = packages.filter(
1260
- (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1261
- );
1262
- if (packagesWithDirs.length > 0) {
1263
- console.log(`
1264
- ${import_chalk5.default.bold("Structure:")}`);
1265
- for (const pkg of packagesWithDirs) {
1266
- const groups = groupByRole(pkg.structure.directories);
1267
- if (groups.length === 0) continue;
1268
- console.log(` ${pkg.relativePath}:`);
1269
- for (const group of groups) {
1270
- console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
1271
- }
1272
- }
1273
- }
1274
- displayConventions(scanResult);
1275
- displaySummarySection(scanResult);
1276
- console.log("");
1277
- }
1278
- function formatPackageSummaryPlain(pkg) {
1279
- const parts = [];
1280
- if (pkg.stack.framework) {
1281
- parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1493
+ function formatPackageSummaryPlain(pkg) {
1494
+ const parts = [];
1495
+ if (pkg.stack.framework) {
1496
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1282
1497
  }
1283
1498
  if (pkg.stack.styling) {
1284
1499
  parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
@@ -1287,14 +1502,18 @@ function formatPackageSummaryPlain(pkg) {
1287
1502
  const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1288
1503
  return ` ${pkg.relativePath} \u2014 ${detail}`;
1289
1504
  }
1290
- function formatMonorepoResultsText(scanResult, config) {
1505
+ function formatMonorepoResultsText(scanResult) {
1291
1506
  const lines = [];
1292
1507
  const { stack, packages } = scanResult;
1293
1508
  lines.push(`Detected: (monorepo, ${packages.length} packages)`);
1294
1509
  const sharedParts = [formatItem(stack.language)];
1295
1510
  if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
1296
- if (stack.linter) sharedParts.push(formatItem(stack.linter));
1297
- if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1511
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1512
+ sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
1513
+ } else {
1514
+ if (stack.linter) sharedParts.push(formatItem(stack.linter));
1515
+ if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
1516
+ }
1298
1517
  if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
1299
1518
  lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
1300
1519
  lines.push("");
@@ -1324,7 +1543,6 @@ function formatMonorepoResultsText(scanResult, config) {
1324
1543
  if (ext) {
1325
1544
  lines.push(ext);
1326
1545
  }
1327
- lines.push(...formatRulesText(config));
1328
1546
  return lines.join("\n");
1329
1547
  }
1330
1548
 
@@ -1344,7 +1562,7 @@ function displayConventions(scanResult) {
1344
1562
  const conventionEntries = Object.entries(scanResult.conventions);
1345
1563
  if (conventionEntries.length === 0) return;
1346
1564
  console.log(`
1347
- ${import_chalk6.default.bold("Conventions:")}`);
1565
+ ${import_chalk4.default.bold("Conventions:")}`);
1348
1566
  for (const [key, convention] of conventionEntries) {
1349
1567
  if (convention.confidence === "low") continue;
1350
1568
  const label = import_types3.CONVENTION_LABELS[key] ?? key;
@@ -1352,19 +1570,19 @@ ${import_chalk6.default.bold("Conventions:")}`);
1352
1570
  const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1353
1571
  const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1354
1572
  if (allSame || pkgValues.length <= 1) {
1355
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1356
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1573
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
1574
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
1357
1575
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1358
1576
  } else {
1359
- console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
1577
+ console.log(` ${import_chalk4.default.yellow("~")} ${label}: varies by package`);
1360
1578
  for (const pv of pkgValues) {
1361
1579
  const pct = Math.round(pv.convention.consistency);
1362
1580
  console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1363
1581
  }
1364
1582
  }
1365
1583
  } else {
1366
- const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
1367
- const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
1584
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
1585
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
1368
1586
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1369
1587
  }
1370
1588
  }
@@ -1372,7 +1590,7 @@ ${import_chalk6.default.bold("Conventions:")}`);
1372
1590
  function displaySummarySection(scanResult) {
1373
1591
  const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1374
1592
  console.log(`
1375
- ${import_chalk6.default.bold("Summary:")}`);
1593
+ ${import_chalk4.default.bold("Summary:")}`);
1376
1594
  console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1377
1595
  const ext = formatExtensions(scanResult.statistics.filesByExtension);
1378
1596
  if (ext) {
@@ -1386,89 +1604,80 @@ function displayScanResults(scanResult) {
1386
1604
  }
1387
1605
  const { stack } = scanResult;
1388
1606
  console.log(`
1389
- ${import_chalk6.default.bold("Detected:")}`);
1607
+ ${import_chalk4.default.bold("Detected:")}`);
1390
1608
  if (stack.framework) {
1391
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1609
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1392
1610
  }
1393
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1611
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
1394
1612
  if (stack.styling) {
1395
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1613
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1396
1614
  }
1397
1615
  if (stack.backend) {
1398
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1616
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1399
1617
  }
1400
1618
  if (stack.orm) {
1401
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1402
- }
1403
- if (stack.linter) {
1404
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1619
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1405
1620
  }
1406
- if (stack.formatter) {
1407
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1621
+ if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
1622
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
1623
+ } else {
1624
+ if (stack.linter) {
1625
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
1626
+ }
1627
+ if (stack.formatter) {
1628
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1629
+ }
1408
1630
  }
1409
1631
  if (stack.testRunner) {
1410
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1632
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1411
1633
  }
1412
1634
  if (stack.packageManager) {
1413
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1635
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1414
1636
  }
1415
1637
  if (stack.libraries.length > 0) {
1416
1638
  for (const lib of stack.libraries) {
1417
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1639
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1418
1640
  }
1419
1641
  }
1420
1642
  const groups = groupByRole(scanResult.structure.directories);
1421
1643
  if (groups.length > 0) {
1422
1644
  console.log(`
1423
- ${import_chalk6.default.bold("Structure:")}`);
1645
+ ${import_chalk4.default.bold("Structure:")}`);
1424
1646
  for (const group of groups) {
1425
- console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1647
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatRoleGroup(group)}`);
1426
1648
  }
1427
1649
  }
1428
1650
  displayConventions(scanResult);
1429
1651
  displaySummarySection(scanResult);
1430
1652
  console.log("");
1431
1653
  }
1432
- function getConventionStr(cv) {
1433
- return typeof cv === "string" ? cv : cv.value;
1434
- }
1435
1654
  function displayRulesPreview(config) {
1436
- console.log(`${import_chalk6.default.bold("Rules:")}`);
1437
- console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1438
- if (config.rules.requireTests && config.structure.testPattern) {
1655
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1656
+ console.log(
1657
+ `${import_chalk4.default.bold("Rules:")} ${import_chalk4.default.dim("(warns on violation; use --enforce in CI to block)")}`
1658
+ );
1659
+ console.log(` ${import_chalk4.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1660
+ if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
1439
1661
  console.log(
1440
- ` ${import_chalk6.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1662
+ ` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
1441
1663
  );
1442
- } else if (config.rules.requireTests) {
1443
- console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: yes`);
1664
+ } else if (config.rules.testCoverage > 0) {
1665
+ console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
1444
1666
  } else {
1445
- console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: no`);
1667
+ console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: disabled`);
1446
1668
  }
1447
- if (config.rules.enforceNaming && config.conventions.fileNaming) {
1448
- console.log(
1449
- ` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1450
- );
1669
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1670
+ console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
1451
1671
  } else {
1452
- console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
1672
+ console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: no`);
1453
1673
  }
1454
1674
  console.log(
1455
- ` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1675
+ ` ${import_chalk4.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1456
1676
  );
1457
1677
  console.log("");
1458
- if (config.enforcement === "enforce") {
1459
- console.log(`${import_chalk6.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
1460
- } else {
1461
- console.log(
1462
- `${import_chalk6.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1463
- );
1464
- }
1465
- console.log("");
1466
1678
  }
1467
1679
 
1468
1680
  // src/display-text.ts
1469
- function getConventionStr2(cv) {
1470
- return typeof cv === "string" ? cv : cv.value;
1471
- }
1472
1681
  function plainConfidenceLabel(convention) {
1473
1682
  const pct = Math.round(convention.consistency);
1474
1683
  if (convention.confidence === "high") {
@@ -1506,28 +1715,30 @@ function formatConventionsText(scanResult) {
1506
1715
  return lines;
1507
1716
  }
1508
1717
  function formatRulesText(config) {
1718
+ const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1509
1719
  const lines = [];
1510
- lines.push("");
1511
- lines.push("Rules:");
1512
- lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
1513
- if (config.rules.requireTests && config.structure.testPattern) {
1514
- lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
1515
- } else if (config.rules.requireTests) {
1516
- lines.push(" \u2022 Require test files: yes");
1720
+ lines.push(`Max file size: ${config.rules.maxFileLines} lines`);
1721
+ if (config.rules.testCoverage > 0) {
1722
+ lines.push(`Test coverage target: ${config.rules.testCoverage}%`);
1723
+ } else {
1724
+ lines.push("Test coverage target: disabled");
1725
+ }
1726
+ const enforceMissing = config.rules.enforceMissingTests ?? config.rules.testCoverage > 0;
1727
+ if (enforceMissing && root?.structure?.testPattern) {
1728
+ lines.push(`Enforce missing tests: yes (${root.structure.testPattern})`);
1517
1729
  } else {
1518
- lines.push(" \u2022 Require test files: no");
1730
+ lines.push("Enforce missing tests: no");
1519
1731
  }
1520
- if (config.rules.enforceNaming && config.conventions.fileNaming) {
1521
- lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
1732
+ if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
1733
+ lines.push(`Enforce file naming: ${root.conventions.fileNaming}`);
1522
1734
  } else {
1523
- lines.push(" \u2022 Enforce file naming: no");
1735
+ lines.push("Enforce file naming: no");
1524
1736
  }
1525
- lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
1526
1737
  return lines;
1527
1738
  }
1528
- function formatScanResultsText(scanResult, config) {
1739
+ function formatScanResultsText(scanResult) {
1529
1740
  if (scanResult.packages.length > 1) {
1530
- return formatMonorepoResultsText(scanResult, config);
1741
+ return formatMonorepoResultsText(scanResult);
1531
1742
  }
1532
1743
  const lines = [];
1533
1744
  const { stack } = scanResult;
@@ -1574,70 +1785,823 @@ function formatScanResultsText(scanResult, config) {
1574
1785
  if (ext) {
1575
1786
  lines.push(ext);
1576
1787
  }
1577
- lines.push(...formatRulesText(config));
1578
- return lines.join("\n");
1788
+ return lines.join("\n");
1789
+ }
1790
+
1791
+ // src/utils/apply-rule-overrides.ts
1792
+ function applyRuleOverrides(config, overrides) {
1793
+ if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
1794
+ config.rules.maxFileLines = overrides.maxFileLines;
1795
+ config.rules.testCoverage = overrides.testCoverage;
1796
+ config.rules.enforceMissingTests = overrides.enforceMissingTests;
1797
+ config.rules.enforceNaming = overrides.enforceNaming;
1798
+ for (const pkg of config.packages) {
1799
+ pkg.coverage = pkg.coverage ?? {};
1800
+ if (pkg.coverage.summaryPath === void 0) {
1801
+ pkg.coverage.summaryPath = overrides.coverageSummaryPath;
1802
+ }
1803
+ if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
1804
+ pkg.coverage.command = overrides.coverageCommand;
1805
+ }
1806
+ }
1807
+ if (overrides.fileNamingValue) {
1808
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1809
+ const oldNaming = rootPkg.conventions?.fileNaming;
1810
+ rootPkg.conventions = rootPkg.conventions ?? {};
1811
+ rootPkg.conventions.fileNaming = overrides.fileNamingValue;
1812
+ if (oldNaming && oldNaming !== overrides.fileNamingValue) {
1813
+ for (const pkg of config.packages) {
1814
+ if (pkg.conventions?.fileNaming === oldNaming) {
1815
+ pkg.conventions.fileNaming = overrides.fileNamingValue;
1816
+ }
1817
+ }
1818
+ }
1819
+ }
1820
+ }
1821
+
1822
+ // src/utils/diff-configs.ts
1823
+ var import_types5 = require("@viberails/types");
1824
+ function parseStackString(s) {
1825
+ const atIdx = s.indexOf("@");
1826
+ if (atIdx > 0) {
1827
+ return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1828
+ }
1829
+ return { name: s };
1830
+ }
1831
+ function displayStackName(s) {
1832
+ const { name, version } = parseStackString(s);
1833
+ const allMaps = {
1834
+ ...import_types5.FRAMEWORK_NAMES,
1835
+ ...import_types5.STYLING_NAMES,
1836
+ ...import_types5.ORM_NAMES
1837
+ };
1838
+ const display = allMaps[name] ?? name;
1839
+ return version ? `${display} ${version}` : display;
1840
+ }
1841
+ function isNewlyDetected(config, pkgPath, key) {
1842
+ return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
1843
+ }
1844
+ var STACK_FIELDS = [
1845
+ "framework",
1846
+ "styling",
1847
+ "backend",
1848
+ "orm",
1849
+ "linter",
1850
+ "formatter",
1851
+ "testRunner"
1852
+ ];
1853
+ var CONVENTION_KEYS = [
1854
+ "fileNaming",
1855
+ "componentNaming",
1856
+ "hookNaming",
1857
+ "importAlias"
1858
+ ];
1859
+ var STRUCTURE_FIELDS = [
1860
+ { key: "srcDir", label: "source directory" },
1861
+ { key: "pages", label: "pages directory" },
1862
+ { key: "components", label: "components directory" },
1863
+ { key: "hooks", label: "hooks directory" },
1864
+ { key: "utils", label: "utilities directory" },
1865
+ { key: "types", label: "types directory" },
1866
+ { key: "tests", label: "tests directory" },
1867
+ { key: "testPattern", label: "test pattern" }
1868
+ ];
1869
+ function diffPackage(existing, merged, mergedConfig) {
1870
+ const changes = [];
1871
+ const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
1872
+ for (const field of STACK_FIELDS) {
1873
+ const oldVal = existing.stack?.[field];
1874
+ const newVal = merged.stack?.[field];
1875
+ if (!oldVal && newVal) {
1876
+ changes.push({
1877
+ type: "added",
1878
+ description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
1879
+ });
1880
+ } else if (oldVal && newVal && oldVal !== newVal) {
1881
+ changes.push({
1882
+ type: "changed",
1883
+ description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1884
+ });
1885
+ }
1886
+ }
1887
+ for (const key of CONVENTION_KEYS) {
1888
+ const oldVal = existing.conventions?.[key];
1889
+ const newVal = merged.conventions?.[key];
1890
+ const label = import_types5.CONVENTION_LABELS[key] ?? key;
1891
+ if (!oldVal && newVal) {
1892
+ changes.push({
1893
+ type: "added",
1894
+ description: `${pkgPrefix}New convention: ${label} (${newVal})`
1895
+ });
1896
+ } else if (oldVal && newVal && oldVal !== newVal) {
1897
+ const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
1898
+ changes.push({
1899
+ type: "changed",
1900
+ description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
1901
+ });
1902
+ }
1903
+ }
1904
+ for (const { key, label } of STRUCTURE_FIELDS) {
1905
+ const oldVal = existing.structure?.[key];
1906
+ const newVal = merged.structure?.[key];
1907
+ if (!oldVal && newVal) {
1908
+ changes.push({
1909
+ type: "added",
1910
+ description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
1911
+ });
1912
+ }
1913
+ }
1914
+ return changes;
1915
+ }
1916
+ function diffConfigs(existing, merged) {
1917
+ const changes = [];
1918
+ const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
1919
+ const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
1920
+ for (const existingPkg of existing.packages) {
1921
+ const mergedPkg = mergedByPath.get(existingPkg.path);
1922
+ if (mergedPkg) {
1923
+ changes.push(...diffPackage(existingPkg, mergedPkg, merged));
1924
+ }
1925
+ }
1926
+ for (const mergedPkg of merged.packages) {
1927
+ if (!existingByPath.has(mergedPkg.path)) {
1928
+ changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
1929
+ }
1930
+ }
1931
+ return changes;
1932
+ }
1933
+ function formatStatsDelta(oldStats, newStats) {
1934
+ const fileDelta = newStats.totalFiles - oldStats.totalFiles;
1935
+ const lineDelta = newStats.totalLines - oldStats.totalLines;
1936
+ if (fileDelta === 0 && lineDelta === 0) return void 0;
1937
+ const parts = [];
1938
+ if (fileDelta !== 0) {
1939
+ const sign = fileDelta > 0 ? "+" : "";
1940
+ parts.push(`${sign}${fileDelta.toLocaleString()} files`);
1941
+ }
1942
+ if (lineDelta !== 0) {
1943
+ const sign = lineDelta > 0 ? "+" : "";
1944
+ parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
1945
+ }
1946
+ return `${parts.join(", ")} since last sync`;
1947
+ }
1948
+
1949
+ // src/utils/write-generated-files.ts
1950
+ var fs9 = __toESM(require("fs"), 1);
1951
+ var path8 = __toESM(require("path"), 1);
1952
+ var import_context = require("@viberails/context");
1953
+ var CONTEXT_DIR = ".viberails";
1954
+ var CONTEXT_FILE = "context.md";
1955
+ var SCAN_RESULT_FILE = "scan-result.json";
1956
+ function writeGeneratedFiles(projectRoot, config, scanResult) {
1957
+ const contextDir = path8.join(projectRoot, CONTEXT_DIR);
1958
+ try {
1959
+ if (!fs9.existsSync(contextDir)) {
1960
+ fs9.mkdirSync(contextDir, { recursive: true });
1961
+ }
1962
+ const context = (0, import_context.generateContext)(config);
1963
+ fs9.writeFileSync(path8.join(contextDir, CONTEXT_FILE), context);
1964
+ fs9.writeFileSync(
1965
+ path8.join(contextDir, SCAN_RESULT_FILE),
1966
+ `${JSON.stringify(scanResult, null, 2)}
1967
+ `
1968
+ );
1969
+ } catch (err) {
1970
+ const message = err instanceof Error ? err.message : String(err);
1971
+ throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
1972
+ }
1973
+ }
1974
+
1975
+ // src/commands/config.ts
1976
+ var CONFIG_FILE3 = "viberails.config.json";
1977
+ async function configCommand(options, cwd) {
1978
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
1979
+ if (!projectRoot) {
1980
+ throw new Error("No package.json found. Make sure you are inside a JS/TS project.");
1981
+ }
1982
+ const configPath = path9.join(projectRoot, CONFIG_FILE3);
1983
+ if (!fs10.existsSync(configPath)) {
1984
+ console.log(`${import_chalk5.default.yellow("!")} No config found. Run ${import_chalk5.default.cyan("viberails init")} first.`);
1985
+ return;
1986
+ }
1987
+ clack6.intro("viberails config");
1988
+ const config = await (0, import_config6.loadConfig)(configPath);
1989
+ let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
1990
+ clack6.note(formatRulesText(config).join("\n"), "Current rules");
1991
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1992
+ const overrides = await promptRuleMenu({
1993
+ maxFileLines: config.rules.maxFileLines,
1994
+ testCoverage: config.rules.testCoverage,
1995
+ enforceMissingTests: config.rules.enforceMissingTests,
1996
+ enforceNaming: config.rules.enforceNaming,
1997
+ fileNamingValue: rootPkg.conventions?.fileNaming,
1998
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
1999
+ coverageCommand: config.defaults?.coverage?.command,
2000
+ packageOverrides: config.packages
2001
+ });
2002
+ applyRuleOverrides(config, overrides);
2003
+ if (options.rescan && config.packages.length > 1) {
2004
+ const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
2005
+ if (shouldInfer) {
2006
+ const bs = clack6.spinner();
2007
+ bs.start("Building import graph...");
2008
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2009
+ const packages = resolveWorkspacePackages(projectRoot, config.packages);
2010
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
2011
+ const inferred = inferBoundaries(graph);
2012
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
2013
+ if (denyCount > 0) {
2014
+ config.boundaries = inferred;
2015
+ config.rules.enforceBoundaries = true;
2016
+ bs.stop(`Inferred ${denyCount} boundary rules`);
2017
+ } else {
2018
+ bs.stop("No boundary rules inferred");
2019
+ }
2020
+ }
2021
+ }
2022
+ const shouldWrite = await confirm3("Save updated configuration?");
2023
+ if (!shouldWrite) {
2024
+ clack6.outro("No changes written.");
2025
+ return;
2026
+ }
2027
+ const compacted = (0, import_config6.compactConfig)(config);
2028
+ fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
2029
+ `);
2030
+ if (!scanResult) {
2031
+ const s = clack6.spinner();
2032
+ s.start("Scanning for context generation...");
2033
+ scanResult = await (0, import_scanner.scan)(projectRoot);
2034
+ s.stop("Scan complete");
2035
+ }
2036
+ writeGeneratedFiles(projectRoot, config, scanResult);
2037
+ clack6.log.success(
2038
+ `Updated:
2039
+ ${CONFIG_FILE3}
2040
+ .viberails/context.md
2041
+ .viberails/scan-result.json`
2042
+ );
2043
+ clack6.outro("Done! Run viberails check to verify.");
2044
+ }
2045
+ async function rescanAndMerge(projectRoot, config) {
2046
+ const s = clack6.spinner();
2047
+ s.start("Re-scanning project...");
2048
+ const scanResult = await (0, import_scanner.scan)(projectRoot);
2049
+ const merged = (0, import_config6.mergeConfig)(config, scanResult);
2050
+ s.stop("Scan complete");
2051
+ const changes = diffConfigs(config, merged);
2052
+ if (changes.length > 0) {
2053
+ const changeLines = changes.map((c) => {
2054
+ const icon = c.type === "removed" ? "-" : "+";
2055
+ return `${icon} ${c.description}`;
2056
+ }).join("\n");
2057
+ clack6.note(changeLines, "Changes detected");
2058
+ } else {
2059
+ clack6.log.info("No new changes detected from scan.");
2060
+ }
2061
+ Object.assign(config, merged);
2062
+ return scanResult;
2063
+ }
2064
+
2065
+ // src/commands/fix.ts
2066
+ var fs13 = __toESM(require("fs"), 1);
2067
+ var path13 = __toESM(require("path"), 1);
2068
+ var import_config7 = require("@viberails/config");
2069
+ var import_chalk7 = __toESM(require("chalk"), 1);
2070
+
2071
+ // src/commands/fix-helpers.ts
2072
+ var import_node_child_process4 = require("child_process");
2073
+ var import_chalk6 = __toESM(require("chalk"), 1);
2074
+ function printPlan(renames, stubs) {
2075
+ if (renames.length > 0) {
2076
+ console.log(import_chalk6.default.bold("\nFile renames:"));
2077
+ for (const r of renames) {
2078
+ console.log(` ${import_chalk6.default.red(r.oldPath)} \u2192 ${import_chalk6.default.green(r.newPath)}`);
2079
+ }
2080
+ }
2081
+ if (stubs.length > 0) {
2082
+ console.log(import_chalk6.default.bold("\nTest stubs to create:"));
2083
+ for (const s of stubs) {
2084
+ console.log(` ${import_chalk6.default.green("+")} ${s.path}`);
2085
+ }
2086
+ }
2087
+ }
2088
+ function checkGitDirty(projectRoot) {
2089
+ try {
2090
+ const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
2091
+ cwd: projectRoot,
2092
+ encoding: "utf-8",
2093
+ stdio: ["ignore", "pipe", "ignore"]
2094
+ });
2095
+ return output.trim().length > 0;
2096
+ } catch {
2097
+ return false;
2098
+ }
2099
+ }
2100
+ function getConventionValue(convention) {
2101
+ if (typeof convention === "string") return convention;
2102
+ return void 0;
2103
+ }
2104
+
2105
+ // src/commands/fix-imports.ts
2106
+ var path10 = __toESM(require("path"), 1);
2107
+ function stripExtension(filePath) {
2108
+ return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2109
+ }
2110
+ function computeNewSpecifier(oldSpecifier, newBare) {
2111
+ const hasJsExt = oldSpecifier.endsWith(".js");
2112
+ const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
2113
+ const dir = base.lastIndexOf("/");
2114
+ const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
2115
+ const newSpec = prefix + newBare;
2116
+ return hasJsExt ? `${newSpec}.js` : newSpec;
2117
+ }
2118
+ async function updateImportsAfterRenames(renames, projectRoot) {
2119
+ if (renames.length === 0) return [];
2120
+ const { Project, SyntaxKind } = await import("ts-morph");
2121
+ const renameMap = /* @__PURE__ */ new Map();
2122
+ for (const r of renames) {
2123
+ const oldStripped = stripExtension(r.oldAbsPath);
2124
+ const newFilename = path10.basename(r.newPath);
2125
+ const newName = newFilename.slice(0, newFilename.indexOf("."));
2126
+ renameMap.set(oldStripped, { newBare: newName });
2127
+ }
2128
+ const project = new Project({
2129
+ tsConfigFilePath: void 0,
2130
+ skipAddingFilesFromTsConfig: true
2131
+ });
2132
+ project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
2133
+ const updates = [];
2134
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
2135
+ for (const sourceFile of project.getSourceFiles()) {
2136
+ const filePath = sourceFile.getFilePath();
2137
+ const segments = filePath.split(path10.sep);
2138
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
2139
+ const fileDir = path10.dirname(filePath);
2140
+ for (const decl of sourceFile.getImportDeclarations()) {
2141
+ const specifier = decl.getModuleSpecifierValue();
2142
+ if (!specifier.startsWith(".")) continue;
2143
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2144
+ if (!match) continue;
2145
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2146
+ updates.push({
2147
+ file: filePath,
2148
+ oldSpecifier: specifier,
2149
+ newSpecifier: newSpec,
2150
+ line: decl.getStartLineNumber()
2151
+ });
2152
+ decl.setModuleSpecifier(newSpec);
2153
+ }
2154
+ for (const decl of sourceFile.getExportDeclarations()) {
2155
+ const specifier = decl.getModuleSpecifierValue();
2156
+ if (!specifier || !specifier.startsWith(".")) continue;
2157
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2158
+ if (!match) continue;
2159
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2160
+ updates.push({
2161
+ file: filePath,
2162
+ oldSpecifier: specifier,
2163
+ newSpecifier: newSpec,
2164
+ line: decl.getStartLineNumber()
2165
+ });
2166
+ decl.setModuleSpecifier(newSpec);
2167
+ }
2168
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2169
+ if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
2170
+ const args = call.getArguments();
2171
+ if (args.length === 0) continue;
2172
+ const arg = args[0];
2173
+ if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
2174
+ const specifier = arg.getText().slice(1, -1);
2175
+ if (!specifier.startsWith(".")) continue;
2176
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
2177
+ if (!match) continue;
2178
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
2179
+ updates.push({
2180
+ file: filePath,
2181
+ oldSpecifier: specifier,
2182
+ newSpecifier: newSpec,
2183
+ line: call.getStartLineNumber()
2184
+ });
2185
+ const quote = arg.getText()[0];
2186
+ arg.replaceWithText(`${quote}${newSpec}${quote}`);
2187
+ }
2188
+ }
2189
+ if (updates.length > 0) {
2190
+ await project.save();
2191
+ }
2192
+ return updates;
2193
+ }
2194
+ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
2195
+ const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
2196
+ const resolved = path10.resolve(fromDir, cleanSpec);
2197
+ for (const ext of extensions) {
2198
+ const candidate = resolved + ext;
2199
+ const stripped = stripExtension(candidate);
2200
+ const match = renameMap.get(stripped);
2201
+ if (match) return match;
2202
+ }
2203
+ return void 0;
2204
+ }
2205
+
2206
+ // src/commands/fix-naming.ts
2207
+ var fs11 = __toESM(require("fs"), 1);
2208
+ var path11 = __toESM(require("path"), 1);
2209
+
2210
+ // src/commands/convert-name.ts
2211
+ function splitIntoWords(name) {
2212
+ const parts = name.split(/[-_]/);
2213
+ const words = [];
2214
+ for (const part of parts) {
2215
+ if (part === "") continue;
2216
+ let current = "";
2217
+ for (let i = 0; i < part.length; i++) {
2218
+ const ch = part[i];
2219
+ const isUpper = ch >= "A" && ch <= "Z";
2220
+ if (isUpper && current.length > 0) {
2221
+ const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
2222
+ const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
2223
+ if (!prevIsUpper || nextIsLower) {
2224
+ words.push(current.toLowerCase());
2225
+ current = "";
2226
+ }
2227
+ }
2228
+ current += ch;
2229
+ }
2230
+ if (current) words.push(current.toLowerCase());
2231
+ }
2232
+ return words;
2233
+ }
2234
+ function convertName(bare, target) {
2235
+ const words = splitIntoWords(bare);
2236
+ if (words.length === 0) return bare;
2237
+ switch (target) {
2238
+ case "kebab-case":
2239
+ return words.join("-");
2240
+ case "camelCase":
2241
+ return words[0] + words.slice(1).map(capitalize).join("");
2242
+ case "PascalCase":
2243
+ return words.map(capitalize).join("");
2244
+ case "snake_case":
2245
+ return words.join("_");
2246
+ default:
2247
+ return bare;
2248
+ }
2249
+ }
2250
+ function capitalize(word) {
2251
+ if (word.length === 0) return word;
2252
+ return word[0].toUpperCase() + word.slice(1);
2253
+ }
2254
+
2255
+ // src/commands/fix-naming.ts
2256
+ function computeRename(relPath, targetConvention, projectRoot) {
2257
+ const filename = path11.basename(relPath);
2258
+ const dir = path11.dirname(relPath);
2259
+ const dotIndex = filename.indexOf(".");
2260
+ if (dotIndex === -1) return null;
2261
+ const bare = filename.slice(0, dotIndex);
2262
+ const suffix = filename.slice(dotIndex);
2263
+ const newBare = convertName(bare, targetConvention);
2264
+ if (newBare === bare) return null;
2265
+ const newFilename = newBare + suffix;
2266
+ const newRelPath = path11.join(dir, newFilename);
2267
+ const oldAbsPath = path11.join(projectRoot, relPath);
2268
+ const newAbsPath = path11.join(projectRoot, newRelPath);
2269
+ if (fs11.existsSync(newAbsPath)) return null;
2270
+ return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
2271
+ }
2272
+ function executeRename(rename) {
2273
+ if (fs11.existsSync(rename.newAbsPath)) return false;
2274
+ fs11.renameSync(rename.oldAbsPath, rename.newAbsPath);
2275
+ return true;
2276
+ }
2277
+ function deduplicateRenames(renames) {
2278
+ const seen = /* @__PURE__ */ new Set();
2279
+ const result = [];
2280
+ for (const r of renames) {
2281
+ if (seen.has(r.newAbsPath)) continue;
2282
+ seen.add(r.newAbsPath);
2283
+ result.push(r);
2284
+ }
2285
+ return result;
2286
+ }
2287
+
2288
+ // src/commands/fix-tests.ts
2289
+ var fs12 = __toESM(require("fs"), 1);
2290
+ var path12 = __toESM(require("path"), 1);
2291
+ function generateTestStub(sourceRelPath, config, projectRoot) {
2292
+ const pkg = resolvePackageForFile(sourceRelPath, config);
2293
+ const testPattern = pkg?.structure?.testPattern;
2294
+ if (!testPattern) return null;
2295
+ const basename8 = path12.basename(sourceRelPath);
2296
+ const ext = path12.extname(basename8);
2297
+ if (!ext) return null;
2298
+ const stem = basename8.slice(0, -ext.length);
2299
+ const testSuffix = testPattern.replace("*", "");
2300
+ const testFilename = `${stem}${testSuffix}`;
2301
+ const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
2302
+ const testAbsPath = path12.join(dir, testFilename);
2303
+ if (fs12.existsSync(testAbsPath)) return null;
2304
+ return {
2305
+ path: path12.relative(projectRoot, testAbsPath),
2306
+ absPath: testAbsPath,
2307
+ moduleName: stem
2308
+ };
2309
+ }
2310
+ function writeTestStub(stub, config) {
2311
+ const pkg = resolvePackageForFile(stub.path, config);
2312
+ const testRunner = pkg?.stack?.testRunner ?? "";
2313
+ const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
2314
+ const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
2315
+ const content = `${importLine}describe('${stub.moduleName}', () => {
2316
+ it.todo('add tests');
2317
+ });
2318
+ `;
2319
+ fs12.mkdirSync(path12.dirname(stub.absPath), { recursive: true });
2320
+ fs12.writeFileSync(stub.absPath, content);
2321
+ }
2322
+
2323
+ // src/commands/fix.ts
2324
+ var CONFIG_FILE4 = "viberails.config.json";
2325
+ async function fixCommand(options, cwd) {
2326
+ const startDir = cwd ?? process.cwd();
2327
+ const projectRoot = findProjectRoot(startDir);
2328
+ if (!projectRoot) {
2329
+ console.error(`${import_chalk7.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
2330
+ return 1;
2331
+ }
2332
+ const configPath = path13.join(projectRoot, CONFIG_FILE4);
2333
+ if (!fs13.existsSync(configPath)) {
2334
+ console.error(
2335
+ `${import_chalk7.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
2336
+ );
2337
+ return 1;
2338
+ }
2339
+ const config = await (0, import_config7.loadConfig)(configPath);
2340
+ if (!options.dryRun) {
2341
+ const isDirty = checkGitDirty(projectRoot);
2342
+ if (isDirty) {
2343
+ console.log(
2344
+ import_chalk7.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
2345
+ );
2346
+ }
2347
+ }
2348
+ const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
2349
+ const shouldFixTests = !options.rule || options.rule.includes("missing-test");
2350
+ const allFiles = getAllSourceFiles(projectRoot, config);
2351
+ const renames = [];
2352
+ if (shouldFixNaming) {
2353
+ for (const file of allFiles) {
2354
+ const resolved = resolveConfigForFile(file, config);
2355
+ if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
2356
+ const violation = checkNaming(file, resolved.conventions);
2357
+ if (!violation) continue;
2358
+ const convention = getConventionValue(resolved.conventions.fileNaming);
2359
+ if (!convention) continue;
2360
+ const rename = computeRename(file, convention, projectRoot);
2361
+ if (rename) renames.push(rename);
2362
+ }
2363
+ }
2364
+ const dedupedRenames = deduplicateRenames(renames);
2365
+ const testStubs = [];
2366
+ if (shouldFixTests) {
2367
+ const testViolations = checkMissingTests(projectRoot, config, "warn");
2368
+ for (const v of testViolations) {
2369
+ const stub = generateTestStub(v.file, config, projectRoot);
2370
+ if (stub) testStubs.push(stub);
2371
+ }
2372
+ }
2373
+ if (dedupedRenames.length === 0 && testStubs.length === 0) {
2374
+ console.log(`${import_chalk7.default.green("\u2713")} No fixable violations found.`);
2375
+ return 0;
2376
+ }
2377
+ printPlan(dedupedRenames, testStubs);
2378
+ if (options.dryRun) {
2379
+ console.log(import_chalk7.default.dim("\nDry run \u2014 no changes applied."));
2380
+ return 0;
2381
+ }
2382
+ if (!options.yes) {
2383
+ const confirmed = await confirmDangerous("Apply these fixes?");
2384
+ if (!confirmed) {
2385
+ console.log("Aborted.");
2386
+ return 0;
2387
+ }
2388
+ }
2389
+ let renameCount = 0;
2390
+ for (const rename of dedupedRenames) {
2391
+ if (executeRename(rename)) {
2392
+ renameCount++;
2393
+ }
2394
+ }
2395
+ let importUpdateCount = 0;
2396
+ if (renameCount > 0) {
2397
+ const appliedRenames = dedupedRenames.filter((r) => fs13.existsSync(r.newAbsPath));
2398
+ const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
2399
+ importUpdateCount = updates.length;
2400
+ }
2401
+ let stubCount = 0;
2402
+ for (const stub of testStubs) {
2403
+ if (!fs13.existsSync(stub.absPath)) {
2404
+ writeTestStub(stub, config);
2405
+ stubCount++;
2406
+ }
2407
+ }
2408
+ console.log("");
2409
+ if (renameCount > 0) {
2410
+ console.log(`${import_chalk7.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
2411
+ }
2412
+ if (importUpdateCount > 0) {
2413
+ console.log(
2414
+ `${import_chalk7.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
2415
+ );
2416
+ }
2417
+ if (stubCount > 0) {
2418
+ console.log(`${import_chalk7.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
2419
+ }
2420
+ return 0;
2421
+ }
2422
+
2423
+ // src/commands/init.ts
2424
+ var fs18 = __toESM(require("fs"), 1);
2425
+ var path18 = __toESM(require("path"), 1);
2426
+ var clack8 = __toESM(require("@clack/prompts"), 1);
2427
+ var import_config8 = require("@viberails/config");
2428
+ var import_scanner2 = require("@viberails/scanner");
2429
+ var import_chalk11 = __toESM(require("chalk"), 1);
2430
+
2431
+ // src/utils/check-prerequisites.ts
2432
+ var import_node_child_process5 = require("child_process");
2433
+ var fs14 = __toESM(require("fs"), 1);
2434
+ var path14 = __toESM(require("path"), 1);
2435
+ var clack7 = __toESM(require("@clack/prompts"), 1);
2436
+ var import_chalk8 = __toESM(require("chalk"), 1);
2437
+ function checkCoveragePrereqs(projectRoot, scanResult) {
2438
+ const testRunner = scanResult.stack.testRunner;
2439
+ if (!testRunner) return [];
2440
+ const runner = testRunner.name;
2441
+ const pm = scanResult.stack.packageManager.name;
2442
+ if (runner === "vitest") {
2443
+ const hasV8 = hasDependency(projectRoot, "@vitest/coverage-v8");
2444
+ const hasIstanbul = hasDependency(projectRoot, "@vitest/coverage-istanbul");
2445
+ const installed = hasV8 || hasIstanbul;
2446
+ const addCmd = pm === "yarn" ? "yarn add -D" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
2447
+ return [
2448
+ {
2449
+ label: "@vitest/coverage-v8",
2450
+ installed,
2451
+ installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
2452
+ reason: "Required for coverage percentage checks with vitest"
2453
+ }
2454
+ ];
2455
+ }
2456
+ return [];
2457
+ }
2458
+ function displayMissingPrereqs(prereqs) {
2459
+ const missing = prereqs.filter((p) => !p.installed);
2460
+ for (const m of missing) {
2461
+ console.log(` ${import_chalk8.default.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
2462
+ if (m.installCommand) {
2463
+ console.log(` Install: ${import_chalk8.default.cyan(m.installCommand)}`);
2464
+ }
2465
+ }
2466
+ }
2467
+ async function promptMissingPrereqs(projectRoot, prereqs) {
2468
+ const missing = prereqs.filter((p) => !p.installed);
2469
+ if (missing.length === 0) return { disableCoverage: false };
2470
+ const prereqLines = prereqs.map(
2471
+ (p) => `${p.installed ? "\u2713" : "\u2717"} ${p.label}${p.installed ? "" : ` \u2014 ${p.reason}`}`
2472
+ ).join("\n");
2473
+ clack7.note(prereqLines, "Coverage prerequisites");
2474
+ let disableCoverage = false;
2475
+ for (const m of missing) {
2476
+ if (!m.installCommand) continue;
2477
+ const choice = await clack7.select({
2478
+ message: `${m.label} is not installed. It is required for coverage percentage checks.`,
2479
+ options: [
2480
+ {
2481
+ value: "install",
2482
+ label: `Yes, install now`,
2483
+ hint: m.installCommand
2484
+ },
2485
+ {
2486
+ value: "disable",
2487
+ label: "No, disable coverage percentage checks",
2488
+ hint: "missing-test checks still active"
2489
+ },
2490
+ {
2491
+ value: "skip",
2492
+ label: "Skip for now",
2493
+ hint: `install later: ${m.installCommand}`
2494
+ }
2495
+ ]
2496
+ });
2497
+ assertNotCancelled(choice);
2498
+ if (choice === "install") {
2499
+ const is = clack7.spinner();
2500
+ is.start(`Installing ${m.label}...`);
2501
+ const result = (0, import_node_child_process5.spawnSync)(m.installCommand, {
2502
+ cwd: projectRoot,
2503
+ shell: true,
2504
+ encoding: "utf-8",
2505
+ stdio: "pipe"
2506
+ });
2507
+ if (result.status === 0) {
2508
+ is.stop(`Installed ${m.label}`);
2509
+ } else {
2510
+ is.stop(`Failed to install ${m.label}`);
2511
+ clack7.log.warn(
2512
+ `Install manually: ${m.installCommand}
2513
+ Coverage percentage checks will not work until the dependency is installed.`
2514
+ );
2515
+ }
2516
+ } else if (choice === "disable") {
2517
+ disableCoverage = true;
2518
+ clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
2519
+ } else {
2520
+ clack7.log.info(
2521
+ `Coverage percentage checks will fail until ${m.label} is installed.
2522
+ Install later: ${m.installCommand}`
2523
+ );
2524
+ }
2525
+ }
2526
+ return { disableCoverage };
2527
+ }
2528
+ function hasDependency(projectRoot, name) {
2529
+ try {
2530
+ const pkgPath = path14.join(projectRoot, "package.json");
2531
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
2532
+ return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
2533
+ } catch {
2534
+ return false;
2535
+ }
1579
2536
  }
1580
2537
 
1581
- // src/utils/write-generated-files.ts
1582
- var fs11 = __toESM(require("fs"), 1);
1583
- var path11 = __toESM(require("path"), 1);
1584
- var import_context = require("@viberails/context");
1585
- var CONTEXT_DIR = ".viberails";
1586
- var CONTEXT_FILE = "context.md";
1587
- var SCAN_RESULT_FILE = "scan-result.json";
1588
- function writeGeneratedFiles(projectRoot, config, scanResult) {
1589
- const contextDir = path11.join(projectRoot, CONTEXT_DIR);
1590
- try {
1591
- if (!fs11.existsSync(contextDir)) {
1592
- fs11.mkdirSync(contextDir, { recursive: true });
2538
+ // src/utils/filter-confidence.ts
2539
+ function filterHighConfidence(conventions, meta) {
2540
+ if (!meta) return conventions;
2541
+ const filtered = {};
2542
+ for (const [key, value] of Object.entries(conventions)) {
2543
+ if (value === void 0) continue;
2544
+ const convMeta = meta[key];
2545
+ if (!convMeta || convMeta.confidence === "high") {
2546
+ filtered[key] = value;
1593
2547
  }
1594
- const context = (0, import_context.generateContext)(config);
1595
- fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1596
- fs11.writeFileSync(
1597
- path11.join(contextDir, SCAN_RESULT_FILE),
1598
- `${JSON.stringify(scanResult, null, 2)}
1599
- `
1600
- );
1601
- } catch (err) {
1602
- const message = err instanceof Error ? err.message : String(err);
1603
- throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
2548
+ }
2549
+ return filtered;
2550
+ }
2551
+
2552
+ // src/utils/update-gitignore.ts
2553
+ var fs15 = __toESM(require("fs"), 1);
2554
+ var path15 = __toESM(require("path"), 1);
2555
+ function updateGitignore(projectRoot) {
2556
+ const gitignorePath = path15.join(projectRoot, ".gitignore");
2557
+ let content = "";
2558
+ if (fs15.existsSync(gitignorePath)) {
2559
+ content = fs15.readFileSync(gitignorePath, "utf-8");
2560
+ }
2561
+ if (!content.includes(".viberails/scan-result.json")) {
2562
+ const block = "\n# viberails\n.viberails/scan-result.json\n";
2563
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
2564
+ `;
2565
+ fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
1604
2566
  }
1605
2567
  }
1606
2568
 
1607
2569
  // src/commands/init-hooks.ts
1608
- var fs12 = __toESM(require("fs"), 1);
1609
- var path12 = __toESM(require("path"), 1);
1610
- var import_chalk7 = __toESM(require("chalk"), 1);
2570
+ var fs16 = __toESM(require("fs"), 1);
2571
+ var path16 = __toESM(require("path"), 1);
2572
+ var import_chalk9 = __toESM(require("chalk"), 1);
1611
2573
  var import_yaml = require("yaml");
1612
2574
  function setupPreCommitHook(projectRoot) {
1613
- const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1614
- if (fs12.existsSync(lefthookPath)) {
2575
+ const lefthookPath = path16.join(projectRoot, "lefthook.yml");
2576
+ if (fs16.existsSync(lefthookPath)) {
1615
2577
  addLefthookPreCommit(lefthookPath);
1616
- console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1617
- return;
2578
+ console.log(` ${import_chalk9.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
2579
+ return "lefthook.yml";
1618
2580
  }
1619
- const huskyDir = path12.join(projectRoot, ".husky");
1620
- if (fs12.existsSync(huskyDir)) {
2581
+ const huskyDir = path16.join(projectRoot, ".husky");
2582
+ if (fs16.existsSync(huskyDir)) {
1621
2583
  writeHuskyPreCommit(huskyDir);
1622
- console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1623
- return;
2584
+ console.log(` ${import_chalk9.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
2585
+ return ".husky/pre-commit";
1624
2586
  }
1625
- const gitDir = path12.join(projectRoot, ".git");
1626
- if (fs12.existsSync(gitDir)) {
1627
- const hooksDir = path12.join(gitDir, "hooks");
1628
- if (!fs12.existsSync(hooksDir)) {
1629
- fs12.mkdirSync(hooksDir, { recursive: true });
2587
+ const gitDir = path16.join(projectRoot, ".git");
2588
+ if (fs16.existsSync(gitDir)) {
2589
+ const hooksDir = path16.join(gitDir, "hooks");
2590
+ if (!fs16.existsSync(hooksDir)) {
2591
+ fs16.mkdirSync(hooksDir, { recursive: true });
1630
2592
  }
1631
2593
  writeGitHookPreCommit(hooksDir);
1632
- console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
2594
+ console.log(` ${import_chalk9.default.green("\u2713")} .git/hooks/pre-commit`);
2595
+ return ".git/hooks/pre-commit";
1633
2596
  }
2597
+ return void 0;
1634
2598
  }
1635
2599
  function writeGitHookPreCommit(hooksDir) {
1636
- const hookPath = path12.join(hooksDir, "pre-commit");
1637
- if (fs12.existsSync(hookPath)) {
1638
- const existing = fs12.readFileSync(hookPath, "utf-8");
2600
+ const hookPath = path16.join(hooksDir, "pre-commit");
2601
+ if (fs16.existsSync(hookPath)) {
2602
+ const existing = fs16.readFileSync(hookPath, "utf-8");
1639
2603
  if (existing.includes("viberails")) return;
1640
- fs12.writeFileSync(
2604
+ fs16.writeFileSync(
1641
2605
  hookPath,
1642
2606
  `${existing.trimEnd()}
1643
2607
 
@@ -1654,10 +2618,10 @@ npx viberails check --staged
1654
2618
  "npx viberails check --staged",
1655
2619
  ""
1656
2620
  ].join("\n");
1657
- fs12.writeFileSync(hookPath, script, { mode: 493 });
2621
+ fs16.writeFileSync(hookPath, script, { mode: 493 });
1658
2622
  }
1659
2623
  function addLefthookPreCommit(lefthookPath) {
1660
- const content = fs12.readFileSync(lefthookPath, "utf-8");
2624
+ const content = fs16.readFileSync(lefthookPath, "utf-8");
1661
2625
  if (content.includes("viberails")) return;
1662
2626
  const doc = (0, import_yaml.parse)(content) ?? {};
1663
2627
  if (!doc["pre-commit"]) {
@@ -1669,29 +2633,29 @@ function addLefthookPreCommit(lefthookPath) {
1669
2633
  doc["pre-commit"].commands.viberails = {
1670
2634
  run: "npx viberails check --staged"
1671
2635
  };
1672
- fs12.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
2636
+ fs16.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
1673
2637
  }
1674
2638
  function detectHookManager(projectRoot) {
1675
- if (fs12.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1676
- if (fs12.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1677
- if (fs12.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
2639
+ if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2640
+ if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
2641
+ if (fs16.existsSync(path16.join(projectRoot, ".git"))) return "git hook";
1678
2642
  return void 0;
1679
2643
  }
1680
2644
  function setupClaudeCodeHook(projectRoot) {
1681
- const claudeDir = path12.join(projectRoot, ".claude");
1682
- if (!fs12.existsSync(claudeDir)) {
1683
- fs12.mkdirSync(claudeDir, { recursive: true });
2645
+ const claudeDir = path16.join(projectRoot, ".claude");
2646
+ if (!fs16.existsSync(claudeDir)) {
2647
+ fs16.mkdirSync(claudeDir, { recursive: true });
1684
2648
  }
1685
- const settingsPath = path12.join(claudeDir, "settings.json");
2649
+ const settingsPath = path16.join(claudeDir, "settings.json");
1686
2650
  let settings = {};
1687
- if (fs12.existsSync(settingsPath)) {
2651
+ if (fs16.existsSync(settingsPath)) {
1688
2652
  try {
1689
- settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
2653
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
1690
2654
  } catch {
1691
2655
  console.warn(
1692
- ` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
2656
+ ` ${import_chalk9.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
1693
2657
  );
1694
- console.warn(` Fix the JSON manually, then re-run ${import_chalk7.default.cyan("viberails init --force")}`);
2658
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk9.default.cyan("viberails init --force")}`);
1695
2659
  return;
1696
2660
  }
1697
2661
  }
@@ -1712,339 +2676,389 @@ function setupClaudeCodeHook(projectRoot) {
1712
2676
  }
1713
2677
  ];
1714
2678
  settings.hooks = hooks;
1715
- fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2679
+ fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1716
2680
  `);
1717
- console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
2681
+ console.log(` ${import_chalk9.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1718
2682
  }
1719
2683
  function setupClaudeMdReference(projectRoot) {
1720
- const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
2684
+ const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
1721
2685
  let content = "";
1722
- if (fs12.existsSync(claudeMdPath)) {
1723
- content = fs12.readFileSync(claudeMdPath, "utf-8");
2686
+ if (fs16.existsSync(claudeMdPath)) {
2687
+ content = fs16.readFileSync(claudeMdPath, "utf-8");
1724
2688
  }
1725
2689
  if (content.includes("@.viberails/context.md")) return;
1726
2690
  const ref = "\n@.viberails/context.md\n";
1727
2691
  const prefix = content.length === 0 ? "" : content.trimEnd();
1728
- fs12.writeFileSync(claudeMdPath, prefix + ref);
1729
- console.log(` ${import_chalk7.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2692
+ fs16.writeFileSync(claudeMdPath, prefix + ref);
2693
+ console.log(` ${import_chalk9.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2694
+ }
2695
+ function setupGithubAction(projectRoot, packageManager) {
2696
+ const workflowDir = path16.join(projectRoot, ".github", "workflows");
2697
+ const workflowPath = path16.join(workflowDir, "viberails.yml");
2698
+ if (fs16.existsSync(workflowPath)) {
2699
+ const existing = fs16.readFileSync(workflowPath, "utf-8");
2700
+ if (existing.includes("viberails")) return void 0;
2701
+ }
2702
+ fs16.mkdirSync(workflowDir, { recursive: true });
2703
+ const pm = packageManager || "npm";
2704
+ const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
2705
+ const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
2706
+ const lines = [
2707
+ "name: viberails",
2708
+ "",
2709
+ "on:",
2710
+ " pull_request:",
2711
+ " branches: [main]",
2712
+ "",
2713
+ "jobs:",
2714
+ " check:",
2715
+ " runs-on: ubuntu-latest",
2716
+ " steps:",
2717
+ " - uses: actions/checkout@v4",
2718
+ " with:",
2719
+ " fetch-depth: 0",
2720
+ ""
2721
+ ];
2722
+ if (pm === "pnpm") {
2723
+ lines.push(" - uses: pnpm/action-setup@v4", "");
2724
+ }
2725
+ lines.push(
2726
+ " - uses: actions/setup-node@v4",
2727
+ " with:",
2728
+ " node-version: 22",
2729
+ pm !== "npm" ? ` cache: ${pm}` : "",
2730
+ "",
2731
+ ` - run: ${installCmd}`,
2732
+ ` - run: ${runPrefix} viberails check --enforce --diff-base origin/\${{ github.event.pull_request.base.ref }}`,
2733
+ ""
2734
+ );
2735
+ const content = lines.filter((l) => l !== void 0).join("\n");
2736
+ fs16.writeFileSync(workflowPath, content);
2737
+ return ".github/workflows/viberails.yml";
1730
2738
  }
1731
2739
  function writeHuskyPreCommit(huskyDir) {
1732
- const hookPath = path12.join(huskyDir, "pre-commit");
1733
- if (fs12.existsSync(hookPath)) {
1734
- const existing = fs12.readFileSync(hookPath, "utf-8");
2740
+ const hookPath = path16.join(huskyDir, "pre-commit");
2741
+ if (fs16.existsSync(hookPath)) {
2742
+ const existing = fs16.readFileSync(hookPath, "utf-8");
1735
2743
  if (!existing.includes("viberails")) {
1736
- fs12.writeFileSync(hookPath, `${existing.trimEnd()}
2744
+ fs16.writeFileSync(hookPath, `${existing.trimEnd()}
1737
2745
  npx viberails check --staged
1738
2746
  `);
1739
2747
  }
1740
2748
  return;
1741
2749
  }
1742
- fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
2750
+ fs16.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1743
2751
  }
1744
2752
 
1745
- // src/commands/init.ts
1746
- var CONFIG_FILE4 = "viberails.config.json";
1747
- function filterHighConfidence(conventions) {
1748
- const filtered = {};
1749
- for (const [key, value] of Object.entries(conventions)) {
1750
- if (value === void 0) continue;
1751
- if (typeof value === "string") {
1752
- filtered[key] = value;
1753
- } else if (value._confidence === "high") {
1754
- filtered[key] = value;
2753
+ // src/commands/init-hooks-extra.ts
2754
+ var fs17 = __toESM(require("fs"), 1);
2755
+ var path17 = __toESM(require("path"), 1);
2756
+ var import_chalk10 = __toESM(require("chalk"), 1);
2757
+ var import_yaml2 = require("yaml");
2758
+ function addPreCommitStep(projectRoot, name, command, marker) {
2759
+ const lefthookPath = path17.join(projectRoot, "lefthook.yml");
2760
+ if (fs17.existsSync(lefthookPath)) {
2761
+ const content = fs17.readFileSync(lefthookPath, "utf-8");
2762
+ if (content.includes(marker)) return void 0;
2763
+ const doc = (0, import_yaml2.parse)(content) ?? {};
2764
+ if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
2765
+ if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
2766
+ doc["pre-commit"].commands[name] = { run: command };
2767
+ fs17.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
2768
+ return "lefthook.yml";
2769
+ }
2770
+ const huskyDir = path17.join(projectRoot, ".husky");
2771
+ if (fs17.existsSync(huskyDir)) {
2772
+ const hookPath = path17.join(huskyDir, "pre-commit");
2773
+ if (fs17.existsSync(hookPath)) {
2774
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2775
+ if (existing.includes(marker)) return void 0;
2776
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2777
+ ${command}
2778
+ `);
2779
+ } else {
2780
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2781
+ ${command}
2782
+ `, { mode: 493 });
2783
+ }
2784
+ return ".husky/pre-commit";
2785
+ }
2786
+ const gitDir = path17.join(projectRoot, ".git");
2787
+ if (fs17.existsSync(gitDir)) {
2788
+ const hooksDir = path17.join(gitDir, "hooks");
2789
+ if (!fs17.existsSync(hooksDir)) fs17.mkdirSync(hooksDir, { recursive: true });
2790
+ const hookPath = path17.join(hooksDir, "pre-commit");
2791
+ if (fs17.existsSync(hookPath)) {
2792
+ const existing = fs17.readFileSync(hookPath, "utf-8");
2793
+ if (existing.includes(marker)) return void 0;
2794
+ fs17.writeFileSync(hookPath, `${existing.trimEnd()}
2795
+
2796
+ # ${name}
2797
+ ${command}
2798
+ `);
2799
+ } else {
2800
+ fs17.writeFileSync(hookPath, `#!/bin/sh
2801
+ # Generated by viberails
2802
+
2803
+ # ${name}
2804
+ ${command}
2805
+ `, {
2806
+ mode: 493
2807
+ });
1755
2808
  }
2809
+ return ".git/hooks/pre-commit";
1756
2810
  }
1757
- return filtered;
2811
+ return void 0;
2812
+ }
2813
+ function setupTypecheckHook(projectRoot) {
2814
+ const target = addPreCommitStep(projectRoot, "typecheck", "npx tsc --noEmit", "tsc");
2815
+ if (target) {
2816
+ console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
2817
+ }
2818
+ return target;
2819
+ }
2820
+ function setupLintHook(projectRoot, linter) {
2821
+ const command = linter === "biome" ? "npx biome check ." : "npx eslint .";
2822
+ const linterName = linter === "biome" ? "Biome" : "ESLint";
2823
+ const target = addPreCommitStep(projectRoot, "lint", command, linter);
2824
+ if (target) {
2825
+ console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
2826
+ }
2827
+ return target;
2828
+ }
2829
+ function setupSelectedIntegrations(projectRoot, integrations, opts) {
2830
+ const created = [];
2831
+ if (integrations.preCommitHook) {
2832
+ const t = setupPreCommitHook(projectRoot);
2833
+ created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
2834
+ }
2835
+ if (integrations.typecheckHook) {
2836
+ const t = setupTypecheckHook(projectRoot);
2837
+ if (t) created.push(`${t} \u2014 added typecheck`);
2838
+ }
2839
+ if (integrations.lintHook && opts.linter) {
2840
+ const t = setupLintHook(projectRoot, opts.linter);
2841
+ if (t) created.push(`${t} \u2014 added lint check`);
2842
+ }
2843
+ if (integrations.claudeCodeHook) {
2844
+ setupClaudeCodeHook(projectRoot);
2845
+ created.push(".claude/settings.json \u2014 added viberails hook");
2846
+ }
2847
+ if (integrations.claudeMdRef) {
2848
+ setupClaudeMdReference(projectRoot);
2849
+ created.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
2850
+ }
2851
+ if (integrations.githubAction) {
2852
+ const t = setupGithubAction(projectRoot, opts.packageManager ?? "npm");
2853
+ if (t) created.push(`${t} \u2014 blocks PRs on violations`);
2854
+ }
2855
+ return created;
1758
2856
  }
1759
- function getConventionStr3(cv) {
1760
- if (!cv) return void 0;
1761
- return typeof cv === "string" ? cv : cv.value;
2857
+
2858
+ // src/commands/init.ts
2859
+ var CONFIG_FILE5 = "viberails.config.json";
2860
+ function getExemptedPackages(config) {
2861
+ return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
1762
2862
  }
1763
2863
  async function initCommand(options, cwd) {
1764
- const startDir = cwd ?? process.cwd();
1765
- const projectRoot = findProjectRoot(startDir);
2864
+ const projectRoot = findProjectRoot(cwd ?? process.cwd());
1766
2865
  if (!projectRoot) {
1767
2866
  throw new Error(
1768
- "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"
2867
+ "No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
1769
2868
  );
1770
2869
  }
1771
- const configPath = path13.join(projectRoot, CONFIG_FILE4);
1772
- if (fs13.existsSync(configPath) && !options.force) {
2870
+ const configPath = path18.join(projectRoot, CONFIG_FILE5);
2871
+ if (fs18.existsSync(configPath) && !options.force) {
1773
2872
  console.log(
1774
- `${import_chalk8.default.yellow("!")} viberails is already initialized.
1775
- Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
2873
+ `${import_chalk11.default.yellow("!")} viberails is already initialized.
2874
+ Run ${import_chalk11.default.cyan("viberails config")} to edit rules, ${import_chalk11.default.cyan("viberails sync")} to update, or ${import_chalk11.default.cyan("viberails init --force")} to start fresh.`
1776
2875
  );
1777
2876
  return;
1778
2877
  }
1779
- if (options.yes) {
1780
- console.log(import_chalk8.default.dim("Scanning project..."));
1781
- const scanResult2 = await (0, import_scanner.scan)(projectRoot);
1782
- const config2 = (0, import_config4.generateConfig)(scanResult2);
1783
- config2.conventions = filterHighConfidence(config2.conventions);
1784
- displayScanResults(scanResult2);
1785
- displayRulesPreview(config2);
1786
- if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
1787
- console.log(import_chalk8.default.dim("Building import graph..."));
1788
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1789
- const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
1790
- const graph = await buildImportGraph(projectRoot, {
1791
- packages,
1792
- ignore: config2.ignore
1793
- });
1794
- const inferred = inferBoundaries(graph);
1795
- const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1796
- if (denyCount > 0) {
1797
- config2.boundaries = inferred;
1798
- config2.rules.enforceBoundaries = true;
1799
- console.log(` Inferred ${denyCount} boundary rules`);
1800
- }
1801
- }
1802
- fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
2878
+ if (options.yes) return initNonInteractive(projectRoot, configPath);
2879
+ await initInteractive(projectRoot, configPath, options);
2880
+ }
2881
+ async function initNonInteractive(projectRoot, configPath) {
2882
+ console.log(import_chalk11.default.dim("Scanning project..."));
2883
+ const scanResult = await (0, import_scanner2.scan)(projectRoot);
2884
+ const config = (0, import_config8.generateConfig)(scanResult);
2885
+ for (const pkg of config.packages) {
2886
+ const pkgMeta = config._meta?.packages?.[pkg.path]?.conventions;
2887
+ pkg.conventions = filterHighConfidence(pkg.conventions ?? {}, pkgMeta);
2888
+ }
2889
+ displayMissingPrereqs(checkCoveragePrereqs(projectRoot, scanResult));
2890
+ displayScanResults(scanResult);
2891
+ displayRulesPreview(config);
2892
+ const exempted = getExemptedPackages(config);
2893
+ if (exempted.length > 0) {
2894
+ console.log(
2895
+ ` ${import_chalk11.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk11.default.dim("(types-only)")}`
2896
+ );
2897
+ }
2898
+ if (config.packages.length > 1) {
2899
+ console.log(import_chalk11.default.dim("Building import graph..."));
2900
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
2901
+ const packages = resolveWorkspacePackages(projectRoot, config.packages);
2902
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
2903
+ const inferred = inferBoundaries(graph);
2904
+ const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
2905
+ if (denyCount > 0) {
2906
+ config.boundaries = inferred;
2907
+ config.rules.enforceBoundaries = true;
2908
+ console.log(` Inferred ${denyCount} boundary rules`);
2909
+ }
2910
+ }
2911
+ const compacted = (0, import_config8.compactConfig)(config);
2912
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
1803
2913
  `);
1804
- writeGeneratedFiles(projectRoot, config2, scanResult2);
1805
- updateGitignore(projectRoot);
1806
- setupClaudeMdReference(projectRoot);
1807
- console.log(`
1808
- Created:`);
1809
- console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
1810
- console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
1811
- console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
1812
- return;
2914
+ writeGeneratedFiles(projectRoot, config, scanResult);
2915
+ updateGitignore(projectRoot);
2916
+ setupClaudeCodeHook(projectRoot);
2917
+ setupClaudeMdReference(projectRoot);
2918
+ const rootPkg = config.packages[0];
2919
+ const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
2920
+ const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
2921
+ const hookManager = detectHookManager(projectRoot);
2922
+ const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
2923
+ const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
2924
+ const linter = rootPkg?.stack?.linter?.split("@")[0];
2925
+ const ok = import_chalk11.default.green("\u2713");
2926
+ const created = [
2927
+ `${ok} ${path18.basename(configPath)}`,
2928
+ `${ok} .viberails/context.md`,
2929
+ `${ok} .viberails/scan-result.json`,
2930
+ `${ok} .claude/settings.json \u2014 added viberails hook`,
2931
+ `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
2932
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk11.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
2933
+ actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
2934
+ ].filter(Boolean);
2935
+ if (hasHookManager && rootPkg?.stack?.language === "typescript") setupTypecheckHook(projectRoot);
2936
+ if (hasHookManager && linter) setupLintHook(projectRoot, linter);
2937
+ console.log(`
2938
+ Created:
2939
+ ${created.map((f) => ` ${f}`).join("\n")}`);
2940
+ }
2941
+ async function initInteractive(projectRoot, configPath, options) {
2942
+ clack8.intro("viberails");
2943
+ if (fs18.existsSync(configPath) && options.force) {
2944
+ const replace = await confirmDangerous(
2945
+ `${path18.basename(configPath)} already exists and will be replaced. Continue?`
2946
+ );
2947
+ if (!replace) {
2948
+ clack8.outro("Aborted. No files were written.");
2949
+ return;
2950
+ }
1813
2951
  }
1814
- clack2.intro("viberails");
1815
- const s = clack2.spinner();
2952
+ const s = clack8.spinner();
1816
2953
  s.start("Scanning project...");
1817
- const scanResult = await (0, import_scanner.scan)(projectRoot);
1818
- const config = (0, import_config4.generateConfig)(scanResult);
2954
+ const scanResult = await (0, import_scanner2.scan)(projectRoot);
2955
+ const config = (0, import_config8.generateConfig)(scanResult);
1819
2956
  s.stop("Scan complete");
2957
+ const prereqResult = await promptMissingPrereqs(
2958
+ projectRoot,
2959
+ checkCoveragePrereqs(projectRoot, scanResult)
2960
+ );
2961
+ if (prereqResult.disableCoverage) {
2962
+ config.rules.testCoverage = 0;
2963
+ }
1820
2964
  if (scanResult.statistics.totalFiles === 0) {
1821
- clack2.log.warn(
1822
- "No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
2965
+ clack8.log.warn(
2966
+ "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
1823
2967
  );
1824
2968
  }
1825
- const resultsText = formatScanResultsText(scanResult, config);
1826
- clack2.note(resultsText, "Scan results");
2969
+ clack8.note(formatScanResultsText(scanResult), "Scan results");
2970
+ const rulesLines = formatRulesText(config);
2971
+ const exemptedPkgs = getExemptedPackages(config);
2972
+ if (exemptedPkgs.length > 0)
2973
+ rulesLines.push(`Auto-exempted from coverage: ${exemptedPkgs.join(", ")} (types-only)`);
2974
+ clack8.note(rulesLines.join("\n"), "Rules");
1827
2975
  const decision = await promptInitDecision();
1828
2976
  if (decision === "customize") {
2977
+ const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1829
2978
  const overrides = await promptRuleMenu({
1830
2979
  maxFileLines: config.rules.maxFileLines,
1831
- requireTests: config.rules.requireTests,
2980
+ testCoverage: config.rules.testCoverage,
2981
+ enforceMissingTests: config.rules.enforceMissingTests,
1832
2982
  enforceNaming: config.rules.enforceNaming,
1833
- enforcement: config.enforcement,
1834
- fileNamingValue: getConventionStr3(config.conventions.fileNaming),
2983
+ fileNamingValue: rootPkg.conventions?.fileNaming,
2984
+ coverageSummaryPath: "coverage/coverage-summary.json",
2985
+ coverageCommand: config.defaults?.coverage?.command,
1835
2986
  packageOverrides: config.packages
1836
2987
  });
1837
- config.rules.maxFileLines = overrides.maxFileLines;
1838
- config.rules.requireTests = overrides.requireTests;
1839
- config.rules.enforceNaming = overrides.enforceNaming;
1840
- config.enforcement = overrides.enforcement;
2988
+ applyRuleOverrides(config, overrides);
1841
2989
  }
1842
- if (config.workspace?.packages && config.workspace.packages.length > 0) {
1843
- clack2.note(
2990
+ if (config.packages.length > 1) {
2991
+ clack8.note(
1844
2992
  "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
1845
2993
  "Boundaries"
1846
2994
  );
1847
- const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
2995
+ const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
1848
2996
  if (shouldInfer) {
1849
- const bs = clack2.spinner();
2997
+ const bs = clack8.spinner();
1850
2998
  bs.start("Building import graph...");
1851
2999
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1852
- const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1853
- const graph = await buildImportGraph(projectRoot, {
1854
- packages,
1855
- ignore: config.ignore
1856
- });
3000
+ const packages = resolveWorkspacePackages(projectRoot, config.packages);
3001
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1857
3002
  const inferred = inferBoundaries(graph);
1858
3003
  const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
1859
3004
  if (denyCount > 0) {
1860
3005
  config.boundaries = inferred;
1861
3006
  config.rules.enforceBoundaries = true;
1862
3007
  bs.stop(`Inferred ${denyCount} boundary rules`);
3008
+ const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
3009
+ clack8.note(boundaryLines, "Boundary rules");
1863
3010
  } else {
1864
3011
  bs.stop("No boundary rules inferred");
1865
3012
  }
1866
3013
  }
1867
3014
  }
1868
3015
  const hookManager = detectHookManager(projectRoot);
1869
- const integrations = await promptIntegrations(hookManager);
1870
- fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
3016
+ const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
3017
+ const integrations = await promptIntegrations(projectRoot, hookManager, {
3018
+ isTypeScript: rootPkgStack?.language === "typescript",
3019
+ linter: rootPkgStack?.linter?.split("@")[0],
3020
+ packageManager: rootPkgStack?.packageManager
3021
+ });
3022
+ const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
3023
+ if (!shouldWrite) {
3024
+ clack8.outro("Aborted. No files were written.");
3025
+ return;
3026
+ }
3027
+ const compacted = (0, import_config8.compactConfig)(config);
3028
+ fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
1871
3029
  `);
1872
3030
  writeGeneratedFiles(projectRoot, config, scanResult);
1873
3031
  updateGitignore(projectRoot);
1874
3032
  const createdFiles = [
1875
- CONFIG_FILE4,
3033
+ path18.basename(configPath),
1876
3034
  ".viberails/context.md",
1877
- ".viberails/scan-result.json"
3035
+ ".viberails/scan-result.json",
3036
+ ...setupSelectedIntegrations(projectRoot, integrations, {
3037
+ linter: rootPkgStack?.linter?.split("@")[0],
3038
+ packageManager: rootPkgStack?.packageManager
3039
+ })
1878
3040
  ];
1879
- if (integrations.preCommitHook) {
1880
- setupPreCommitHook(projectRoot);
1881
- if (hookManager === "Lefthook") {
1882
- createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1883
- }
1884
- }
1885
- if (integrations.claudeCodeHook) {
1886
- setupClaudeCodeHook(projectRoot);
1887
- createdFiles.push(".claude/settings.json \u2014 added viberails hook");
1888
- }
1889
- if (integrations.claudeMdRef) {
1890
- setupClaudeMdReference(projectRoot);
1891
- createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
1892
- }
1893
- clack2.log.success(`Created:
3041
+ clack8.log.success(`Created:
1894
3042
  ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1895
- clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
1896
- }
1897
- function updateGitignore(projectRoot) {
1898
- const gitignorePath = path13.join(projectRoot, ".gitignore");
1899
- let content = "";
1900
- if (fs13.existsSync(gitignorePath)) {
1901
- content = fs13.readFileSync(gitignorePath, "utf-8");
1902
- }
1903
- if (!content.includes(".viberails/scan-result.json")) {
1904
- const block = "\n# viberails\n.viberails/scan-result.json\n";
1905
- const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1906
- `;
1907
- fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
1908
- }
1909
- }
1910
-
1911
- // src/commands/sync.ts
1912
- var fs14 = __toESM(require("fs"), 1);
1913
- var path14 = __toESM(require("path"), 1);
1914
- var import_config5 = require("@viberails/config");
1915
- var import_scanner2 = require("@viberails/scanner");
1916
- var import_chalk9 = __toESM(require("chalk"), 1);
1917
-
1918
- // src/utils/diff-configs.ts
1919
- var import_types5 = require("@viberails/types");
1920
- function parseStackString(s) {
1921
- const atIdx = s.indexOf("@");
1922
- if (atIdx > 0) {
1923
- return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
1924
- }
1925
- return { name: s };
1926
- }
1927
- function displayStackName(s) {
1928
- const { name, version } = parseStackString(s);
1929
- const allMaps = {
1930
- ...import_types5.FRAMEWORK_NAMES,
1931
- ...import_types5.STYLING_NAMES,
1932
- ...import_types5.ORM_NAMES
1933
- };
1934
- const display = allMaps[name] ?? name;
1935
- return version ? `${display} ${version}` : display;
1936
- }
1937
- function conventionStr(cv) {
1938
- return typeof cv === "string" ? cv : cv.value;
1939
- }
1940
- function isDetected(cv) {
1941
- return typeof cv !== "string" && cv._detected === true;
1942
- }
1943
- var STACK_FIELDS = [
1944
- "framework",
1945
- "styling",
1946
- "backend",
1947
- "orm",
1948
- "linter",
1949
- "formatter",
1950
- "testRunner"
1951
- ];
1952
- var CONVENTION_KEYS = [
1953
- "fileNaming",
1954
- "componentNaming",
1955
- "hookNaming",
1956
- "importAlias"
1957
- ];
1958
- var STRUCTURE_FIELDS = [
1959
- { key: "srcDir", label: "source directory" },
1960
- { key: "pages", label: "pages directory" },
1961
- { key: "components", label: "components directory" },
1962
- { key: "hooks", label: "hooks directory" },
1963
- { key: "utils", label: "utilities directory" },
1964
- { key: "types", label: "types directory" },
1965
- { key: "tests", label: "tests directory" },
1966
- { key: "testPattern", label: "test pattern" }
1967
- ];
1968
- function diffConfigs(existing, merged) {
1969
- const changes = [];
1970
- for (const field of STACK_FIELDS) {
1971
- const oldVal = existing.stack[field];
1972
- const newVal = merged.stack[field];
1973
- if (!oldVal && newVal) {
1974
- changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
1975
- } else if (oldVal && newVal && oldVal !== newVal) {
1976
- changes.push({
1977
- type: "changed",
1978
- description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
1979
- });
1980
- }
1981
- }
1982
- for (const key of CONVENTION_KEYS) {
1983
- const oldVal = existing.conventions[key];
1984
- const newVal = merged.conventions[key];
1985
- const label = import_types5.CONVENTION_LABELS[key] ?? key;
1986
- if (!oldVal && newVal) {
1987
- changes.push({
1988
- type: "added",
1989
- description: `New convention: ${label} (${conventionStr(newVal)})`
1990
- });
1991
- } else if (oldVal && newVal && isDetected(newVal)) {
1992
- changes.push({
1993
- type: "changed",
1994
- description: `Convention updated: ${label} (${conventionStr(newVal)})`
1995
- });
1996
- }
1997
- }
1998
- for (const { key, label } of STRUCTURE_FIELDS) {
1999
- const oldVal = existing.structure[key];
2000
- const newVal = merged.structure[key];
2001
- if (!oldVal && newVal) {
2002
- changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
2003
- }
2004
- }
2005
- const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
2006
- for (const pkg of merged.packages ?? []) {
2007
- if (!existingPaths.has(pkg.path)) {
2008
- changes.push({ type: "added", description: `New package: ${pkg.path}` });
2009
- }
2010
- }
2011
- const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
2012
- const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
2013
- for (const pkg of mergedWsPkgs) {
2014
- if (!existingWsPkgs.has(pkg)) {
2015
- changes.push({ type: "added", description: `Workspace: added ${pkg}` });
2016
- }
2017
- }
2018
- for (const pkg of existingWsPkgs) {
2019
- if (!mergedWsPkgs.has(pkg)) {
2020
- changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
2021
- }
2022
- }
2023
- return changes;
2024
- }
2025
- function formatStatsDelta(oldStats, newStats) {
2026
- const fileDelta = newStats.totalFiles - oldStats.totalFiles;
2027
- const lineDelta = newStats.totalLines - oldStats.totalLines;
2028
- if (fileDelta === 0 && lineDelta === 0) return void 0;
2029
- const parts = [];
2030
- if (fileDelta !== 0) {
2031
- const sign = fileDelta > 0 ? "+" : "";
2032
- parts.push(`${sign}${fileDelta.toLocaleString()} files`);
2033
- }
2034
- if (lineDelta !== 0) {
2035
- const sign = lineDelta > 0 ? "+" : "";
2036
- parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
2037
- }
2038
- return `${parts.join(", ")} since last sync`;
3043
+ clack8.outro(
3044
+ `Done! Next: review viberails.config.json, then run viberails check
3045
+ ${import_chalk11.default.dim("Tip: use")} ${import_chalk11.default.cyan("viberails check --enforce")} ${import_chalk11.default.dim("in CI to block PRs on violations.")}`
3046
+ );
2039
3047
  }
2040
3048
 
2041
3049
  // src/commands/sync.ts
2042
- var CONFIG_FILE5 = "viberails.config.json";
3050
+ var fs19 = __toESM(require("fs"), 1);
3051
+ var path19 = __toESM(require("path"), 1);
3052
+ var clack9 = __toESM(require("@clack/prompts"), 1);
3053
+ var import_config9 = require("@viberails/config");
3054
+ var import_scanner3 = require("@viberails/scanner");
3055
+ var import_chalk12 = __toESM(require("chalk"), 1);
3056
+ var CONFIG_FILE6 = "viberails.config.json";
2043
3057
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
2044
3058
  function loadPreviousStats(projectRoot) {
2045
- const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
3059
+ const scanResultPath = path19.join(projectRoot, SCAN_RESULT_FILE2);
2046
3060
  try {
2047
- const raw = fs14.readFileSync(scanResultPath, "utf-8");
3061
+ const raw = fs19.readFileSync(scanResultPath, "utf-8");
2048
3062
  const parsed = JSON.parse(raw);
2049
3063
  if (parsed?.statistics?.totalFiles !== void 0) {
2050
3064
  return parsed.statistics;
@@ -2053,7 +3067,7 @@ function loadPreviousStats(projectRoot) {
2053
3067
  }
2054
3068
  return void 0;
2055
3069
  }
2056
- async function syncCommand(cwd) {
3070
+ async function syncCommand(options, cwd) {
2057
3071
  const startDir = cwd ?? process.cwd();
2058
3072
  const projectRoot = findProjectRoot(startDir);
2059
3073
  if (!projectRoot) {
@@ -2061,44 +3075,85 @@ async function syncCommand(cwd) {
2061
3075
  "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"
2062
3076
  );
2063
3077
  }
2064
- const configPath = path14.join(projectRoot, CONFIG_FILE5);
2065
- const existing = await (0, import_config5.loadConfig)(configPath);
3078
+ const configPath = path19.join(projectRoot, CONFIG_FILE6);
3079
+ const existing = await (0, import_config9.loadConfig)(configPath);
2066
3080
  const previousStats = loadPreviousStats(projectRoot);
2067
- console.log(import_chalk9.default.dim("Scanning project..."));
2068
- const scanResult = await (0, import_scanner2.scan)(projectRoot);
2069
- const merged = (0, import_config5.mergeConfig)(existing, scanResult);
2070
- const existingJson = JSON.stringify(existing, null, 2);
2071
- const mergedJson = JSON.stringify(merged, null, 2);
2072
- const configChanged = existingJson !== mergedJson;
3081
+ console.log(import_chalk12.default.dim("Scanning project..."));
3082
+ const scanResult = await (0, import_scanner3.scan)(projectRoot);
3083
+ const merged = (0, import_config9.mergeConfig)(existing, scanResult);
3084
+ const compacted = (0, import_config9.compactConfig)(merged);
3085
+ const compactedJson = JSON.stringify(compacted, null, 2);
3086
+ const rawDisk = fs19.readFileSync(configPath, "utf-8").trim();
3087
+ const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3088
+ const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
3089
+ const configChanged = diskWithoutSync !== mergedWithoutSync;
2073
3090
  const changes = configChanged ? diffConfigs(existing, merged) : [];
2074
3091
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
2075
3092
  if (changes.length > 0 || statsDelta) {
2076
3093
  console.log(`
2077
- ${import_chalk9.default.bold("Changes:")}`);
3094
+ ${import_chalk12.default.bold("Changes:")}`);
2078
3095
  for (const change of changes) {
2079
- const icon = change.type === "removed" ? import_chalk9.default.red("-") : import_chalk9.default.green("+");
3096
+ const icon = change.type === "removed" ? import_chalk12.default.red("-") : import_chalk12.default.green("+");
2080
3097
  console.log(` ${icon} ${change.description}`);
2081
3098
  }
2082
3099
  if (statsDelta) {
2083
- console.log(` ${import_chalk9.default.dim(statsDelta)}`);
3100
+ console.log(` ${import_chalk12.default.dim(statsDelta)}`);
3101
+ }
3102
+ }
3103
+ if (options?.interactive) {
3104
+ clack9.intro("viberails sync (interactive)");
3105
+ clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
3106
+ const decision = await clack9.select({
3107
+ message: "How would you like to proceed?",
3108
+ options: [
3109
+ { value: "accept", label: "Accept changes" },
3110
+ { value: "customize", label: "Customize rules" },
3111
+ { value: "cancel", label: "Cancel (no changes written)" }
3112
+ ]
3113
+ });
3114
+ assertNotCancelled(decision);
3115
+ if (decision === "cancel") {
3116
+ clack9.outro("Sync cancelled. No files were written.");
3117
+ return;
3118
+ }
3119
+ if (decision === "customize") {
3120
+ const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
3121
+ const overrides = await promptRuleMenu({
3122
+ maxFileLines: merged.rules.maxFileLines,
3123
+ testCoverage: merged.rules.testCoverage,
3124
+ enforceMissingTests: merged.rules.enforceMissingTests,
3125
+ enforceNaming: merged.rules.enforceNaming,
3126
+ fileNamingValue: rootPkg.conventions?.fileNaming,
3127
+ coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3128
+ coverageCommand: merged.defaults?.coverage?.command,
3129
+ packageOverrides: merged.packages
3130
+ });
3131
+ applyRuleOverrides(merged, overrides);
3132
+ const recompacted = (0, import_config9.compactConfig)(merged);
3133
+ fs19.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
3134
+ `);
3135
+ writeGeneratedFiles(projectRoot, merged, scanResult);
3136
+ clack9.log.success("Updated config with your customizations.");
3137
+ clack9.outro("Done! Run viberails check to verify.");
3138
+ return;
2084
3139
  }
2085
3140
  }
2086
- fs14.writeFileSync(configPath, `${mergedJson}
3141
+ fs19.writeFileSync(configPath, `${compactedJson}
2087
3142
  `);
2088
3143
  writeGeneratedFiles(projectRoot, merged, scanResult);
2089
3144
  console.log(`
2090
- ${import_chalk9.default.bold("Synced:")}`);
3145
+ ${import_chalk12.default.bold("Synced:")}`);
2091
3146
  if (configChanged) {
2092
- console.log(` ${import_chalk9.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
3147
+ console.log(` ${import_chalk12.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
2093
3148
  } else {
2094
- console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
3149
+ console.log(` ${import_chalk12.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
2095
3150
  }
2096
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
2097
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
3151
+ console.log(` ${import_chalk12.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
3152
+ console.log(` ${import_chalk12.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
2098
3153
  }
2099
3154
 
2100
3155
  // src/index.ts
2101
- var VERSION = "0.4.0";
3156
+ var VERSION = "0.5.1";
2102
3157
  var program = new import_commander.Command();
2103
3158
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
2104
3159
  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) => {
@@ -2106,20 +3161,29 @@ program.command("init", { isDefault: true }).description("Scan your project and
2106
3161
  await initCommand(options);
2107
3162
  } catch (err) {
2108
3163
  const message = err instanceof Error ? err.message : String(err);
2109
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
3164
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
3165
+ process.exit(1);
3166
+ }
3167
+ });
3168
+ program.command("sync").description("Re-scan and update generated files").option("-i, --interactive", "Review changes before writing").action(async (options) => {
3169
+ try {
3170
+ await syncCommand(options);
3171
+ } catch (err) {
3172
+ const message = err instanceof Error ? err.message : String(err);
3173
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2110
3174
  process.exit(1);
2111
3175
  }
2112
3176
  });
2113
- program.command("sync").description("Re-scan and update generated files").action(async () => {
3177
+ program.command("config").description("Interactively edit existing config rules").option("--rescan", "Re-scan project first (picks up new packages, stack changes)").action(async (options) => {
2114
3178
  try {
2115
- await syncCommand();
3179
+ await configCommand(options);
2116
3180
  } catch (err) {
2117
3181
  const message = err instanceof Error ? err.message : String(err);
2118
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
3182
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2119
3183
  process.exit(1);
2120
3184
  }
2121
3185
  });
2122
- program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").action(
3186
+ program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--diff-base <ref>", "Only check files changed since <ref> (for CI on PRs)").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").option("--enforce", "Exit with error on violations (for CI)").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").action(
2123
3187
  async (options) => {
2124
3188
  try {
2125
3189
  if (options.hook) {
@@ -2128,13 +3192,15 @@ program.command("check").description("Check files against enforced rules").optio
2128
3192
  }
2129
3193
  const exitCode = await checkCommand({
2130
3194
  ...options,
3195
+ diffBase: options.diffBase,
3196
+ enforce: options.enforce,
2131
3197
  noBoundaries: options.boundaries === false,
2132
3198
  format: options.format === "json" ? "json" : "text"
2133
3199
  });
2134
3200
  process.exit(exitCode);
2135
3201
  } catch (err) {
2136
3202
  const message = err instanceof Error ? err.message : String(err);
2137
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
3203
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2138
3204
  process.exit(1);
2139
3205
  }
2140
3206
  }
@@ -2145,7 +3211,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
2145
3211
  process.exit(exitCode);
2146
3212
  } catch (err) {
2147
3213
  const message = err instanceof Error ? err.message : String(err);
2148
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
3214
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2149
3215
  process.exit(1);
2150
3216
  }
2151
3217
  });
@@ -2154,7 +3220,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
2154
3220
  await boundariesCommand(options);
2155
3221
  } catch (err) {
2156
3222
  const message = err instanceof Error ? err.message : String(err);
2157
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
3223
+ console.error(`${import_chalk13.default.red("Error:")} ${message}`);
2158
3224
  process.exit(1);
2159
3225
  }
2160
3226
  });