viberails 0.6.2 → 0.6.4

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
@@ -31,11 +31,34 @@ function findProjectRoot(startDir) {
31
31
  import * as clack5 from "@clack/prompts";
32
32
 
33
33
  // src/utils/prompt-integrations.ts
34
- import { spawnSync } from "child_process";
35
34
  import * as clack from "@clack/prompts";
35
+
36
+ // src/utils/spawn-async.ts
37
+ import { spawn } from "child_process";
38
+ function spawnAsync(command, cwd) {
39
+ return new Promise((resolve4) => {
40
+ const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
41
+ let stdout = "";
42
+ let stderr = "";
43
+ child.stdout.on("data", (d) => {
44
+ stdout += d.toString();
45
+ });
46
+ child.stderr.on("data", (d) => {
47
+ stderr += d.toString();
48
+ });
49
+ child.on("close", (status) => {
50
+ resolve4({ status, stdout, stderr });
51
+ });
52
+ child.on("error", () => {
53
+ resolve4({ status: 1, stdout, stderr });
54
+ });
55
+ });
56
+ }
57
+
58
+ // src/utils/prompt-integrations.ts
36
59
  async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
37
60
  const choice = await clack.select({
38
- message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
61
+ message: "No shared git hook manager detected. Install Lefthook?",
39
62
  options: [
40
63
  {
41
64
  value: "install",
@@ -55,12 +78,7 @@ async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace
55
78
  const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
56
79
  const s = clack.spinner();
57
80
  s.start("Installing Lefthook...");
58
- const result = spawnSync(installCmd, {
59
- cwd: projectRoot,
60
- shell: true,
61
- encoding: "utf-8",
62
- stdio: "pipe"
63
- });
81
+ const result = await spawnAsync(installCmd, projectRoot);
64
82
  if (result.status === 0) {
65
83
  const fs21 = await import("fs");
66
84
  const path21 = await import("path");
@@ -128,7 +146,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
128
146
  );
129
147
  const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
130
148
  const result = await clack.multiselect({
131
- message: "Set up integrations?",
149
+ message: "Optional integrations",
132
150
  options,
133
151
  initialValues,
134
152
  required: false
@@ -535,16 +553,49 @@ async function confirmDangerous(message) {
535
553
  assertNotCancelled(result);
536
554
  return result;
537
555
  }
556
+ async function promptExistingConfigAction(configFile) {
557
+ const result = await clack5.select({
558
+ message: `${configFile} already exists. What do you want to do?`,
559
+ options: [
560
+ {
561
+ value: "edit",
562
+ label: "Edit existing config",
563
+ hint: "open the current rules and save updates in place"
564
+ },
565
+ {
566
+ value: "replace",
567
+ label: "Replace with a fresh scan",
568
+ hint: "re-scan the project and overwrite the current config"
569
+ },
570
+ {
571
+ value: "cancel",
572
+ label: "Cancel",
573
+ hint: "leave the current setup unchanged"
574
+ }
575
+ ]
576
+ });
577
+ assertNotCancelled(result);
578
+ return result;
579
+ }
538
580
  async function promptInitDecision() {
539
581
  const result = await clack5.select({
540
- message: "Accept these rules?",
582
+ message: "How do you want to proceed?",
541
583
  options: [
542
584
  {
543
585
  value: "accept",
544
- label: "Yes, looks good",
545
- hint: "warns on violation; use --enforce in CI to block"
586
+ label: "Accept defaults",
587
+ hint: "writes the config with these defaults; use --enforce in CI to block"
546
588
  },
547
- { value: "customize", label: "Let me customize rules" }
589
+ {
590
+ value: "customize",
591
+ label: "Customize rules",
592
+ hint: "edit limits, naming, test coverage, and package overrides"
593
+ },
594
+ {
595
+ value: "review",
596
+ label: "Review detected details",
597
+ hint: "show the full scan report with package and structure details"
598
+ }
548
599
  ]
549
600
  });
550
601
  assertNotCancelled(result);
@@ -742,7 +793,7 @@ function resolveIgnoreForFile(relPath, config) {
742
793
  }
743
794
 
744
795
  // src/commands/check-coverage.ts
745
- import { spawnSync as spawnSync2 } from "child_process";
796
+ import { spawnSync } from "child_process";
746
797
  import * as fs4 from "fs";
747
798
  import * as path4 from "path";
748
799
  import { inferCoverageCommand } from "@viberails/config";
@@ -785,7 +836,7 @@ function readCoveragePercentage(summaryPath) {
785
836
  }
786
837
  }
787
838
  function runCoverageCommand(pkgRoot, command) {
788
- const result = spawnSync2(command, {
839
+ const result = spawnSync(command, {
789
840
  cwd: pkgRoot,
790
841
  shell: true,
791
842
  encoding: "utf-8",
@@ -1603,6 +1654,15 @@ function formatMonorepoResultsText(scanResult) {
1603
1654
  }
1604
1655
 
1605
1656
  // src/display.ts
1657
+ var INIT_OVERVIEW_NAMES = {
1658
+ typescript: "TypeScript",
1659
+ javascript: "JavaScript",
1660
+ eslint: "ESLint",
1661
+ prettier: "Prettier",
1662
+ jest: "Jest",
1663
+ vitest: "Vitest",
1664
+ biome: "Biome"
1665
+ };
1606
1666
  function formatItem(item, nameMap) {
1607
1667
  const name = nameMap?.[item.name] ?? item.name;
1608
1668
  return item.version ? `${name} ${item.version}` : name;
@@ -1732,11 +1792,40 @@ function displayRulesPreview(config) {
1732
1792
  );
1733
1793
  console.log("");
1734
1794
  }
1735
- function displayInitSummary(config, exemptedPackages) {
1795
+ function formatDetectedOverview(scanResult) {
1796
+ const { stack } = scanResult;
1797
+ const primaryParts = [];
1798
+ const secondaryParts = [];
1799
+ const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
1800
+ if (scanResult.packages.length > 1) {
1801
+ primaryParts.push("monorepo");
1802
+ primaryParts.push(`${scanResult.packages.length} packages`);
1803
+ } else if (stack.framework) {
1804
+ primaryParts.push(formatItem(stack.framework, FRAMEWORK_NAMES2));
1805
+ } else {
1806
+ primaryParts.push("single package");
1807
+ }
1808
+ primaryParts.push(formatOverviewItem(stack.language));
1809
+ if (stack.styling) {
1810
+ primaryParts.push(formatOverviewItem(stack.styling, STYLING_NAMES2));
1811
+ }
1812
+ if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
1813
+ if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
1814
+ if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
1815
+ if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
1816
+ const primary = primaryParts.map((part) => chalk5.cyan(part)).join(chalk5.dim(" \xB7 "));
1817
+ const secondary = secondaryParts.join(chalk5.dim(" \xB7 "));
1818
+ return secondary ? `${primary}
1819
+ ${chalk5.dim(secondary)}` : primary;
1820
+ }
1821
+ function displayInitOverview(scanResult, config, exemptedPackages) {
1736
1822
  const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
1737
1823
  const isMonorepo = config.packages.length > 1;
1738
1824
  const ok = chalk5.green("\u2713");
1739
- const off = chalk5.dim("\u25CB");
1825
+ const info = chalk5.yellow("~");
1826
+ console.log("");
1827
+ console.log(` ${chalk5.bold("Ready to initialize:")}`);
1828
+ console.log(` ${formatDetectedOverview(scanResult)}`);
1740
1829
  console.log("");
1741
1830
  console.log(` ${chalk5.bold("Rules to apply:")}`);
1742
1831
  console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
@@ -1744,7 +1833,7 @@ function displayInitSummary(config, exemptedPackages) {
1744
1833
  if (config.rules.enforceNaming && fileNaming) {
1745
1834
  console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
1746
1835
  } else {
1747
- console.log(` ${off} File naming: ${chalk5.dim("not enforced")}`);
1836
+ console.log(` ${info} File naming: ${chalk5.dim("not enforced")}`);
1748
1837
  }
1749
1838
  const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
1750
1839
  if (config.rules.enforceMissingTests && testPattern) {
@@ -1752,7 +1841,7 @@ function displayInitSummary(config, exemptedPackages) {
1752
1841
  } else if (config.rules.enforceMissingTests) {
1753
1842
  console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
1754
1843
  } else {
1755
- console.log(` ${off} Missing tests: ${chalk5.dim("not enforced")}`);
1844
+ console.log(` ${info} Missing tests: ${chalk5.dim("not enforced")}`);
1756
1845
  }
1757
1846
  if (config.rules.testCoverage > 0) {
1758
1847
  if (isMonorepo) {
@@ -1766,21 +1855,68 @@ function displayInitSummary(config, exemptedPackages) {
1766
1855
  console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
1767
1856
  }
1768
1857
  } else {
1769
- console.log(` ${off} Coverage: ${chalk5.dim("disabled")}`);
1858
+ console.log(` ${info} Coverage: ${chalk5.dim("disabled")}`);
1770
1859
  }
1771
1860
  if (exemptedPackages.length > 0) {
1772
1861
  console.log(
1773
1862
  ` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
1774
1863
  );
1775
1864
  }
1865
+ console.log("");
1866
+ console.log(` ${chalk5.bold("Also available:")}`);
1776
1867
  if (isMonorepo) {
1777
- console.log(
1778
- `
1779
- ${chalk5.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
1780
- );
1868
+ console.log(` ${info} Infer boundaries from current imports`);
1869
+ }
1870
+ console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
1871
+ console.log(
1872
+ `
1873
+ ${chalk5.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
1874
+ );
1875
+ console.log("");
1876
+ }
1877
+ function summarizeSelectedIntegrations(integrations, opts) {
1878
+ const lines = [];
1879
+ if (opts.hasBoundaries) {
1880
+ lines.push("\u2713 Boundary rules: inferred from current imports");
1781
1881
  } else {
1782
- console.log(`
1783
- ${chalk5.dim("warns on violation \xB7 use --enforce in CI to block")}`);
1882
+ lines.push("~ Boundary rules: not enabled");
1883
+ }
1884
+ if (opts.hasCoverage) {
1885
+ lines.push("\u2713 Coverage checks: enabled");
1886
+ } else {
1887
+ lines.push("~ Coverage checks: disabled");
1888
+ }
1889
+ const selectedIntegrations = [
1890
+ integrations.preCommitHook ? "pre-commit hook" : void 0,
1891
+ integrations.typecheckHook ? "typecheck" : void 0,
1892
+ integrations.lintHook ? "lint check" : void 0,
1893
+ integrations.claudeCodeHook ? "Claude Code hook" : void 0,
1894
+ integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
1895
+ integrations.githubAction ? "GitHub Actions workflow" : void 0
1896
+ ].filter(Boolean);
1897
+ if (selectedIntegrations.length > 0) {
1898
+ lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
1899
+ } else {
1900
+ lines.push("~ Integrations: none selected");
1901
+ }
1902
+ return lines;
1903
+ }
1904
+ function displaySetupPlan(config, integrations, opts = {}) {
1905
+ const configFile = opts.configFile ?? "viberails.config.json";
1906
+ const lines = summarizeSelectedIntegrations(integrations, {
1907
+ hasBoundaries: config.rules.enforceBoundaries,
1908
+ hasCoverage: config.rules.testCoverage > 0
1909
+ });
1910
+ console.log("");
1911
+ console.log(` ${chalk5.bold("Ready to write:")}`);
1912
+ console.log(
1913
+ ` ${opts.replacingExistingConfig ? chalk5.yellow("!") : chalk5.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? chalk5.dim(" (replacing existing config)") : ""}`
1914
+ );
1915
+ console.log(` ${chalk5.green("\u2713")} .viberails/context.md`);
1916
+ console.log(` ${chalk5.green("\u2713")} .viberails/scan-result.json`);
1917
+ for (const line of lines) {
1918
+ const icon = line.startsWith("\u2713") ? chalk5.green("\u2713") : chalk5.yellow("~");
1919
+ console.log(` ${icon} ${line.slice(2)}`);
1784
1920
  }
1785
1921
  console.log("");
1786
1922
  }
@@ -2092,10 +2228,12 @@ async function configCommand(options, cwd) {
2092
2228
  }
2093
2229
  const configPath = path9.join(projectRoot, CONFIG_FILE3);
2094
2230
  if (!fs10.existsSync(configPath)) {
2095
- console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails init")} first.`);
2231
+ console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails")} first.`);
2096
2232
  return;
2097
2233
  }
2098
- clack6.intro("viberails config");
2234
+ if (!options.suppressIntro) {
2235
+ clack6.intro("viberails config");
2236
+ }
2099
2237
  const config = await loadConfig3(configPath);
2100
2238
  let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
2101
2239
  clack6.note(formatRulesText(config).join("\n"), "Current rules");
@@ -2669,7 +2807,6 @@ import { scan as scan2 } from "@viberails/scanner";
2669
2807
  import chalk12 from "chalk";
2670
2808
 
2671
2809
  // src/utils/check-prerequisites.ts
2672
- import { spawnSync as spawnSync3 } from "child_process";
2673
2810
  import * as fs14 from "fs";
2674
2811
  import * as path14 from "path";
2675
2812
  import * as clack7 from "@clack/prompts";
@@ -2718,7 +2855,7 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2718
2855
  const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
2719
2856
  return `\u2717 ${p.label} \u2014 ${detail}`;
2720
2857
  }).join("\n");
2721
- clack7.note(prereqLines, "Coverage prerequisites");
2858
+ clack7.note(prereqLines, "Coverage support");
2722
2859
  let disableCoverage = false;
2723
2860
  for (const m of missing) {
2724
2861
  if (!m.installCommand) continue;
@@ -2729,13 +2866,13 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2729
2866
  options: [
2730
2867
  {
2731
2868
  value: "install",
2732
- label: `Yes, install now`,
2869
+ label: "Install now",
2733
2870
  hint: m.installCommand
2734
2871
  },
2735
2872
  {
2736
2873
  value: "disable",
2737
- label: "No, disable coverage percentage checks",
2738
- hint: "missing-test checks still active"
2874
+ label: "Disable coverage checks",
2875
+ hint: "missing-test checks still stay active"
2739
2876
  },
2740
2877
  {
2741
2878
  value: "skip",
@@ -2748,12 +2885,7 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
2748
2885
  if (choice === "install") {
2749
2886
  const is = clack7.spinner();
2750
2887
  is.start(`Installing ${m.label}...`);
2751
- const result = spawnSync3(m.installCommand, {
2752
- cwd: projectRoot,
2753
- shell: true,
2754
- encoding: "utf-8",
2755
- stdio: "pipe"
2756
- });
2888
+ const result = await spawnAsync(m.installCommand, projectRoot);
2757
2889
  if (result.status === 0) {
2758
2890
  is.stop(`Installed ${m.label}`);
2759
2891
  } else {
@@ -3200,9 +3332,12 @@ async function initCommand(options, cwd) {
3200
3332
  }
3201
3333
  const configPath = path19.join(projectRoot, CONFIG_FILE5);
3202
3334
  if (fs19.existsSync(configPath) && !options.force) {
3335
+ if (!options.yes) {
3336
+ return initInteractive(projectRoot, configPath, options);
3337
+ }
3203
3338
  console.log(
3204
3339
  `${chalk12.yellow("!")} viberails is already initialized.
3205
- Run ${chalk12.cyan("viberails config")} to edit rules, ${chalk12.cyan("viberails sync")} to update, or ${chalk12.cyan("viberails init --force")} to start fresh.`
3340
+ Run ${chalk12.cyan("viberails")} to review or edit the existing setup, ${chalk12.cyan("viberails sync")} to update generated files, or ${chalk12.cyan("viberails init --force")} to replace it.`
3206
3341
  );
3207
3342
  return;
3208
3343
  }
@@ -3280,6 +3415,19 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
3280
3415
  }
3281
3416
  async function initInteractive(projectRoot, configPath, options) {
3282
3417
  clack8.intro("viberails");
3418
+ const replacingExistingConfig = fs19.existsSync(configPath);
3419
+ if (fs19.existsSync(configPath) && !options.force) {
3420
+ const action = await promptExistingConfigAction(path19.basename(configPath));
3421
+ if (action === "cancel") {
3422
+ clack8.outro("Aborted. No files were written.");
3423
+ return;
3424
+ }
3425
+ if (action === "edit") {
3426
+ await configCommand({ suppressIntro: true }, projectRoot);
3427
+ return;
3428
+ }
3429
+ options.force = true;
3430
+ }
3283
3431
  if (fs19.existsSync(configPath) && options.force) {
3284
3432
  const replace = await confirmDangerous(
3285
3433
  `${path19.basename(configPath)} already exists and will be replaced. Continue?`
@@ -3299,10 +3447,18 @@ async function initInteractive(projectRoot, configPath, options) {
3299
3447
  "No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
3300
3448
  );
3301
3449
  }
3302
- clack8.note(formatScanResultsText(scanResult), "Scan results");
3303
3450
  const exemptedPkgs = getExemptedPackages(config);
3304
- displayInitSummary(config, exemptedPkgs);
3305
- const decision = await promptInitDecision();
3451
+ let decision;
3452
+ while (true) {
3453
+ displayInitOverview(scanResult, config, exemptedPkgs);
3454
+ const nextDecision = await promptInitDecision();
3455
+ if (nextDecision === "review") {
3456
+ clack8.note(formatScanResultsText(scanResult), "Detected details");
3457
+ continue;
3458
+ }
3459
+ decision = nextDecision;
3460
+ break;
3461
+ }
3306
3462
  if (decision === "customize") {
3307
3463
  const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
3308
3464
  const overrides = await promptRuleMenu({
@@ -3319,10 +3475,10 @@ async function initInteractive(projectRoot, configPath, options) {
3319
3475
  }
3320
3476
  if (config.packages.length > 1) {
3321
3477
  clack8.note(
3322
- "Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
3478
+ "Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
3323
3479
  "Boundaries"
3324
3480
  );
3325
- const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
3481
+ const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
3326
3482
  if (shouldInfer) {
3327
3483
  const bs = clack8.spinner();
3328
3484
  bs.start("Building import graph...");
@@ -3358,27 +3514,31 @@ async function initInteractive(projectRoot, configPath, options) {
3358
3514
  packageManager: rootPkgStack?.packageManager?.split("@")[0],
3359
3515
  isWorkspace: config.packages.length > 1
3360
3516
  });
3361
- const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
3517
+ displaySetupPlan(config, integrations, {
3518
+ replacingExistingConfig,
3519
+ configFile: path19.basename(configPath)
3520
+ });
3521
+ const shouldWrite = await confirm3("Apply this setup?");
3362
3522
  if (!shouldWrite) {
3363
3523
  clack8.outro("Aborted. No files were written.");
3364
3524
  return;
3365
3525
  }
3366
3526
  const ws = clack8.spinner();
3367
- ws.start("Writing configuration and setting up integrations...");
3527
+ ws.start("Writing configuration...");
3368
3528
  const compacted = compactConfig3(config);
3369
3529
  fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
3370
3530
  `);
3371
3531
  writeGeneratedFiles(projectRoot, config, scanResult);
3372
3532
  updateGitignore(projectRoot);
3373
- setupSelectedIntegrations(projectRoot, integrations, {
3374
- linter: rootPkgStack?.linter?.split("@")[0],
3375
- packageManager: rootPkgStack?.packageManager?.split("@")[0]
3376
- });
3377
3533
  ws.stop("Configuration written");
3378
3534
  const ok = chalk12.green("\u2713");
3379
3535
  clack8.log.step(`${ok} ${path19.basename(configPath)}`);
3380
3536
  clack8.log.step(`${ok} .viberails/context.md`);
3381
3537
  clack8.log.step(`${ok} .viberails/scan-result.json`);
3538
+ setupSelectedIntegrations(projectRoot, integrations, {
3539
+ linter: rootPkgStack?.linter?.split("@")[0],
3540
+ packageManager: rootPkgStack?.packageManager?.split("@")[0]
3541
+ });
3382
3542
  clack8.outro(
3383
3543
  `Done! Next: review viberails.config.json, then run viberails check
3384
3544
  ${chalk12.dim("Tip: use")} ${chalk12.cyan("viberails check --enforce")} ${chalk12.dim("in CI to block PRs on violations.")}`
@@ -3494,7 +3654,7 @@ ${chalk13.bold("Synced:")}`);
3494
3654
  }
3495
3655
 
3496
3656
  // src/index.ts
3497
- var VERSION = "0.6.2";
3657
+ var VERSION = "0.6.4";
3498
3658
  var program = new Command();
3499
3659
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
3500
3660
  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) => {