viberails 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ import * as clack5 from "@clack/prompts";
33
33
  // src/utils/prompt-integrations.ts
34
34
  import { spawnSync } from "child_process";
35
35
  import * as clack from "@clack/prompts";
36
- async function promptHookManagerInstall(projectRoot, packageManager) {
36
+ async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
37
37
  const choice = await clack.select({
38
38
  message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
39
39
  options: [
@@ -52,7 +52,7 @@ async function promptHookManagerInstall(projectRoot, packageManager) {
52
52
  assertNotCancelled(choice);
53
53
  if (choice !== "install") return void 0;
54
54
  const pm = packageManager || "npm";
55
- const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? "pnpm add -D lefthook" : "npm install -D lefthook";
55
+ const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
56
56
  const s = clack.spinner();
57
57
  s.start("Installing Lefthook...");
58
58
  const result = spawnSync(installCmd, {
@@ -80,7 +80,8 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
80
80
  if (!resolvedHookManager) {
81
81
  resolvedHookManager = await promptHookManagerInstall(
82
82
  projectRoot,
83
- tools?.packageManager ?? "npm"
83
+ tools?.packageManager ?? "npm",
84
+ tools?.isWorkspace
84
85
  );
85
86
  }
86
87
  const isBareHook = !resolvedHookManager;
@@ -97,7 +98,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
97
98
  options.push({
98
99
  value: "typecheck",
99
100
  label: "Typecheck (tsc --noEmit)",
100
- hint: "catches type errors before commit"
101
+ hint: "pre-commit hook + CI check"
101
102
  });
102
103
  }
103
104
  if (tools?.linter) {
@@ -105,7 +106,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
105
106
  options.push({
106
107
  value: "lint",
107
108
  label: `Lint check (${linterName})`,
108
- hint: "runs linter on staged files before commit"
109
+ hint: "pre-commit hook + CI check"
109
110
  });
110
111
  }
111
112
  options.push(
@@ -1420,7 +1421,8 @@ function formatPackageSummary(pkg) {
1420
1421
  if (pkg.stack.styling) {
1421
1422
  parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1422
1423
  }
1423
- const files = `${pkg.statistics.totalFiles} files`;
1424
+ const n = pkg.statistics.totalFiles;
1425
+ const files = `${n} ${n === 1 ? "file" : "files"}`;
1424
1426
  const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1425
1427
  return ` ${pkg.relativePath} \u2014 ${detail}`;
1426
1428
  }
@@ -1476,7 +1478,8 @@ function formatPackageSummaryPlain(pkg) {
1476
1478
  if (pkg.stack.styling) {
1477
1479
  parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
1478
1480
  }
1479
- const files = `${pkg.statistics.totalFiles} files`;
1481
+ const n = pkg.statistics.totalFiles;
1482
+ const files = `${n} ${n === 1 ? "file" : "files"}`;
1480
1483
  const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1481
1484
  return ` ${pkg.relativePath} \u2014 ${detail}`;
1482
1485
  }
@@ -2479,7 +2482,8 @@ function checkCoveragePrereqs(projectRoot, scanResult) {
2479
2482
  return hasDependency(pkgDir, "@vitest/coverage-v8") || hasDependency(pkgDir, "@vitest/coverage-istanbul");
2480
2483
  });
2481
2484
  }
2482
- const addCmd = pm === "yarn" ? "yarn add -D" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
2485
+ const isWorkspace = scanResult.packages.length > 1;
2486
+ const addCmd = pm === "yarn" ? "yarn add -D" : pm === "pnpm" && isWorkspace ? "pnpm add -D -w" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
2483
2487
  const affectedPackages = vitestPackages.length > 1 ? vitestPackages : void 0;
2484
2488
  const reason = affectedPackages ? `Required for coverage in: ${affectedPackages.join(", ")}` : "Required for coverage percentage checks with vitest";
2485
2489
  return [
@@ -2680,7 +2684,6 @@ function addLefthookPreCommit(lefthookPath) {
2680
2684
  function detectHookManager(projectRoot) {
2681
2685
  if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
2682
2686
  if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
2683
- if (fs16.existsSync(path16.join(projectRoot, ".git"))) return "git hook";
2684
2687
  return void 0;
2685
2688
  }
2686
2689
  function setupClaudeCodeHook(projectRoot) {
@@ -2734,7 +2737,7 @@ function setupClaudeMdReference(projectRoot) {
2734
2737
  fs16.writeFileSync(claudeMdPath, prefix + ref);
2735
2738
  console.log(` ${chalk9.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
2736
2739
  }
2737
- function setupGithubAction(projectRoot, packageManager) {
2740
+ function setupGithubAction(projectRoot, packageManager, options) {
2738
2741
  const workflowDir = path16.join(projectRoot, ".github", "workflows");
2739
2742
  const workflowPath = path16.join(workflowDir, "viberails.yml");
2740
2743
  if (fs16.existsSync(workflowPath)) {
@@ -2770,7 +2773,16 @@ function setupGithubAction(projectRoot, packageManager) {
2770
2773
  " node-version: 22",
2771
2774
  pm !== "npm" ? ` cache: ${pm}` : "",
2772
2775
  "",
2773
- ` - run: ${installCmd}`,
2776
+ ` - run: ${installCmd}`
2777
+ );
2778
+ if (options?.typecheck) {
2779
+ lines.push(` - run: ${runPrefix} tsc --noEmit`);
2780
+ }
2781
+ if (options?.linter) {
2782
+ const lintCmd = options.linter === "biome" ? "biome check ." : "eslint .";
2783
+ lines.push(` - run: ${runPrefix} ${lintCmd}`);
2784
+ }
2785
+ lines.push(
2774
2786
  ` - run: ${runPrefix} viberails check --enforce --diff-base origin/\${{ github.event.pull_request.base.ref }}`,
2775
2787
  ""
2776
2788
  );
@@ -2891,7 +2903,10 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
2891
2903
  created.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
2892
2904
  }
2893
2905
  if (integrations.githubAction) {
2894
- const t = setupGithubAction(projectRoot, opts.packageManager ?? "npm");
2906
+ const t = setupGithubAction(projectRoot, opts.packageManager ?? "npm", {
2907
+ linter: integrations.lintHook ? opts.linter : void 0,
2908
+ typecheck: integrations.typecheckHook
2909
+ });
2895
2910
  if (t) created.push(`${t} \u2014 blocks PRs on violations`);
2896
2911
  }
2897
2912
  return created;
@@ -2959,11 +2974,15 @@ async function initNonInteractive(projectRoot, configPath) {
2959
2974
  setupClaudeMdReference(projectRoot);
2960
2975
  const rootPkg = config.packages[0];
2961
2976
  const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
2962
- const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
2977
+ const linter = rootPkg?.stack?.linter?.split("@")[0];
2978
+ const isTypeScript = rootPkg?.stack?.language === "typescript";
2979
+ const actionTarget = setupGithubAction(projectRoot, rootPkgPm, {
2980
+ linter,
2981
+ typecheck: isTypeScript
2982
+ });
2963
2983
  const hookManager = detectHookManager(projectRoot);
2964
2984
  const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
2965
2985
  const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
2966
- const linter = rootPkg?.stack?.linter?.split("@")[0];
2967
2986
  const ok = chalk11.green("\u2713");
2968
2987
  const created = [
2969
2988
  `${ok} ${path18.basename(configPath)}`,
@@ -2996,13 +3015,6 @@ async function initInteractive(projectRoot, configPath, options) {
2996
3015
  const scanResult = await scan2(projectRoot);
2997
3016
  const config = generateConfig(scanResult);
2998
3017
  s.stop("Scan complete");
2999
- const prereqResult = await promptMissingPrereqs(
3000
- projectRoot,
3001
- checkCoveragePrereqs(projectRoot, scanResult)
3002
- );
3003
- if (prereqResult.disableCoverage) {
3004
- config.rules.testCoverage = 0;
3005
- }
3006
3018
  if (scanResult.statistics.totalFiles === 0) {
3007
3019
  clack8.log.warn(
3008
3020
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
@@ -3043,20 +3055,29 @@ async function initInteractive(projectRoot, configPath, options) {
3043
3055
  if (denyCount > 0) {
3044
3056
  config.boundaries = inferred;
3045
3057
  config.rules.enforceBoundaries = true;
3046
- bs.stop(`Inferred ${denyCount} boundary rules`);
3047
- const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
3048
- clack8.note(boundaryLines, "Boundary rules");
3058
+ const pkgCount = Object.keys(inferred.deny).length;
3059
+ bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
3049
3060
  } else {
3050
3061
  bs.stop("No boundary rules inferred");
3051
3062
  }
3052
3063
  }
3053
3064
  }
3054
3065
  const hookManager = detectHookManager(projectRoot);
3066
+ const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
3067
+ const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
3068
+ if (hasMissingPrereqs) {
3069
+ clack8.log.info("Some dependencies are needed for full functionality.");
3070
+ }
3071
+ const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
3072
+ if (prereqResult.disableCoverage) {
3073
+ config.rules.testCoverage = 0;
3074
+ }
3055
3075
  const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
3056
3076
  const integrations = await promptIntegrations(projectRoot, hookManager, {
3057
3077
  isTypeScript: rootPkgStack?.language === "typescript",
3058
3078
  linter: rootPkgStack?.linter?.split("@")[0],
3059
- packageManager: rootPkgStack?.packageManager
3079
+ packageManager: rootPkgStack?.packageManager,
3080
+ isWorkspace: config.packages.length > 1
3060
3081
  });
3061
3082
  const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
3062
3083
  if (!shouldWrite) {
@@ -3068,17 +3089,14 @@ async function initInteractive(projectRoot, configPath, options) {
3068
3089
  `);
3069
3090
  writeGeneratedFiles(projectRoot, config, scanResult);
3070
3091
  updateGitignore(projectRoot);
3071
- const createdFiles = [
3072
- path18.basename(configPath),
3073
- ".viberails/context.md",
3074
- ".viberails/scan-result.json",
3075
- ...setupSelectedIntegrations(projectRoot, integrations, {
3076
- linter: rootPkgStack?.linter?.split("@")[0],
3077
- packageManager: rootPkgStack?.packageManager
3078
- })
3079
- ];
3080
- clack8.log.success(`Created:
3081
- ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
3092
+ const ok = chalk11.green("\u2713");
3093
+ clack8.log.step(`${ok} ${path18.basename(configPath)}`);
3094
+ clack8.log.step(`${ok} .viberails/context.md`);
3095
+ clack8.log.step(`${ok} .viberails/scan-result.json`);
3096
+ setupSelectedIntegrations(projectRoot, integrations, {
3097
+ linter: rootPkgStack?.linter?.split("@")[0],
3098
+ packageManager: rootPkgStack?.packageManager
3099
+ });
3082
3100
  clack8.outro(
3083
3101
  `Done! Next: review viberails.config.json, then run viberails check
3084
3102
  ${chalk11.dim("Tip: use")} ${chalk11.cyan("viberails check --enforce")} ${chalk11.dim("in CI to block PRs on violations.")}`
@@ -3192,7 +3210,7 @@ ${chalk12.bold("Synced:")}`);
3192
3210
  }
3193
3211
 
3194
3212
  // src/index.ts
3195
- var VERSION = "0.5.3";
3213
+ var VERSION = "0.5.5";
3196
3214
  var program = new Command();
3197
3215
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
3198
3216
  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) => {