viberails 0.6.10 → 0.6.12

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_chalk18 = __toESM(require("chalk"), 1);
37
+ var import_chalk20 = __toESM(require("chalk"), 1);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/boundaries.ts
@@ -99,8 +99,8 @@ var FILE_NAMING_OPTIONS = [
99
99
  { value: "snake_case", label: "snake_case" }
100
100
  ];
101
101
  var COMPONENT_NAMING_OPTIONS = [
102
- { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
103
- { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
102
+ { value: "PascalCase", label: "PascalCase", hint: "e.g. MyComponent, UserProfile" },
103
+ { value: "camelCase", label: "camelCase", hint: "e.g. myComponent, userProfile" }
104
104
  ];
105
105
  var HOOK_NAMING_OPTIONS = [
106
106
  { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
@@ -162,12 +162,12 @@ async function promptNamingMenu(state) {
162
162
  },
163
163
  {
164
164
  value: "componentNaming",
165
- label: `${state.componentNaming ? ok : unset} Component naming`,
165
+ label: `${state.componentNaming ? ok : unset} Component exports`,
166
166
  hint: state.componentNaming ?? HINT_NOT_SET
167
167
  },
168
168
  {
169
169
  value: "hookNaming",
170
- label: `${state.hookNaming ? ok : unset} Hook naming`,
170
+ label: `${state.hookNaming ? ok : unset} Hook exports`,
171
171
  hint: state.hookNaming ?? HINT_NOT_SET
172
172
  },
173
173
  {
@@ -204,7 +204,7 @@ async function promptNamingMenu(state) {
204
204
  }
205
205
  if (choice === "componentNaming") {
206
206
  const selected = await clack.select({
207
- message: "Component naming convention",
207
+ message: "Component export naming (e.g. UserProfile)",
208
208
  options: [
209
209
  ...COMPONENT_NAMING_OPTIONS,
210
210
  { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
@@ -216,7 +216,7 @@ async function promptNamingMenu(state) {
216
216
  }
217
217
  if (choice === "hookNaming") {
218
218
  const selected = await clack.select({
219
- message: "Hook naming convention",
219
+ message: "Hook export naming (e.g. useAuth)",
220
220
  options: [
221
221
  ...HOOK_NAMING_OPTIONS,
222
222
  { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
@@ -1452,9 +1452,9 @@ async function checkCommand(options, cwd) {
1452
1452
  }
1453
1453
  const violations = [];
1454
1454
  const severity = options.enforce ? "error" : "warn";
1455
- const log9 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk5.default.dim(msg)) : () => {
1455
+ const log10 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk5.default.dim(msg)) : () => {
1456
1456
  };
1457
- log9(" Checking files...");
1457
+ log10(" Checking files...");
1458
1458
  for (const file of filesToCheck) {
1459
1459
  const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
1460
1460
  const relPath = path7.relative(projectRoot, absPath);
@@ -1487,9 +1487,9 @@ async function checkCommand(options, cwd) {
1487
1487
  }
1488
1488
  }
1489
1489
  }
1490
- log9(" done\n");
1490
+ log10(" done\n");
1491
1491
  if (!options.files) {
1492
- log9(" Checking missing tests...");
1492
+ log10(" Checking missing tests...");
1493
1493
  const testViolations = checkMissingTests(projectRoot, config, severity);
1494
1494
  if (options.staged) {
1495
1495
  const stagedSet = new Set(filesToCheck);
@@ -1502,14 +1502,14 @@ async function checkCommand(options, cwd) {
1502
1502
  } else {
1503
1503
  violations.push(...testViolations);
1504
1504
  }
1505
- log9(" done\n");
1505
+ log10(" done\n");
1506
1506
  }
1507
1507
  if (!options.files && !options.staged && !options.diffBase) {
1508
- log9(" Running test coverage...\n");
1508
+ log10(" Running test coverage...\n");
1509
1509
  const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
1510
1510
  staged: options.staged,
1511
1511
  enforce: options.enforce,
1512
- onProgress: (pkg) => log9(` Coverage: ${pkg}...
1512
+ onProgress: (pkg) => log10(` Coverage: ${pkg}...
1513
1513
  `)
1514
1514
  });
1515
1515
  violations.push(...coverageViolations);
@@ -1534,7 +1534,7 @@ async function checkCommand(options, cwd) {
1534
1534
  severity
1535
1535
  });
1536
1536
  }
1537
- log9(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1537
+ log10(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
1538
1538
  `);
1539
1539
  }
1540
1540
  if (options.format === "json") {
@@ -2816,10 +2816,10 @@ ${import_chalk10.default.yellow("!")} No safe fixes to apply. Resolve aliased im
2816
2816
  // src/commands/init.ts
2817
2817
  var fs21 = __toESM(require("fs"), 1);
2818
2818
  var path21 = __toESM(require("path"), 1);
2819
- var clack13 = __toESM(require("@clack/prompts"), 1);
2819
+ var clack14 = __toESM(require("@clack/prompts"), 1);
2820
2820
  var import_config9 = require("@viberails/config");
2821
2821
  var import_scanner3 = require("@viberails/scanner");
2822
- var import_chalk16 = __toESM(require("chalk"), 1);
2822
+ var import_chalk18 = __toESM(require("chalk"), 1);
2823
2823
 
2824
2824
  // src/utils/check-prerequisites.ts
2825
2825
  var fs14 = __toESM(require("fs"), 1);
@@ -2926,15 +2926,7 @@ async function executeDeferredInstalls(projectRoot, installs) {
2926
2926
  return successCount;
2927
2927
  }
2928
2928
 
2929
- // src/utils/prompt-main-menu.ts
2930
- var clack11 = __toESM(require("@clack/prompts"), 1);
2931
-
2932
- // src/utils/prompt-main-menu-handlers.ts
2933
- var clack10 = __toESM(require("@clack/prompts"), 1);
2934
-
2935
2929
  // src/utils/prompt-integrations.ts
2936
- var fs15 = __toESM(require("fs"), 1);
2937
- var path15 = __toESM(require("path"), 1);
2938
2930
  var clack9 = __toESM(require("@clack/prompts"), 1);
2939
2931
  function buildLefthookInstallCommand(pm, isWorkspace) {
2940
2932
  if (pm === "yarn") return "yarn add -D lefthook";
@@ -2942,26 +2934,24 @@ function buildLefthookInstallCommand(pm, isWorkspace) {
2942
2934
  if (pm === "npm") return "npm install -D lefthook";
2943
2935
  return `${pm} add -D lefthook`;
2944
2936
  }
2945
- async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
2937
+ async function promptIntegrationsDeferred(hookManager, tools) {
2938
+ const hasHookManager = !!hookManager;
2946
2939
  const options = [];
2947
- const needsLefthook = !hookManager;
2948
- if (needsLefthook) {
2949
- const pm = packageManager ?? "npm";
2950
- options.push({
2951
- value: "installLefthook",
2952
- label: "Install Lefthook",
2953
- hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
2954
- });
2955
- }
2956
- const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
2957
- const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
2940
+ const hookLabel = hasHookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
2941
+ const hookHint = hasHookManager ? "runs viberails checks when you commit" : "local hook only \u2014 use lefthook or husky to commit hooks to repo";
2958
2942
  options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
2959
- if (tools?.isTypeScript) {
2943
+ if (tools?.typecheckLabel) {
2960
2944
  options.push({
2961
2945
  value: "typecheck",
2962
- label: "Typecheck (tsc --noEmit)",
2946
+ label: `Typecheck (${tools.typecheckLabel})`,
2963
2947
  hint: "pre-commit hook + CI check"
2964
2948
  });
2949
+ } else if (tools?.isTypeScript) {
2950
+ options.push({
2951
+ value: "typecheck",
2952
+ label: "Typecheck",
2953
+ hint: "needs root tsconfig.json, typecheck script, or turbo task"
2954
+ });
2965
2955
  }
2966
2956
  if (tools?.linter) {
2967
2957
  const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
@@ -2988,7 +2978,12 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
2988
2978
  hint: "blocks PRs that fail viberails check"
2989
2979
  }
2990
2980
  );
2991
- const initialValues = options.map((o) => o.value);
2981
+ const hasTypecheck = !!tools?.typecheckLabel;
2982
+ const initialValues = options.filter((o) => {
2983
+ if (o.value === "preCommit" && !hasHookManager) return false;
2984
+ if (o.value === "typecheck" && !hasTypecheck) return false;
2985
+ return true;
2986
+ }).map((o) => o.value);
2992
2987
  const result = await clack9.multiselect({
2993
2988
  message: "Integrations",
2994
2989
  options,
@@ -2996,20 +2991,6 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
2996
2991
  required: false
2997
2992
  });
2998
2993
  assertNotCancelled(result);
2999
- let lefthookInstall;
3000
- if (needsLefthook && result.includes("installLefthook")) {
3001
- const pm = packageManager ?? "npm";
3002
- lefthookInstall = {
3003
- label: "Lefthook",
3004
- command: buildLefthookInstallCommand(pm, isWorkspace),
3005
- onSuccess: projectRoot ? () => {
3006
- const ymlPath = path15.join(projectRoot, "lefthook.yml");
3007
- if (!fs15.existsSync(ymlPath)) {
3008
- fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
3009
- }
3010
- } : void 0
3011
- };
3012
- }
3013
2994
  return {
3014
2995
  choice: {
3015
2996
  preCommitHook: result.includes("preCommit"),
@@ -3018,39 +2999,178 @@ async function promptIntegrationsDeferred(hookManager, tools, packageManager, is
3018
2999
  githubAction: result.includes("githubAction"),
3019
3000
  typecheckHook: result.includes("typecheck"),
3020
3001
  lintHook: result.includes("lint")
3021
- },
3022
- lefthookInstall
3002
+ }
3023
3003
  };
3024
3004
  }
3025
3005
 
3006
+ // src/utils/prompt-main-menu.ts
3007
+ var clack11 = __toESM(require("@clack/prompts"), 1);
3008
+
3026
3009
  // src/utils/prompt-main-menu-handlers.ts
3027
- async function handleAdvancedNaming(config) {
3010
+ var clack10 = __toESM(require("@clack/prompts"), 1);
3011
+ var import_chalk13 = __toESM(require("chalk"), 1);
3012
+
3013
+ // src/utils/prompt-main-menu-hints.ts
3014
+ var import_chalk12 = __toESM(require("chalk"), 1);
3015
+ function fileLimitsHint(config) {
3016
+ const max = config.rules.maxFileLines;
3017
+ const test = config.rules.maxTestFileLines;
3018
+ return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
3019
+ }
3020
+ function getEffectiveFileNaming(config) {
3028
3021
  const rootPkg = getRootPackage(config.packages);
3029
- const state = {
3030
- maxFileLines: config.rules.maxFileLines,
3031
- maxTestFileLines: config.rules.maxTestFileLines,
3032
- testCoverage: config.rules.testCoverage,
3033
- enforceMissingTests: config.rules.enforceMissingTests,
3034
- enforceNaming: config.rules.enforceNaming,
3035
- fileNamingValue: rootPkg.conventions?.fileNaming,
3036
- componentNaming: rootPkg.conventions?.componentNaming,
3037
- hookNaming: rootPkg.conventions?.hookNaming,
3038
- importAlias: rootPkg.conventions?.importAlias,
3039
- coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
3040
- coverageCommand: config.defaults?.coverage?.command
3041
- };
3042
- await promptNamingMenu(state);
3043
- rootPkg.conventions = rootPkg.conventions ?? {};
3044
- config.rules.enforceNaming = state.enforceNaming;
3045
- if (state.fileNamingValue) {
3046
- rootPkg.conventions.fileNaming = state.fileNamingValue;
3047
- } else {
3048
- delete rootPkg.conventions.fileNaming;
3022
+ if (rootPkg.conventions?.fileNaming) {
3023
+ return { naming: rootPkg.conventions.fileNaming, source: "root" };
3024
+ }
3025
+ if (config.packages.length > 1) {
3026
+ const namingValues = config.packages.map((p) => p.conventions?.fileNaming).filter((n) => !!n);
3027
+ if (namingValues.length > 0 && new Set(namingValues).size === 1) {
3028
+ return { naming: namingValues[0], source: "consensus" };
3029
+ }
3030
+ }
3031
+ return void 0;
3032
+ }
3033
+ function fileNamingHint(config, scanResult) {
3034
+ if (!config.rules.enforceNaming) return "not enforced";
3035
+ const effective = getEffectiveFileNaming(config);
3036
+ if (effective) {
3037
+ const detected = scanResult.packages.some(
3038
+ (p) => p.conventions.fileNaming?.value === effective.naming && p.conventions.fileNaming.confidence !== "low"
3039
+ );
3040
+ return detected ? `${effective.naming} (detected)` : effective.naming;
3041
+ }
3042
+ return "not set \u2014 select to configure";
3043
+ }
3044
+ function fileNamingStatus(config) {
3045
+ if (!config.rules.enforceNaming) return "unconfigured";
3046
+ return getEffectiveFileNaming(config) ? "ok" : "needs-input";
3047
+ }
3048
+ function missingTestsHint(config) {
3049
+ if (!config.rules.enforceMissingTests) return "not enforced";
3050
+ const rootPkg = getRootPackage(config.packages);
3051
+ const pattern = rootPkg.structure?.testPattern;
3052
+ return pattern ? `enforced (${pattern})` : "enforced";
3053
+ }
3054
+ function coverageHint(config, hasTestRunner) {
3055
+ if (config.rules.testCoverage === 0) return "disabled";
3056
+ if (!hasTestRunner)
3057
+ return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3058
+ const isMonorepo = config.packages.length > 1;
3059
+ if (isMonorepo) {
3060
+ const withCov = config.packages.filter(
3061
+ (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3062
+ );
3063
+ const exempt = config.packages.length - withCov.length;
3064
+ return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3065
+ }
3066
+ return `${config.rules.testCoverage}%`;
3067
+ }
3068
+ function aiContextHint(config) {
3069
+ const rootPkg = getRootPackage(config.packages);
3070
+ const count = [
3071
+ rootPkg.conventions?.componentNaming,
3072
+ rootPkg.conventions?.hookNaming,
3073
+ rootPkg.conventions?.importAlias
3074
+ ].filter(Boolean).length;
3075
+ if (count === 3) return "all set";
3076
+ if (count > 0) return `${count} of 3 conventions`;
3077
+ return "none set \u2014 optional AI guidelines";
3078
+ }
3079
+ function aiContextStatus(config) {
3080
+ const rootPkg = getRootPackage(config.packages);
3081
+ const count = [
3082
+ rootPkg.conventions?.componentNaming,
3083
+ rootPkg.conventions?.hookNaming,
3084
+ rootPkg.conventions?.importAlias
3085
+ ].filter(Boolean).length;
3086
+ if (count === 3) return "ok";
3087
+ if (count > 0) return "partial";
3088
+ return "unconfigured";
3089
+ }
3090
+ function packageOverridesHint(config) {
3091
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3092
+ const editable = config.packages.filter((p) => p.path !== ".");
3093
+ const customized = editable.filter(
3094
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3095
+ ).length;
3096
+ return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3097
+ }
3098
+ function boundariesHint(config, state) {
3099
+ if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3100
+ const deny = config.boundaries?.deny;
3101
+ if (!deny) return "enabled";
3102
+ const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3103
+ const pkgCount = Object.keys(deny).length;
3104
+ return `${ruleCount} rules across ${pkgCount} packages`;
3105
+ }
3106
+ function packageOverridesStatus(config) {
3107
+ const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3108
+ const editable = config.packages.filter((p) => p.path !== ".");
3109
+ const customized = editable.some(
3110
+ (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3111
+ );
3112
+ return customized ? "ok" : "unconfigured";
3113
+ }
3114
+ function statusIcon(status) {
3115
+ if (status === "ok") return import_chalk12.default.green("\u2713");
3116
+ if (status === "needs-input") return import_chalk12.default.yellow("?");
3117
+ if (status === "unconfigured") return import_chalk12.default.dim("-");
3118
+ return import_chalk12.default.yellow("~");
3119
+ }
3120
+ function buildMainMenuOptions(config, scanResult, state) {
3121
+ const namingStatus = fileNamingStatus(config);
3122
+ const coverageStatus = config.rules.testCoverage === 0 ? "unconfigured" : !state.hasTestRunner ? "partial" : "ok";
3123
+ const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "unconfigured";
3124
+ const options = [
3125
+ {
3126
+ value: "fileLimits",
3127
+ label: `${statusIcon("ok")} Max file size`,
3128
+ hint: fileLimitsHint(config)
3129
+ },
3130
+ {
3131
+ value: "fileNaming",
3132
+ label: `${statusIcon(namingStatus)} File naming`,
3133
+ hint: fileNamingHint(config, scanResult)
3134
+ },
3135
+ {
3136
+ value: "missingTests",
3137
+ label: `${statusIcon(missingTestsStatus)} Missing tests`,
3138
+ hint: missingTestsHint(config)
3139
+ },
3140
+ {
3141
+ value: "coverage",
3142
+ label: `${statusIcon(coverageStatus)} Coverage`,
3143
+ hint: coverageHint(config, state.hasTestRunner)
3144
+ },
3145
+ {
3146
+ value: "aiContext",
3147
+ label: `${statusIcon(aiContextStatus(config))} AI context`,
3148
+ hint: aiContextHint(config)
3149
+ }
3150
+ ];
3151
+ if (config.packages.length > 1) {
3152
+ const bIcon = statusIcon(
3153
+ state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
3154
+ );
3155
+ const poIcon = statusIcon(packageOverridesStatus(config));
3156
+ options.push(
3157
+ {
3158
+ value: "packageOverrides",
3159
+ label: `${poIcon} Per-package overrides`,
3160
+ hint: packageOverridesHint(config)
3161
+ },
3162
+ { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3163
+ );
3049
3164
  }
3050
- rootPkg.conventions.componentNaming = state.componentNaming || void 0;
3051
- rootPkg.conventions.hookNaming = state.hookNaming || void 0;
3052
- rootPkg.conventions.importAlias = state.importAlias || void 0;
3165
+ options.push(
3166
+ { value: "reset", label: " Reset all to defaults" },
3167
+ { value: "review", label: " Review scan details", hint: "detected stack & conventions" },
3168
+ { value: "done", label: " Done \u2014 write config" }
3169
+ );
3170
+ return options;
3053
3171
  }
3172
+
3173
+ // src/utils/prompt-main-menu-handlers.ts
3054
3174
  async function handleFileNaming(config, scanResult) {
3055
3175
  const isMonorepo = config.packages.length > 1;
3056
3176
  if (isMonorepo) {
@@ -3074,10 +3194,11 @@ async function handleFileNaming(config, scanResult) {
3074
3194
  return { value: opt.value, label: opt.label };
3075
3195
  });
3076
3196
  const rootPkg = getRootPackage(config.packages);
3197
+ const effective = getEffectiveFileNaming(config);
3077
3198
  const selected = await clack10.select({
3078
3199
  message: isMonorepo ? "Default file naming convention" : "File naming convention",
3079
3200
  options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
3080
- initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
3201
+ initialValue: effective?.naming ?? SENTINEL_SKIP
3081
3202
  });
3082
3203
  if (isCancelled(selected)) return;
3083
3204
  if (selected === SENTINEL_SKIP) {
@@ -3190,192 +3311,98 @@ async function handleBoundaries(config, state, opts) {
3190
3311
  clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
3191
3312
  }
3192
3313
  }
3193
- async function handleIntegrations(state, opts) {
3194
- const result = await promptIntegrationsDeferred(
3195
- state.hookManager,
3196
- opts.tools,
3197
- opts.tools.packageManager,
3198
- opts.tools.isWorkspace,
3199
- opts.projectRoot
3200
- );
3201
- state.visited.integrations = true;
3202
- state.integrations = result.choice;
3203
- state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
3204
- if (result.lefthookInstall) {
3205
- state.deferredInstalls.push(result.lefthookInstall);
3206
- }
3207
- }
3208
-
3209
- // src/utils/prompt-main-menu-hints.ts
3210
- var import_chalk12 = __toESM(require("chalk"), 1);
3211
- function fileLimitsHint(config) {
3212
- const max = config.rules.maxFileLines;
3213
- const test = config.rules.maxTestFileLines;
3214
- return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
3215
- }
3216
- function fileNamingHint(config, scanResult) {
3217
- const rootPkg = getRootPackage(config.packages);
3218
- const naming = rootPkg.conventions?.fileNaming;
3219
- if (!config.rules.enforceNaming) return "not enforced";
3220
- if (naming) {
3221
- const detected = scanResult.packages.some(
3222
- (p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
3223
- );
3224
- return detected ? `${naming} (detected)` : naming;
3225
- }
3226
- return "mixed \u2014 will not enforce if skipped";
3227
- }
3228
- function fileNamingStatus(config) {
3229
- if (!config.rules.enforceNaming) return "unconfigured";
3230
- const rootPkg = getRootPackage(config.packages);
3231
- return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
3232
- }
3233
- function missingTestsHint(config) {
3234
- if (!config.rules.enforceMissingTests) return "not enforced";
3235
- const rootPkg = getRootPackage(config.packages);
3236
- const pattern = rootPkg.structure?.testPattern;
3237
- return pattern ? `enforced (${pattern})` : "enforced";
3238
- }
3239
- function coverageHint(config, hasTestRunner) {
3240
- if (config.rules.testCoverage === 0) return "disabled";
3241
- if (!hasTestRunner)
3242
- return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
3243
- const isMonorepo = config.packages.length > 1;
3244
- if (isMonorepo) {
3245
- const withCov = config.packages.filter(
3246
- (p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
3247
- );
3248
- const exempt = config.packages.length - withCov.length;
3249
- return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
3250
- }
3251
- return `${config.rules.testCoverage}%`;
3252
- }
3253
- function advancedNamingHint(config) {
3254
- const rootPkg = getRootPackage(config.packages);
3255
- if (!config.rules.enforceNaming) return "not enforced";
3256
- const ok = import_chalk12.default.green("\u2713");
3257
- const no = import_chalk12.default.dim("\u2717");
3258
- const parts = [
3259
- `${rootPkg.conventions?.fileNaming ? ok : no} file naming`,
3260
- `${rootPkg.conventions?.componentNaming ? ok : no} components`,
3261
- `${rootPkg.conventions?.hookNaming ? ok : no} hooks`,
3262
- `${rootPkg.conventions?.importAlias ? ok : no} alias`
3263
- ];
3264
- return parts.join(import_chalk12.default.dim(", "));
3265
- }
3266
- function integrationsHint(state) {
3267
- if (!state.visited.integrations || !state.integrations)
3268
- return "not configured \u2014 select to set up";
3269
- const items = [];
3270
- if (state.integrations.preCommitHook) items.push(import_chalk12.default.green("pre-commit"));
3271
- if (state.integrations.typecheckHook) items.push(import_chalk12.default.green("typecheck"));
3272
- if (state.integrations.lintHook) items.push(import_chalk12.default.green("lint"));
3273
- if (state.integrations.claudeCodeHook) items.push(import_chalk12.default.green("Claude"));
3274
- if (state.integrations.claudeMdRef) items.push(import_chalk12.default.green("CLAUDE.md"));
3275
- if (state.integrations.githubAction) items.push(import_chalk12.default.green("CI"));
3276
- return items.length > 0 ? items.join(import_chalk12.default.dim(" \xB7 ")) : "none selected";
3277
- }
3278
- function packageOverridesHint(config) {
3279
- const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3280
- const editable = config.packages.filter((p) => p.path !== ".");
3281
- const customized = editable.filter(
3282
- (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3283
- ).length;
3284
- return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
3285
- }
3286
- function boundariesHint(config, state) {
3287
- if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
3288
- const deny = config.boundaries?.deny;
3289
- if (!deny) return "enabled";
3290
- const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
3291
- const pkgCount = Object.keys(deny).length;
3292
- return `${ruleCount} rules across ${pkgCount} packages`;
3293
- }
3294
- function advancedNamingStatus(config) {
3295
- if (!config.rules.enforceNaming) return "unconfigured";
3314
+ async function handleAiContext(config) {
3296
3315
  const rootPkg = getRootPackage(config.packages);
3297
- const hasFile = !!rootPkg.conventions?.fileNaming;
3298
- const hasComp = !!rootPkg.conventions?.componentNaming;
3299
- const hasHook = !!rootPkg.conventions?.hookNaming;
3300
- const hasAlias = !!rootPkg.conventions?.importAlias;
3301
- if (hasFile && hasComp && hasHook && hasAlias) return "ok";
3302
- if (hasFile || hasComp || hasHook || hasAlias) return "partial";
3303
- return "unconfigured";
3304
- }
3305
- function packageOverridesStatus(config) {
3306
- const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
3307
- const editable = config.packages.filter((p) => p.path !== ".");
3308
- const customized = editable.some(
3309
- (p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
3310
- );
3311
- return customized ? "ok" : "unconfigured";
3312
- }
3313
- function statusIcon(status) {
3314
- if (status === "ok") return import_chalk12.default.green("\u2713");
3315
- if (status === "needs-input") return import_chalk12.default.yellow("?");
3316
- if (status === "unconfigured") return import_chalk12.default.dim("-");
3317
- return import_chalk12.default.yellow("~");
3318
- }
3319
- function buildMainMenuOptions(config, scanResult, state) {
3320
- const namingStatus = fileNamingStatus(config);
3321
- const coverageStatus = config.rules.testCoverage === 0 ? "unconfigured" : !state.hasTestRunner ? "partial" : "ok";
3322
- const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "unconfigured";
3323
- const options = [
3324
- {
3325
- value: "fileLimits",
3326
- label: `${statusIcon("ok")} Max file size`,
3327
- hint: fileLimitsHint(config)
3328
- },
3329
- {
3330
- value: "fileNaming",
3331
- label: `${statusIcon(namingStatus)} Default file naming`,
3332
- hint: fileNamingHint(config, scanResult)
3333
- },
3334
- {
3335
- value: "missingTests",
3336
- label: `${statusIcon(missingTestsStatus)} Missing tests`,
3337
- hint: missingTestsHint(config)
3338
- },
3339
- {
3340
- value: "coverage",
3341
- label: `${statusIcon(coverageStatus)} Coverage`,
3342
- hint: coverageHint(config, state.hasTestRunner)
3343
- },
3344
- {
3345
- value: "advancedNaming",
3346
- label: `${statusIcon(advancedNamingStatus(config))} Advanced naming`,
3347
- hint: advancedNamingHint(config)
3348
- }
3349
- ];
3350
- if (config.packages.length > 1) {
3351
- const bIcon = statusIcon(
3352
- state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
3353
- );
3354
- const poIcon = statusIcon(packageOverridesStatus(config));
3355
- options.push(
3316
+ rootPkg.conventions = rootPkg.conventions ?? {};
3317
+ while (true) {
3318
+ const ok = import_chalk13.default.green("\u2713");
3319
+ const unset = import_chalk13.default.dim("-");
3320
+ const options = [
3356
3321
  {
3357
- value: "packageOverrides",
3358
- label: `${poIcon} Per-package overrides`,
3359
- hint: packageOverridesHint(config)
3322
+ value: "componentNaming",
3323
+ label: `${rootPkg.conventions.componentNaming ? ok : unset} Component exports`,
3324
+ hint: rootPkg.conventions.componentNaming ?? "not set"
3360
3325
  },
3361
- { value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
3362
- );
3326
+ {
3327
+ value: "hookNaming",
3328
+ label: `${rootPkg.conventions.hookNaming ? ok : unset} Hook exports`,
3329
+ hint: rootPkg.conventions.hookNaming ?? "not set"
3330
+ },
3331
+ {
3332
+ value: "importAlias",
3333
+ label: `${rootPkg.conventions.importAlias ? ok : unset} Import alias`,
3334
+ hint: rootPkg.conventions.importAlias ?? "not set"
3335
+ },
3336
+ { value: "back", label: " Back" }
3337
+ ];
3338
+ const choice = await clack10.select({
3339
+ message: "AI context \u2014 conventions written to context.md for AI tools",
3340
+ options
3341
+ });
3342
+ if (isCancelled(choice) || choice === "back") return;
3343
+ if (choice === "componentNaming") {
3344
+ const selected = await clack10.select({
3345
+ message: "Component export naming (e.g. UserProfile)",
3346
+ options: [
3347
+ ...COMPONENT_NAMING_OPTIONS,
3348
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
3349
+ ],
3350
+ initialValue: rootPkg.conventions.componentNaming ?? SENTINEL_CLEAR
3351
+ });
3352
+ if (isCancelled(selected)) continue;
3353
+ rootPkg.conventions.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
3354
+ }
3355
+ if (choice === "hookNaming") {
3356
+ const selected = await clack10.select({
3357
+ message: "Hook export naming (e.g. useAuth)",
3358
+ options: [
3359
+ ...HOOK_NAMING_OPTIONS,
3360
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
3361
+ ],
3362
+ initialValue: rootPkg.conventions.hookNaming ?? SENTINEL_CLEAR
3363
+ });
3364
+ if (isCancelled(selected)) continue;
3365
+ rootPkg.conventions.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
3366
+ }
3367
+ if (choice === "importAlias") {
3368
+ const selected = await clack10.select({
3369
+ message: "Import alias pattern",
3370
+ options: [
3371
+ { value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
3372
+ { value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
3373
+ { value: SENTINEL_CUSTOM, label: "Custom..." },
3374
+ { value: SENTINEL_CLEAR, label: "Clear (no alias)" }
3375
+ ],
3376
+ initialValue: rootPkg.conventions.importAlias ?? SENTINEL_CLEAR
3377
+ });
3378
+ if (isCancelled(selected)) continue;
3379
+ if (selected === SENTINEL_CLEAR) {
3380
+ rootPkg.conventions.importAlias = void 0;
3381
+ } else if (selected === SENTINEL_CUSTOM) {
3382
+ const result = await clack10.text({
3383
+ message: "Custom import alias (e.g. #/*)?",
3384
+ initialValue: rootPkg.conventions.importAlias ?? "",
3385
+ placeholder: "e.g. #/*",
3386
+ validate: (v) => {
3387
+ if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
3388
+ if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
3389
+ return "Must match pattern like @/*, ~/*, or #src/*";
3390
+ }
3391
+ });
3392
+ if (isCancelled(result)) continue;
3393
+ rootPkg.conventions.importAlias = result.trim();
3394
+ } else {
3395
+ rootPkg.conventions.importAlias = selected;
3396
+ }
3397
+ }
3363
3398
  }
3364
- const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
3365
- options.push(
3366
- { value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
3367
- { value: "reset", label: " Reset all to defaults" },
3368
- { value: "review", label: " Review scan details", hint: "detected stack & conventions" },
3369
- { value: "done", label: " Done \u2014 write config" }
3370
- );
3371
- return options;
3372
3399
  }
3373
3400
 
3374
3401
  // src/utils/prompt-main-menu.ts
3375
3402
  async function promptMainMenu(config, scanResult, opts) {
3376
3403
  const originalConfig = structuredClone(config);
3377
3404
  const state = {
3378
- visited: { integrations: false, boundaries: false },
3405
+ visited: { boundaries: false },
3379
3406
  deferredInstalls: [],
3380
3407
  hasTestRunner: opts.hasTestRunner,
3381
3408
  hookManager: opts.hookManager
@@ -3402,10 +3429,9 @@ async function promptMainMenu(config, scanResult, opts) {
3402
3429
  if (choice === "fileNaming") await handleFileNaming(config, scanResult);
3403
3430
  if (choice === "missingTests") await handleMissingTests(config);
3404
3431
  if (choice === "coverage") await handleCoverage(config, state, opts);
3405
- if (choice === "advancedNaming") await handleAdvancedNaming(config);
3432
+ if (choice === "aiContext") await handleAiContext(config);
3406
3433
  if (choice === "packageOverrides") await handlePackageOverrides(config);
3407
3434
  if (choice === "boundaries") await handleBoundaries(config, state, opts);
3408
- if (choice === "integrations") await handleIntegrations(state, opts);
3409
3435
  if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
3410
3436
  if (choice === "reset") {
3411
3437
  const confirmed = await clack11.confirm({
@@ -3416,8 +3442,7 @@ async function promptMainMenu(config, scanResult, opts) {
3416
3442
  if (confirmed) {
3417
3443
  Object.assign(config, structuredClone(originalConfig));
3418
3444
  state.deferredInstalls = [];
3419
- state.visited = { integrations: false, boundaries: false };
3420
- state.integrations = void 0;
3445
+ state.visited = { boundaries: false };
3421
3446
  clack11.log.info("Reset all settings to scan-detected defaults.");
3422
3447
  }
3423
3448
  }
@@ -3425,37 +3450,26 @@ async function promptMainMenu(config, scanResult, opts) {
3425
3450
  return state;
3426
3451
  }
3427
3452
 
3428
- // src/utils/update-gitignore.ts
3429
- var fs16 = __toESM(require("fs"), 1);
3430
- var path16 = __toESM(require("path"), 1);
3431
- function updateGitignore(projectRoot) {
3432
- const gitignorePath = path16.join(projectRoot, ".gitignore");
3433
- let content = "";
3434
- if (fs16.existsSync(gitignorePath)) {
3435
- content = fs16.readFileSync(gitignorePath, "utf-8");
3436
- }
3437
- if (!content.includes(".viberails/scan-result.json")) {
3438
- const block = "\n# viberails\n.viberails/scan-result.json\n";
3439
- const prefix = content.length === 0 ? "" : `${content.trimEnd()}
3440
- `;
3441
- fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
3442
- }
3443
- }
3453
+ // src/utils/prompt-prereqs.ts
3454
+ var fs17 = __toESM(require("fs"), 1);
3455
+ var path17 = __toESM(require("path"), 1);
3456
+ var clack12 = __toESM(require("@clack/prompts"), 1);
3457
+ var import_chalk15 = __toESM(require("chalk"), 1);
3444
3458
 
3445
3459
  // src/commands/init-hooks.ts
3446
- var fs18 = __toESM(require("fs"), 1);
3447
- var path18 = __toESM(require("path"), 1);
3448
- var import_chalk13 = __toESM(require("chalk"), 1);
3460
+ var fs16 = __toESM(require("fs"), 1);
3461
+ var path16 = __toESM(require("path"), 1);
3462
+ var import_chalk14 = __toESM(require("chalk"), 1);
3449
3463
  var import_yaml = require("yaml");
3450
3464
 
3451
3465
  // src/commands/resolve-typecheck.ts
3452
- var fs17 = __toESM(require("fs"), 1);
3453
- var path17 = __toESM(require("path"), 1);
3466
+ var fs15 = __toESM(require("fs"), 1);
3467
+ var path15 = __toESM(require("path"), 1);
3454
3468
  function hasTurboTask(projectRoot, taskName) {
3455
- const turboPath = path17.join(projectRoot, "turbo.json");
3456
- if (!fs17.existsSync(turboPath)) return false;
3469
+ const turboPath = path15.join(projectRoot, "turbo.json");
3470
+ if (!fs15.existsSync(turboPath)) return false;
3457
3471
  try {
3458
- const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
3472
+ const turbo = JSON.parse(fs15.readFileSync(turboPath, "utf-8"));
3459
3473
  const tasks = turbo.tasks ?? turbo.pipeline ?? {};
3460
3474
  return taskName in tasks;
3461
3475
  } catch {
@@ -3466,10 +3480,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3466
3480
  if (hasTurboTask(projectRoot, "typecheck")) {
3467
3481
  return { command: "npx turbo typecheck", label: "turbo typecheck" };
3468
3482
  }
3469
- const pkgJsonPath = path17.join(projectRoot, "package.json");
3470
- if (fs17.existsSync(pkgJsonPath)) {
3483
+ const pkgJsonPath = path15.join(projectRoot, "package.json");
3484
+ if (fs15.existsSync(pkgJsonPath)) {
3471
3485
  try {
3472
- const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
3486
+ const pkg = JSON.parse(fs15.readFileSync(pkgJsonPath, "utf-8"));
3473
3487
  if (pkg.scripts?.typecheck) {
3474
3488
  const pm = packageManager ?? "npm";
3475
3489
  return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
@@ -3477,7 +3491,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3477
3491
  } catch {
3478
3492
  }
3479
3493
  }
3480
- if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
3494
+ if (fs15.existsSync(path15.join(projectRoot, "tsconfig.json"))) {
3481
3495
  return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
3482
3496
  }
3483
3497
  return {
@@ -3487,36 +3501,36 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
3487
3501
 
3488
3502
  // src/commands/init-hooks.ts
3489
3503
  function setupPreCommitHook(projectRoot) {
3490
- const lefthookPath = path18.join(projectRoot, "lefthook.yml");
3491
- if (fs18.existsSync(lefthookPath)) {
3504
+ const lefthookPath = path16.join(projectRoot, "lefthook.yml");
3505
+ if (fs16.existsSync(lefthookPath)) {
3492
3506
  addLefthookPreCommit(lefthookPath);
3493
- console.log(` ${import_chalk13.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3507
+ console.log(` ${import_chalk14.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
3494
3508
  return "lefthook.yml";
3495
3509
  }
3496
- const huskyDir = path18.join(projectRoot, ".husky");
3497
- if (fs18.existsSync(huskyDir)) {
3510
+ const huskyDir = path16.join(projectRoot, ".husky");
3511
+ if (fs16.existsSync(huskyDir)) {
3498
3512
  writeHuskyPreCommit(huskyDir);
3499
- console.log(` ${import_chalk13.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3513
+ console.log(` ${import_chalk14.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
3500
3514
  return ".husky/pre-commit";
3501
3515
  }
3502
- const gitDir = path18.join(projectRoot, ".git");
3503
- if (fs18.existsSync(gitDir)) {
3504
- const hooksDir = path18.join(gitDir, "hooks");
3505
- if (!fs18.existsSync(hooksDir)) {
3506
- fs18.mkdirSync(hooksDir, { recursive: true });
3516
+ const gitDir = path16.join(projectRoot, ".git");
3517
+ if (fs16.existsSync(gitDir)) {
3518
+ const hooksDir = path16.join(gitDir, "hooks");
3519
+ if (!fs16.existsSync(hooksDir)) {
3520
+ fs16.mkdirSync(hooksDir, { recursive: true });
3507
3521
  }
3508
3522
  writeGitHookPreCommit(hooksDir);
3509
- console.log(` ${import_chalk13.default.green("\u2713")} .git/hooks/pre-commit`);
3523
+ console.log(` ${import_chalk14.default.green("\u2713")} .git/hooks/pre-commit`);
3510
3524
  return ".git/hooks/pre-commit";
3511
3525
  }
3512
3526
  return void 0;
3513
3527
  }
3514
3528
  function writeGitHookPreCommit(hooksDir) {
3515
- const hookPath = path18.join(hooksDir, "pre-commit");
3516
- if (fs18.existsSync(hookPath)) {
3517
- const existing = fs18.readFileSync(hookPath, "utf-8");
3529
+ const hookPath = path16.join(hooksDir, "pre-commit");
3530
+ if (fs16.existsSync(hookPath)) {
3531
+ const existing = fs16.readFileSync(hookPath, "utf-8");
3518
3532
  if (existing.includes("viberails")) return;
3519
- fs18.writeFileSync(
3533
+ fs16.writeFileSync(
3520
3534
  hookPath,
3521
3535
  `${existing.trimEnd()}
3522
3536
 
@@ -3533,10 +3547,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
3533
3547
  "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
3534
3548
  ""
3535
3549
  ].join("\n");
3536
- fs18.writeFileSync(hookPath, script, { mode: 493 });
3550
+ fs16.writeFileSync(hookPath, script, { mode: 493 });
3537
3551
  }
3538
3552
  function addLefthookPreCommit(lefthookPath) {
3539
- const content = fs18.readFileSync(lefthookPath, "utf-8");
3553
+ const content = fs16.readFileSync(lefthookPath, "utf-8");
3540
3554
  if (content.includes("viberails")) return;
3541
3555
  const doc = (0, import_yaml.parse)(content) ?? {};
3542
3556
  if (!doc["pre-commit"]) {
@@ -3548,28 +3562,28 @@ function addLefthookPreCommit(lefthookPath) {
3548
3562
  doc["pre-commit"].commands.viberails = {
3549
3563
  run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
3550
3564
  };
3551
- fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3565
+ fs16.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
3552
3566
  }
3553
3567
  function detectHookManager(projectRoot) {
3554
- if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3555
- if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
3568
+ if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
3569
+ if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
3556
3570
  return void 0;
3557
3571
  }
3558
3572
  function setupClaudeCodeHook(projectRoot) {
3559
- const claudeDir = path18.join(projectRoot, ".claude");
3560
- if (!fs18.existsSync(claudeDir)) {
3561
- fs18.mkdirSync(claudeDir, { recursive: true });
3573
+ const claudeDir = path16.join(projectRoot, ".claude");
3574
+ if (!fs16.existsSync(claudeDir)) {
3575
+ fs16.mkdirSync(claudeDir, { recursive: true });
3562
3576
  }
3563
- const settingsPath = path18.join(claudeDir, "settings.json");
3577
+ const settingsPath = path16.join(claudeDir, "settings.json");
3564
3578
  let settings = {};
3565
- if (fs18.existsSync(settingsPath)) {
3579
+ if (fs16.existsSync(settingsPath)) {
3566
3580
  try {
3567
- settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
3581
+ settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
3568
3582
  } catch {
3569
3583
  console.warn(
3570
- ` ${import_chalk13.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3584
+ ` ${import_chalk14.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
3571
3585
  );
3572
- console.warn(` Fix the JSON manually, then re-run ${import_chalk13.default.cyan("viberails init --force")}`);
3586
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk14.default.cyan("viberails init --force")}`);
3573
3587
  return;
3574
3588
  }
3575
3589
  }
@@ -3590,30 +3604,30 @@ function setupClaudeCodeHook(projectRoot) {
3590
3604
  }
3591
3605
  ];
3592
3606
  settings.hooks = hooks;
3593
- fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3607
+ fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
3594
3608
  `);
3595
- console.log(` ${import_chalk13.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3609
+ console.log(` ${import_chalk14.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
3596
3610
  }
3597
3611
  function setupClaudeMdReference(projectRoot) {
3598
- const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
3612
+ const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
3599
3613
  let content = "";
3600
- if (fs18.existsSync(claudeMdPath)) {
3601
- content = fs18.readFileSync(claudeMdPath, "utf-8");
3614
+ if (fs16.existsSync(claudeMdPath)) {
3615
+ content = fs16.readFileSync(claudeMdPath, "utf-8");
3602
3616
  }
3603
3617
  if (content.includes("@.viberails/context.md")) return;
3604
3618
  const ref = "\n@.viberails/context.md\n";
3605
3619
  const prefix = content.length === 0 ? "" : content.trimEnd();
3606
- fs18.writeFileSync(claudeMdPath, prefix + ref);
3607
- console.log(` ${import_chalk13.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3620
+ fs16.writeFileSync(claudeMdPath, prefix + ref);
3621
+ console.log(` ${import_chalk14.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
3608
3622
  }
3609
3623
  function setupGithubAction(projectRoot, packageManager, options) {
3610
- const workflowDir = path18.join(projectRoot, ".github", "workflows");
3611
- const workflowPath = path18.join(workflowDir, "viberails.yml");
3612
- if (fs18.existsSync(workflowPath)) {
3613
- const existing = fs18.readFileSync(workflowPath, "utf-8");
3624
+ const workflowDir = path16.join(projectRoot, ".github", "workflows");
3625
+ const workflowPath = path16.join(workflowDir, "viberails.yml");
3626
+ if (fs16.existsSync(workflowPath)) {
3627
+ const existing = fs16.readFileSync(workflowPath, "utf-8");
3614
3628
  if (existing.includes("viberails")) return void 0;
3615
3629
  }
3616
- fs18.mkdirSync(workflowDir, { recursive: true });
3630
+ fs16.mkdirSync(workflowDir, { recursive: true });
3617
3631
  const pm = packageManager || "npm";
3618
3632
  const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
3619
3633
  const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
@@ -3667,30 +3681,212 @@ function setupGithubAction(projectRoot, packageManager, options) {
3667
3681
  ""
3668
3682
  );
3669
3683
  const content = lines.filter((l) => l !== void 0).join("\n");
3670
- fs18.writeFileSync(workflowPath, content);
3684
+ fs16.writeFileSync(workflowPath, content);
3671
3685
  return ".github/workflows/viberails.yml";
3672
3686
  }
3673
3687
  function writeHuskyPreCommit(huskyDir) {
3674
- const hookPath = path18.join(huskyDir, "pre-commit");
3688
+ const hookPath = path16.join(huskyDir, "pre-commit");
3675
3689
  const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
3676
- if (fs18.existsSync(hookPath)) {
3677
- const existing = fs18.readFileSync(hookPath, "utf-8");
3690
+ if (fs16.existsSync(hookPath)) {
3691
+ const existing = fs16.readFileSync(hookPath, "utf-8");
3678
3692
  if (!existing.includes("viberails")) {
3679
- fs18.writeFileSync(hookPath, `${existing.trimEnd()}
3693
+ fs16.writeFileSync(hookPath, `${existing.trimEnd()}
3680
3694
  ${cmd}
3681
3695
  `);
3682
3696
  }
3683
3697
  return;
3684
3698
  }
3685
- fs18.writeFileSync(hookPath, `#!/bin/sh
3699
+ fs16.writeFileSync(hookPath, `#!/bin/sh
3686
3700
  ${cmd}
3687
3701
  `, { mode: 493 });
3688
3702
  }
3689
3703
 
3704
+ // src/utils/prompt-prereqs.ts
3705
+ function buildVitestInstallCommand(pm, isWorkspace) {
3706
+ if (pm === "yarn") return "yarn add -D vitest";
3707
+ if (pm === "npm") return "npm install -D vitest";
3708
+ return isWorkspace ? "pnpm add -D -w vitest" : "pnpm add -D vitest";
3709
+ }
3710
+ function statusIcon2(status) {
3711
+ if (status === "ok") return import_chalk15.default.green("\u2713");
3712
+ if (status === "missing") return import_chalk15.default.yellow("!");
3713
+ if (status === "skipped") return import_chalk15.default.dim("\u2717");
3714
+ return import_chalk15.default.dim("-");
3715
+ }
3716
+ function buildReadinessNote(state) {
3717
+ const lines = [];
3718
+ const tr = state.testRunner;
3719
+ lines.push(
3720
+ `${statusIcon2(tr.status)} Test runner ${tr.label ?? (tr.status === "skipped" ? "skipped" : "not detected")}`
3721
+ );
3722
+ const hm = state.hookManager;
3723
+ lines.push(
3724
+ `${statusIcon2(hm.status)} Hook manager ${hm.label ?? (hm.status === "skipped" ? "skipped" : "not detected")}`
3725
+ );
3726
+ const li = state.linter;
3727
+ lines.push(`${statusIcon2(li.status)} Linter ${li.label ?? "none"}`);
3728
+ const tc = state.typecheck;
3729
+ if (tc.status === "ok") {
3730
+ lines.push(`${statusIcon2("ok")} Typecheck ${tc.label}`);
3731
+ } else if (tc.status === "skipped") {
3732
+ lines.push(`${statusIcon2("skipped")} Typecheck skipped`);
3733
+ } else {
3734
+ lines.push(
3735
+ `${statusIcon2("missing")} Typecheck needs root tsconfig.json, typecheck script, or turbo task`
3736
+ );
3737
+ }
3738
+ return lines.join("\n");
3739
+ }
3740
+ function hasMissing(state) {
3741
+ return state.testRunner.status === "missing" || state.hookManager.status === "missing" || state.typecheck.status === "missing";
3742
+ }
3743
+ async function promptPrereqs(projectRoot, scanResult, hookManager, packageManager, isWorkspace) {
3744
+ let hasTestRunner = !!scanResult.stack.testRunner;
3745
+ let currentHookManager = hookManager;
3746
+ let skipCoverage = false;
3747
+ let skipHooks = false;
3748
+ const linterName = scanResult.stack.linter?.name;
3749
+ const linterLabel = linterName === "biome" ? "Biome" : linterName === "eslint" ? "ESLint" : linterName;
3750
+ const typecheckResolved = resolveTypecheckCommand(projectRoot, packageManager);
3751
+ const state = {
3752
+ testRunner: hasTestRunner ? { status: "ok", label: scanResult.stack.testRunner?.name } : { status: "missing" },
3753
+ hookManager: currentHookManager ? { status: "ok", label: currentHookManager } : { status: "missing" },
3754
+ linter: linterName ? { status: "ok", label: linterLabel } : { status: "none" },
3755
+ typecheck: typecheckResolved.label ? { status: "ok", label: typecheckResolved.label } : { status: "missing", reason: typecheckResolved.reason }
3756
+ };
3757
+ if (!hasMissing(state)) {
3758
+ return {
3759
+ hasTestRunner,
3760
+ hookManager: currentHookManager,
3761
+ skipCoverage,
3762
+ skipHooks,
3763
+ typecheckLabel: typecheckResolved.label
3764
+ };
3765
+ }
3766
+ if (state.testRunner.status === "missing") {
3767
+ clack12.note(buildReadinessNote(state), "Project readiness");
3768
+ const cmd = buildVitestInstallCommand(packageManager, isWorkspace);
3769
+ const choice = await clack12.select({
3770
+ message: "No test runner detected. Coverage checks require one.",
3771
+ options: [
3772
+ { value: "install", label: "Install vitest", hint: cmd },
3773
+ { value: "skip", label: "Skip \u2014 disable coverage checks" },
3774
+ { value: "exit", label: "Exit" }
3775
+ ]
3776
+ });
3777
+ assertNotCancelled(choice);
3778
+ if (choice === "install") {
3779
+ const s = clack12.spinner();
3780
+ s.start("Installing vitest...");
3781
+ const result = await spawnAsync(cmd, projectRoot);
3782
+ if (result.status === 0) {
3783
+ s.stop("Installed vitest");
3784
+ hasTestRunner = true;
3785
+ state.testRunner = { status: "ok", label: "vitest" };
3786
+ } else {
3787
+ s.stop("Failed to install vitest");
3788
+ clack12.log.warn(`Install manually: ${cmd}`);
3789
+ skipCoverage = true;
3790
+ state.testRunner = { status: "skipped" };
3791
+ }
3792
+ } else if (choice === "skip") {
3793
+ skipCoverage = true;
3794
+ state.testRunner = { status: "skipped" };
3795
+ } else {
3796
+ clack12.outro("Aborted.");
3797
+ process.exit(0);
3798
+ }
3799
+ }
3800
+ if (state.hookManager.status === "missing") {
3801
+ clack12.note(buildReadinessNote(state), "Project readiness");
3802
+ const cmd = buildLefthookInstallCommand(packageManager, isWorkspace);
3803
+ const choice = await clack12.select({
3804
+ message: "No git hook manager detected. Pre-commit integration requires one.",
3805
+ options: [
3806
+ { value: "install", label: "Install lefthook", hint: cmd },
3807
+ { value: "skip", label: "Skip \u2014 no pre-commit integration" },
3808
+ { value: "exit", label: "Exit" }
3809
+ ]
3810
+ });
3811
+ assertNotCancelled(choice);
3812
+ if (choice === "install") {
3813
+ const s = clack12.spinner();
3814
+ s.start("Installing lefthook...");
3815
+ const result = await spawnAsync(cmd, projectRoot);
3816
+ if (result.status === 0) {
3817
+ s.stop("Installed lefthook");
3818
+ const ymlPath = path17.join(projectRoot, "lefthook.yml");
3819
+ if (!fs17.existsSync(ymlPath)) {
3820
+ fs17.writeFileSync(ymlPath, "# Managed by viberails\npre-commit:\n commands: {}\n");
3821
+ }
3822
+ currentHookManager = detectHookManager(projectRoot);
3823
+ state.hookManager = { status: "ok", label: currentHookManager ?? "lefthook" };
3824
+ } else {
3825
+ s.stop("Failed to install lefthook");
3826
+ clack12.log.warn(`Install manually: ${cmd}`);
3827
+ skipHooks = true;
3828
+ state.hookManager = { status: "skipped" };
3829
+ }
3830
+ } else if (choice === "skip") {
3831
+ skipHooks = true;
3832
+ state.hookManager = { status: "skipped" };
3833
+ } else {
3834
+ clack12.outro("Aborted.");
3835
+ process.exit(0);
3836
+ }
3837
+ }
3838
+ if (state.typecheck.status === "missing") {
3839
+ clack12.note(buildReadinessNote(state), "Project readiness");
3840
+ const choice = await clack12.select({
3841
+ message: "No typecheck command found. Without this, pre-commit and CI typecheck hooks will be unavailable.",
3842
+ options: [
3843
+ {
3844
+ value: "continue",
3845
+ label: "Continue without typecheck",
3846
+ hint: "add a root tsconfig.json or typecheck script later, then re-run viberails"
3847
+ },
3848
+ { value: "exit", label: "Exit \u2014 fix this first" }
3849
+ ]
3850
+ });
3851
+ assertNotCancelled(choice);
3852
+ if (choice === "exit") {
3853
+ clack12.outro(
3854
+ "Add a root tsconfig.json, a typecheck script, or a turbo typecheck task, then re-run viberails."
3855
+ );
3856
+ process.exit(0);
3857
+ }
3858
+ state.typecheck = { status: "skipped" };
3859
+ }
3860
+ return {
3861
+ hasTestRunner,
3862
+ hookManager: currentHookManager,
3863
+ skipCoverage,
3864
+ skipHooks,
3865
+ typecheckLabel: typecheckResolved.label
3866
+ };
3867
+ }
3868
+
3869
+ // src/utils/update-gitignore.ts
3870
+ var fs18 = __toESM(require("fs"), 1);
3871
+ var path18 = __toESM(require("path"), 1);
3872
+ function updateGitignore(projectRoot) {
3873
+ const gitignorePath = path18.join(projectRoot, ".gitignore");
3874
+ let content = "";
3875
+ if (fs18.existsSync(gitignorePath)) {
3876
+ content = fs18.readFileSync(gitignorePath, "utf-8");
3877
+ }
3878
+ if (!content.includes(".viberails/scan-result.json")) {
3879
+ const block = "\n# viberails\n.viberails/scan-result.json\n";
3880
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
3881
+ `;
3882
+ fs18.writeFileSync(gitignorePath, `${prefix}${block}`);
3883
+ }
3884
+ }
3885
+
3690
3886
  // src/commands/init-hooks-extra.ts
3691
3887
  var fs19 = __toESM(require("fs"), 1);
3692
3888
  var path19 = __toESM(require("path"), 1);
3693
- var import_chalk14 = __toESM(require("chalk"), 1);
3889
+ var import_chalk16 = __toESM(require("chalk"), 1);
3694
3890
  var import_yaml2 = require("yaml");
3695
3891
  function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
3696
3892
  const lefthookPath = path19.join(projectRoot, "lefthook.yml");
@@ -3750,12 +3946,12 @@ ${command}
3750
3946
  function setupTypecheckHook(projectRoot, packageManager) {
3751
3947
  const resolved = resolveTypecheckCommand(projectRoot, packageManager);
3752
3948
  if (!resolved.command) {
3753
- console.log(` ${import_chalk14.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3949
+ console.log(` ${import_chalk16.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
3754
3950
  return void 0;
3755
3951
  }
3756
3952
  const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
3757
3953
  if (target) {
3758
- console.log(` ${import_chalk14.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3954
+ console.log(` ${import_chalk16.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
3759
3955
  }
3760
3956
  return target;
3761
3957
  }
@@ -3777,7 +3973,7 @@ function setupLintHook(projectRoot, linter) {
3777
3973
  }
3778
3974
  const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
3779
3975
  if (target) {
3780
- console.log(` ${import_chalk14.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3976
+ console.log(` ${import_chalk16.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
3781
3977
  }
3782
3978
  return target;
3783
3979
  }
@@ -3786,7 +3982,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3786
3982
  if (integrations.preCommitHook) {
3787
3983
  const t = setupPreCommitHook(projectRoot);
3788
3984
  if (t && opts.lefthookExpected && !t.includes("lefthook")) {
3789
- console.log(` ${import_chalk14.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3985
+ console.log(` ${import_chalk16.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
3790
3986
  }
3791
3987
  created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
3792
3988
  }
@@ -3819,10 +4015,10 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
3819
4015
  // src/commands/init-non-interactive.ts
3820
4016
  var fs20 = __toESM(require("fs"), 1);
3821
4017
  var path20 = __toESM(require("path"), 1);
3822
- var clack12 = __toESM(require("@clack/prompts"), 1);
4018
+ var clack13 = __toESM(require("@clack/prompts"), 1);
3823
4019
  var import_config8 = require("@viberails/config");
3824
4020
  var import_scanner2 = require("@viberails/scanner");
3825
- var import_chalk15 = __toESM(require("chalk"), 1);
4021
+ var import_chalk17 = __toESM(require("chalk"), 1);
3826
4022
 
3827
4023
  // src/utils/filter-confidence.ts
3828
4024
  function filterHighConfidence(conventions, meta) {
@@ -3843,7 +4039,7 @@ function getExemptedPackages(config) {
3843
4039
  return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
3844
4040
  }
3845
4041
  async function initNonInteractive(projectRoot, configPath) {
3846
- const s = clack12.spinner();
4042
+ const s = clack13.spinner();
3847
4043
  s.start("Scanning project...");
3848
4044
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
3849
4045
  const config = (0, import_config8.generateConfig)(scanResult);
@@ -3858,11 +4054,11 @@ async function initNonInteractive(projectRoot, configPath) {
3858
4054
  const exempted = getExemptedPackages(config);
3859
4055
  if (exempted.length > 0) {
3860
4056
  console.log(
3861
- ` ${import_chalk15.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk15.default.dim("(types-only)")}`
4057
+ ` ${import_chalk17.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk17.default.dim("(types-only)")}`
3862
4058
  );
3863
4059
  }
3864
4060
  if (config.packages.length > 1) {
3865
- const bs = clack12.spinner();
4061
+ const bs = clack13.spinner();
3866
4062
  bs.start("Building import graph...");
3867
4063
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
3868
4064
  const packages = resolveWorkspacePackages(projectRoot, config.packages);
@@ -3895,14 +4091,14 @@ async function initNonInteractive(projectRoot, configPath) {
3895
4091
  const hookManager = detectHookManager(projectRoot);
3896
4092
  const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
3897
4093
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
3898
- const ok = import_chalk15.default.green("\u2713");
4094
+ const ok = import_chalk17.default.green("\u2713");
3899
4095
  const created = [
3900
4096
  `${ok} ${path20.basename(configPath)}`,
3901
4097
  `${ok} .viberails/context.md`,
3902
4098
  `${ok} .viberails/scan-result.json`,
3903
4099
  `${ok} .claude/settings.json \u2014 added viberails hook`,
3904
4100
  `${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
3905
- preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk15.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
4101
+ preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk17.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
3906
4102
  actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
3907
4103
  ].filter(Boolean);
3908
4104
  if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
@@ -3927,8 +4123,8 @@ async function initCommand(options, cwd) {
3927
4123
  return initInteractive(projectRoot, configPath, options);
3928
4124
  }
3929
4125
  console.log(
3930
- `${import_chalk16.default.yellow("!")} viberails is already initialized.
3931
- Run ${import_chalk16.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk16.default.cyan("viberails sync")} to update generated files, or ${import_chalk16.default.cyan("viberails init --force")} to replace it.`
4126
+ `${import_chalk18.default.yellow("!")} viberails is already initialized.
4127
+ Run ${import_chalk18.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk18.default.cyan("viberails sync")} to update generated files, or ${import_chalk18.default.cyan("viberails init --force")} to replace it.`
3932
4128
  );
3933
4129
  return;
3934
4130
  }
@@ -3936,11 +4132,11 @@ async function initCommand(options, cwd) {
3936
4132
  await initInteractive(projectRoot, configPath, options);
3937
4133
  }
3938
4134
  async function initInteractive(projectRoot, configPath, options) {
3939
- clack13.intro("viberails");
4135
+ clack14.intro("viberails");
3940
4136
  if (fs21.existsSync(configPath) && !options.force) {
3941
4137
  const action = await promptExistingConfigAction(path21.basename(configPath));
3942
4138
  if (action === "cancel") {
3943
- clack13.outro("Aborted. No files were written.");
4139
+ clack14.outro("Aborted. No files were written.");
3944
4140
  return;
3945
4141
  }
3946
4142
  if (action === "edit") {
@@ -3954,45 +4150,60 @@ async function initInteractive(projectRoot, configPath, options) {
3954
4150
  `${path21.basename(configPath)} already exists and will be replaced. Continue?`
3955
4151
  );
3956
4152
  if (!replace) {
3957
- clack13.outro("Aborted. No files were written.");
4153
+ clack14.outro("Aborted. No files were written.");
3958
4154
  return;
3959
4155
  }
3960
4156
  }
3961
- const s = clack13.spinner();
4157
+ const s = clack14.spinner();
3962
4158
  s.start("Scanning project...");
3963
4159
  const scanResult = await (0, import_scanner3.scan)(projectRoot);
3964
4160
  const config = (0, import_config9.generateConfig)(scanResult);
3965
4161
  s.stop("Scan complete");
3966
4162
  if (scanResult.statistics.totalFiles === 0) {
3967
- clack13.log.warn(
4163
+ clack14.log.warn(
3968
4164
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3969
4165
  );
3970
4166
  }
3971
- const hasTestRunner = !!scanResult.stack.testRunner;
3972
- const hookManager = detectHookManager(projectRoot);
3973
- const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
3974
4167
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
4168
+ const packageManager = rootPkgStack?.packageManager?.split("@")[0] ?? "npm";
4169
+ const isWorkspace = config.packages.length > 1;
4170
+ const prereqs = await promptPrereqs(
4171
+ projectRoot,
4172
+ scanResult,
4173
+ detectHookManager(projectRoot),
4174
+ packageManager,
4175
+ isWorkspace
4176
+ );
4177
+ if (prereqs.skipCoverage) config.rules.testCoverage = 0;
4178
+ const coveragePrereqs = prereqs.hasTestRunner ? checkCoveragePrereqs(projectRoot, scanResult) : [];
3975
4179
  const state = await promptMainMenu(config, scanResult, {
3976
- hasTestRunner,
3977
- hookManager,
4180
+ hasTestRunner: prereqs.hasTestRunner,
4181
+ hookManager: prereqs.hookManager,
3978
4182
  coveragePrereqs,
3979
4183
  projectRoot,
3980
4184
  tools: {
3981
4185
  isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
3982
4186
  linter: rootPkgStack?.linter?.split("@")[0],
3983
- packageManager: rootPkgStack?.packageManager?.split("@")[0],
3984
- isWorkspace: config.packages.length > 1
4187
+ packageManager,
4188
+ isWorkspace
3985
4189
  }
3986
4190
  });
3987
4191
  const shouldWrite = await confirm3("Apply this setup?");
3988
4192
  if (!shouldWrite) {
3989
- clack13.outro("Aborted. No files were written.");
4193
+ clack14.outro("Aborted. No files were written.");
3990
4194
  return;
3991
4195
  }
4196
+ const integrations = await promptIntegrationsDeferred(prereqs.hookManager, {
4197
+ isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
4198
+ typecheckLabel: prereqs.typecheckLabel,
4199
+ linter: rootPkgStack?.linter?.split("@")[0],
4200
+ packageManager,
4201
+ isWorkspace
4202
+ });
3992
4203
  if (state.deferredInstalls.length > 0) {
3993
4204
  await executeDeferredInstalls(projectRoot, state.deferredInstalls);
3994
4205
  }
3995
- const ws = clack13.spinner();
4206
+ const ws = clack14.spinner();
3996
4207
  ws.start("Writing configuration...");
3997
4208
  const compacted = (0, import_config9.compactConfig)(config);
3998
4209
  fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
@@ -4000,31 +4211,28 @@ async function initInteractive(projectRoot, configPath, options) {
4000
4211
  writeGeneratedFiles(projectRoot, config, scanResult);
4001
4212
  updateGitignore(projectRoot);
4002
4213
  ws.stop("Configuration written");
4003
- const ok = import_chalk16.default.green("\u2713");
4004
- clack13.log.step(`${ok} ${path21.basename(configPath)}`);
4005
- clack13.log.step(`${ok} .viberails/context.md`);
4006
- clack13.log.step(`${ok} .viberails/scan-result.json`);
4007
- if (state.visited.integrations && state.integrations) {
4008
- const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
4009
- setupSelectedIntegrations(projectRoot, state.integrations, {
4010
- linter: rootPkgStack?.linter?.split("@")[0],
4011
- packageManager: rootPkgStack?.packageManager?.split("@")[0],
4012
- lefthookExpected
4013
- });
4014
- }
4015
- clack13.outro(
4214
+ const ok = import_chalk18.default.green("\u2713");
4215
+ clack14.log.step(`${ok} ${path21.basename(configPath)}`);
4216
+ clack14.log.step(`${ok} .viberails/context.md`);
4217
+ clack14.log.step(`${ok} .viberails/scan-result.json`);
4218
+ setupSelectedIntegrations(projectRoot, integrations.choice, {
4219
+ linter: rootPkgStack?.linter?.split("@")[0],
4220
+ packageManager,
4221
+ lefthookExpected: prereqs.hookManager === "lefthook"
4222
+ });
4223
+ clack14.outro(
4016
4224
  `Done! Next: review viberails.config.json, then run viberails check
4017
- ${import_chalk16.default.dim("Tip: use")} ${import_chalk16.default.cyan("viberails check --enforce")} ${import_chalk16.default.dim("in CI to block PRs on violations.")}`
4225
+ ${import_chalk18.default.dim("Tip: use")} ${import_chalk18.default.cyan("viberails check --enforce")} ${import_chalk18.default.dim("in CI to block PRs on violations.")}`
4018
4226
  );
4019
4227
  }
4020
4228
 
4021
4229
  // src/commands/sync.ts
4022
4230
  var fs22 = __toESM(require("fs"), 1);
4023
4231
  var path22 = __toESM(require("path"), 1);
4024
- var clack14 = __toESM(require("@clack/prompts"), 1);
4232
+ var clack15 = __toESM(require("@clack/prompts"), 1);
4025
4233
  var import_config11 = require("@viberails/config");
4026
4234
  var import_scanner4 = require("@viberails/scanner");
4027
- var import_chalk17 = __toESM(require("chalk"), 1);
4235
+ var import_chalk19 = __toESM(require("chalk"), 1);
4028
4236
  var CONFIG_FILE6 = "viberails.config.json";
4029
4237
  var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
4030
4238
  function loadPreviousStats(projectRoot) {
@@ -4050,7 +4258,7 @@ async function syncCommand(options, cwd) {
4050
4258
  const configPath = path22.join(projectRoot, CONFIG_FILE6);
4051
4259
  const existing = await (0, import_config11.loadConfig)(configPath);
4052
4260
  const previousStats = loadPreviousStats(projectRoot);
4053
- const s = clack14.spinner();
4261
+ const s = clack15.spinner();
4054
4262
  s.start("Scanning project...");
4055
4263
  const scanResult = await (0, import_scanner4.scan)(projectRoot);
4056
4264
  s.stop("Scan complete");
@@ -4065,19 +4273,19 @@ async function syncCommand(options, cwd) {
4065
4273
  const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
4066
4274
  if (changes.length > 0 || statsDelta) {
4067
4275
  console.log(`
4068
- ${import_chalk17.default.bold("Changes:")}`);
4276
+ ${import_chalk19.default.bold("Changes:")}`);
4069
4277
  for (const change of changes) {
4070
- const icon = change.type === "removed" ? import_chalk17.default.red("-") : import_chalk17.default.green("+");
4278
+ const icon = change.type === "removed" ? import_chalk19.default.red("-") : import_chalk19.default.green("+");
4071
4279
  console.log(` ${icon} ${change.description}`);
4072
4280
  }
4073
4281
  if (statsDelta) {
4074
- console.log(` ${import_chalk17.default.dim(statsDelta)}`);
4282
+ console.log(` ${import_chalk19.default.dim(statsDelta)}`);
4075
4283
  }
4076
4284
  }
4077
4285
  if (options?.interactive) {
4078
- clack14.intro("viberails sync (interactive)");
4079
- clack14.note(formatRulesText(merged).join("\n"), "Rules after sync");
4080
- const decision = await clack14.select({
4286
+ clack15.intro("viberails sync (interactive)");
4287
+ clack15.note(formatRulesText(merged).join("\n"), "Rules after sync");
4288
+ const decision = await clack15.select({
4081
4289
  message: "How would you like to proceed?",
4082
4290
  options: [
4083
4291
  { value: "accept", label: "Accept changes" },
@@ -4087,7 +4295,7 @@ ${import_chalk17.default.bold("Changes:")}`);
4087
4295
  });
4088
4296
  assertNotCancelled(decision);
4089
4297
  if (decision === "cancel") {
4090
- clack14.outro("Sync cancelled. No files were written.");
4298
+ clack15.outro("Sync cancelled. No files were written.");
4091
4299
  return;
4092
4300
  }
4093
4301
  if (decision === "customize") {
@@ -4111,8 +4319,8 @@ ${import_chalk17.default.bold("Changes:")}`);
4111
4319
  fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
4112
4320
  `);
4113
4321
  writeGeneratedFiles(projectRoot, merged, scanResult);
4114
- clack14.log.success("Updated config with your customizations.");
4115
- clack14.outro("Done! Run viberails check to verify.");
4322
+ clack15.log.success("Updated config with your customizations.");
4323
+ clack15.outro("Done! Run viberails check to verify.");
4116
4324
  return;
4117
4325
  }
4118
4326
  }
@@ -4120,18 +4328,18 @@ ${import_chalk17.default.bold("Changes:")}`);
4120
4328
  `);
4121
4329
  writeGeneratedFiles(projectRoot, merged, scanResult);
4122
4330
  console.log(`
4123
- ${import_chalk17.default.bold("Synced:")}`);
4331
+ ${import_chalk19.default.bold("Synced:")}`);
4124
4332
  if (configChanged) {
4125
- console.log(` ${import_chalk17.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4333
+ console.log(` ${import_chalk19.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
4126
4334
  } else {
4127
- console.log(` ${import_chalk17.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4335
+ console.log(` ${import_chalk19.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
4128
4336
  }
4129
- console.log(` ${import_chalk17.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4130
- console.log(` ${import_chalk17.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
4337
+ console.log(` ${import_chalk19.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
4338
+ console.log(` ${import_chalk19.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
4131
4339
  }
4132
4340
 
4133
4341
  // src/index.ts
4134
- var VERSION = "0.6.10";
4342
+ var VERSION = "0.6.12";
4135
4343
  var program = new import_commander.Command();
4136
4344
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
4137
4345
  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) => {
@@ -4139,7 +4347,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
4139
4347
  await initCommand(options);
4140
4348
  } catch (err) {
4141
4349
  const message = err instanceof Error ? err.message : String(err);
4142
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4350
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4143
4351
  process.exit(1);
4144
4352
  }
4145
4353
  });
@@ -4148,7 +4356,7 @@ program.command("sync").description("Re-scan and update generated files").option
4148
4356
  await syncCommand(options);
4149
4357
  } catch (err) {
4150
4358
  const message = err instanceof Error ? err.message : String(err);
4151
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4359
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4152
4360
  process.exit(1);
4153
4361
  }
4154
4362
  });
@@ -4157,7 +4365,7 @@ program.command("config").description("Interactively edit existing config rules"
4157
4365
  await configCommand(options);
4158
4366
  } catch (err) {
4159
4367
  const message = err instanceof Error ? err.message : String(err);
4160
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4368
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4161
4369
  process.exit(1);
4162
4370
  }
4163
4371
  });
@@ -4178,7 +4386,7 @@ program.command("check").description("Check files against enforced rules").optio
4178
4386
  process.exit(exitCode);
4179
4387
  } catch (err) {
4180
4388
  const message = err instanceof Error ? err.message : String(err);
4181
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4389
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4182
4390
  process.exit(1);
4183
4391
  }
4184
4392
  }
@@ -4189,7 +4397,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
4189
4397
  process.exit(exitCode);
4190
4398
  } catch (err) {
4191
4399
  const message = err instanceof Error ? err.message : String(err);
4192
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4400
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4193
4401
  process.exit(1);
4194
4402
  }
4195
4403
  });
@@ -4198,7 +4406,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
4198
4406
  await boundariesCommand(options);
4199
4407
  } catch (err) {
4200
4408
  const message = err instanceof Error ? err.message : String(err);
4201
- console.error(`${import_chalk18.default.red("Error:")} ${message}`);
4409
+ console.error(`${import_chalk20.default.red("Error:")} ${message}`);
4202
4410
  process.exit(1);
4203
4411
  }
4204
4412
  });