viberails 0.6.11 → 0.6.13
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/README.md +140 -0
- package/dist/index.cjs +169 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +169 -155
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# viberails
|
|
2
|
+
|
|
3
|
+
Guardrails for vibe coding.
|
|
4
|
+
|
|
5
|
+
viberails scans your existing JavaScript/TypeScript project, detects the conventions you're already following, and enforces them with file-level feedback during AI edits and structured enforcement at commit and PR time. Rules are derived from your actual codebase, not a template.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
AI coding tools are fast but inconsistent. They'll use camelCase in one file and kebab-case in another, create 500-line files, and ignore your project's import boundaries. viberails catches this by learning your conventions and enforcing them where it matters: file-level checks surface naming and size issues immediately during AI edits, while commit hooks and CI enforce naming, file-size, missing-test, and boundary rules. Coverage enforcement runs via `viberails check` (full mode).
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd your-project
|
|
15
|
+
npx viberails
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The interactive wizard scans your project, shows what it found with confidence levels, and lets you customize rules before generating config. It also sets up pre-commit hooks and Claude Code integration.
|
|
19
|
+
If a config already exists, re-running `viberails` lets you edit it, replace it with a fresh scan, or cancel. `viberails config` remains available as a shortcut for direct rule editing.
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
**Scans** your codebase to detect framework, language, styling, tooling, directory structure, and naming conventions, each scored by consistency across your files.
|
|
24
|
+
|
|
25
|
+
**Enforces** four rules:
|
|
26
|
+
- **File size** — files over 300 lines (configurable) are flagged
|
|
27
|
+
- **Naming conventions** — detects your naming style (kebab-case, camelCase, PascalCase, snake_case) and enforces it
|
|
28
|
+
- **Missing tests** — source files must have corresponding test files
|
|
29
|
+
- **Import boundaries** — prevents packages from importing where they shouldn't (monorepos)
|
|
30
|
+
|
|
31
|
+
**Fixes** violations automatically:
|
|
32
|
+
- Renames files to match your convention and updates relative imports via AST rewriting (aliased imports like `@/...` require manual updates)
|
|
33
|
+
- Generates test stubs for missing test files
|
|
34
|
+
|
|
35
|
+
## What It Generates
|
|
36
|
+
|
|
37
|
+
| File | Purpose |
|
|
38
|
+
|------|---------|
|
|
39
|
+
| `viberails.config.json` | Detected stack, conventions, boundary rules, and thresholds |
|
|
40
|
+
| `.viberails/context.md` | Enforced rules in natural language for AI tools to follow |
|
|
41
|
+
| `.viberails/scan-result.json` | Raw scan data (gitignored) |
|
|
42
|
+
|
|
43
|
+
The generated `context.md` is designed to be referenced from your `CLAUDE.md`, `.cursorrules`, or similar AI context files so that AI tools automatically follow your project's conventions.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
### `npx viberails` / `viberails init`
|
|
48
|
+
|
|
49
|
+
Scans your project and walks you through setup. If a config already exists, the same command lets you edit the current setup or replace it with a fresh scan.
|
|
50
|
+
|
|
51
|
+
| Flag | Effect |
|
|
52
|
+
|------|--------|
|
|
53
|
+
| `--yes` / `-y` | Non-interactive. Uses defaults, keeps high-confidence conventions, and auto-sets up integrations. |
|
|
54
|
+
| `--force` / `-f` | Re-initialize from scratch, replacing existing config without the edit/replace chooser. |
|
|
55
|
+
|
|
56
|
+
### `viberails check`
|
|
57
|
+
|
|
58
|
+
Validates your project against configured rules.
|
|
59
|
+
|
|
60
|
+
| Flag | Effect |
|
|
61
|
+
|------|--------|
|
|
62
|
+
| `--staged` | Check only git-staged files (used by pre-commit hook). |
|
|
63
|
+
| `--files <paths>` | Check specific files. |
|
|
64
|
+
| `--format json` | Machine-readable output for tool integration. |
|
|
65
|
+
| `--quiet` | Summary only. |
|
|
66
|
+
| `--enforce` | Return exit code 1 when violations are found (CI mode). |
|
|
67
|
+
| `--diff-base <ref>` | Check only files changed since a git ref (useful in CI). |
|
|
68
|
+
| `--hook` | Output via stdin/stderr for Claude Code hook integration. |
|
|
69
|
+
|
|
70
|
+
### `viberails fix`
|
|
71
|
+
|
|
72
|
+
Auto-fixes naming violations and generates missing test stubs.
|
|
73
|
+
|
|
74
|
+
| Flag | Effect |
|
|
75
|
+
|------|--------|
|
|
76
|
+
| `--dry-run` | Preview changes without applying them. |
|
|
77
|
+
| `--rule <name>` | Fix only `file-naming` or `missing-test`. |
|
|
78
|
+
| `--yes` / `-y` | Apply fixes without confirmation. |
|
|
79
|
+
|
|
80
|
+
### `viberails config`
|
|
81
|
+
|
|
82
|
+
Interactively edit existing config rules without re-initializing. Opens the same rule menu used during `init` with your current values pre-filled. Most users can simply re-run `viberails`; this command is the direct shortcut.
|
|
83
|
+
|
|
84
|
+
| Flag | Effect |
|
|
85
|
+
|------|--------|
|
|
86
|
+
| `--rescan` | Re-scan the project first, picking up new packages and stack changes. |
|
|
87
|
+
|
|
88
|
+
### `viberails sync`
|
|
89
|
+
|
|
90
|
+
Re-scans and regenerates context files. Preserves manual edits to `viberails.config.json` and reports what changed.
|
|
91
|
+
|
|
92
|
+
| Flag | Effect |
|
|
93
|
+
|------|--------|
|
|
94
|
+
| `--interactive` / `-i` | Review changes before writing. Choose to accept, customize rules, or cancel. |
|
|
95
|
+
|
|
96
|
+
### `viberails boundaries`
|
|
97
|
+
|
|
98
|
+
Displays boundary rules and violations. Use `--infer` to re-infer rules from your current import graph.
|
|
99
|
+
|
|
100
|
+
## Hooks
|
|
101
|
+
|
|
102
|
+
During `viberails init`, you can set up automatic enforcement:
|
|
103
|
+
|
|
104
|
+
### Pre-commit hook
|
|
105
|
+
|
|
106
|
+
Detects your hook manager (Lefthook, Husky, or bare git) and adds `viberails check --staged`. Violations warn by default. Use `viberails check --enforce` (for CI) to make violations fail with exit code 1.
|
|
107
|
+
|
|
108
|
+
### Claude Code hook
|
|
109
|
+
|
|
110
|
+
Adds a PostToolUse hook to `.claude/settings.json` that runs fast file-scoped checks after every edit, surfacing naming and file-size issues immediately. Repository-level checks like missing tests and boundaries are enforced at commit and PR time via staged checks and CI. Coverage enforcement runs via `viberails check` (full mode) or your existing CI test pipeline.
|
|
111
|
+
|
|
112
|
+
### GitHub Actions
|
|
113
|
+
|
|
114
|
+
Generates a `.github/workflows/viberails.yml` workflow that runs `viberails check --enforce --diff-base` on pull requests, checking only files changed in the PR.
|
|
115
|
+
|
|
116
|
+
### Typecheck & lint hooks
|
|
117
|
+
|
|
118
|
+
Optionally adds `tsc --noEmit` and your linter (Biome, ESLint) as pre-commit checks during `viberails init`.
|
|
119
|
+
|
|
120
|
+
## Confidence Model
|
|
121
|
+
|
|
122
|
+
Not all conventions are equally consistent in a codebase. viberails scores each detection:
|
|
123
|
+
|
|
124
|
+
| Level | Consistency | Behavior |
|
|
125
|
+
|-------|-------------|----------|
|
|
126
|
+
| **High** | >= 90% | Enforced by default |
|
|
127
|
+
| **Medium** | 70-89% | Included in config, not enforced |
|
|
128
|
+
| **Low** | < 70% | Omitted entirely |
|
|
129
|
+
|
|
130
|
+
In `--yes` mode, only high-confidence conventions are included.
|
|
131
|
+
|
|
132
|
+
## Monorepo Support
|
|
133
|
+
|
|
134
|
+
viberails detects workspaces (pnpm, npm, yarn), scans each package independently, and infers import boundaries from your existing dependency graph. Packages that don't import from each other get automatic deny rules, preventing accidental coupling before it starts.
|
|
135
|
+
|
|
136
|
+
Per-package convention overrides are detected and displayed during setup.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -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
|
|
103
|
-
{ value: "camelCase", label: "camelCase", hint: "myComponent
|
|
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" },
|
|
@@ -3008,7 +3008,169 @@ var clack11 = __toESM(require("@clack/prompts"), 1);
|
|
|
3008
3008
|
|
|
3009
3009
|
// src/utils/prompt-main-menu-handlers.ts
|
|
3010
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
|
|
3011
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) {
|
|
3021
|
+
const rootPkg = getRootPackage(config.packages);
|
|
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
|
+
);
|
|
3164
|
+
}
|
|
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;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// src/utils/prompt-main-menu-handlers.ts
|
|
3012
3174
|
async function handleFileNaming(config, scanResult) {
|
|
3013
3175
|
const isMonorepo = config.packages.length > 1;
|
|
3014
3176
|
if (isMonorepo) {
|
|
@@ -3032,10 +3194,11 @@ async function handleFileNaming(config, scanResult) {
|
|
|
3032
3194
|
return { value: opt.value, label: opt.label };
|
|
3033
3195
|
});
|
|
3034
3196
|
const rootPkg = getRootPackage(config.packages);
|
|
3197
|
+
const effective = getEffectiveFileNaming(config);
|
|
3035
3198
|
const selected = await clack10.select({
|
|
3036
3199
|
message: isMonorepo ? "Default file naming convention" : "File naming convention",
|
|
3037
3200
|
options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
|
|
3038
|
-
initialValue:
|
|
3201
|
+
initialValue: effective?.naming ?? SENTINEL_SKIP
|
|
3039
3202
|
});
|
|
3040
3203
|
if (isCancelled(selected)) return;
|
|
3041
3204
|
if (selected === SENTINEL_SKIP) {
|
|
@@ -3152,8 +3315,8 @@ async function handleAiContext(config) {
|
|
|
3152
3315
|
const rootPkg = getRootPackage(config.packages);
|
|
3153
3316
|
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3154
3317
|
while (true) {
|
|
3155
|
-
const ok =
|
|
3156
|
-
const unset =
|
|
3318
|
+
const ok = import_chalk13.default.green("\u2713");
|
|
3319
|
+
const unset = import_chalk13.default.dim("-");
|
|
3157
3320
|
const options = [
|
|
3158
3321
|
{
|
|
3159
3322
|
value: "componentNaming",
|
|
@@ -3235,155 +3398,6 @@ async function handleAiContext(config) {
|
|
|
3235
3398
|
}
|
|
3236
3399
|
}
|
|
3237
3400
|
|
|
3238
|
-
// src/utils/prompt-main-menu-hints.ts
|
|
3239
|
-
var import_chalk13 = __toESM(require("chalk"), 1);
|
|
3240
|
-
function fileLimitsHint(config) {
|
|
3241
|
-
const max = config.rules.maxFileLines;
|
|
3242
|
-
const test = config.rules.maxTestFileLines;
|
|
3243
|
-
return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
|
|
3244
|
-
}
|
|
3245
|
-
function fileNamingHint(config, scanResult) {
|
|
3246
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3247
|
-
const naming = rootPkg.conventions?.fileNaming;
|
|
3248
|
-
if (!config.rules.enforceNaming) return "not enforced";
|
|
3249
|
-
if (naming) {
|
|
3250
|
-
const detected = scanResult.packages.some(
|
|
3251
|
-
(p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
|
|
3252
|
-
);
|
|
3253
|
-
return detected ? `${naming} (detected)` : naming;
|
|
3254
|
-
}
|
|
3255
|
-
return "not set \u2014 select to configure";
|
|
3256
|
-
}
|
|
3257
|
-
function fileNamingStatus(config) {
|
|
3258
|
-
if (!config.rules.enforceNaming) return "unconfigured";
|
|
3259
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3260
|
-
return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
|
|
3261
|
-
}
|
|
3262
|
-
function missingTestsHint(config) {
|
|
3263
|
-
if (!config.rules.enforceMissingTests) return "not enforced";
|
|
3264
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3265
|
-
const pattern = rootPkg.structure?.testPattern;
|
|
3266
|
-
return pattern ? `enforced (${pattern})` : "enforced";
|
|
3267
|
-
}
|
|
3268
|
-
function coverageHint(config, hasTestRunner) {
|
|
3269
|
-
if (config.rules.testCoverage === 0) return "disabled";
|
|
3270
|
-
if (!hasTestRunner)
|
|
3271
|
-
return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
|
|
3272
|
-
const isMonorepo = config.packages.length > 1;
|
|
3273
|
-
if (isMonorepo) {
|
|
3274
|
-
const withCov = config.packages.filter(
|
|
3275
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
3276
|
-
);
|
|
3277
|
-
const exempt = config.packages.length - withCov.length;
|
|
3278
|
-
return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
|
|
3279
|
-
}
|
|
3280
|
-
return `${config.rules.testCoverage}%`;
|
|
3281
|
-
}
|
|
3282
|
-
function aiContextHint(config) {
|
|
3283
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3284
|
-
const count = [
|
|
3285
|
-
rootPkg.conventions?.componentNaming,
|
|
3286
|
-
rootPkg.conventions?.hookNaming,
|
|
3287
|
-
rootPkg.conventions?.importAlias
|
|
3288
|
-
].filter(Boolean).length;
|
|
3289
|
-
if (count === 3) return "all set";
|
|
3290
|
-
if (count > 0) return `${count} of 3 conventions`;
|
|
3291
|
-
return "none set \u2014 optional AI guidelines";
|
|
3292
|
-
}
|
|
3293
|
-
function aiContextStatus(config) {
|
|
3294
|
-
const rootPkg = getRootPackage(config.packages);
|
|
3295
|
-
const count = [
|
|
3296
|
-
rootPkg.conventions?.componentNaming,
|
|
3297
|
-
rootPkg.conventions?.hookNaming,
|
|
3298
|
-
rootPkg.conventions?.importAlias
|
|
3299
|
-
].filter(Boolean).length;
|
|
3300
|
-
if (count === 3) return "ok";
|
|
3301
|
-
if (count > 0) return "partial";
|
|
3302
|
-
return "unconfigured";
|
|
3303
|
-
}
|
|
3304
|
-
function packageOverridesHint(config) {
|
|
3305
|
-
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3306
|
-
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3307
|
-
const customized = editable.filter(
|
|
3308
|
-
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3309
|
-
).length;
|
|
3310
|
-
return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
|
|
3311
|
-
}
|
|
3312
|
-
function boundariesHint(config, state) {
|
|
3313
|
-
if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
|
|
3314
|
-
const deny = config.boundaries?.deny;
|
|
3315
|
-
if (!deny) return "enabled";
|
|
3316
|
-
const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
|
|
3317
|
-
const pkgCount = Object.keys(deny).length;
|
|
3318
|
-
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3319
|
-
}
|
|
3320
|
-
function packageOverridesStatus(config) {
|
|
3321
|
-
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3322
|
-
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3323
|
-
const customized = editable.some(
|
|
3324
|
-
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3325
|
-
);
|
|
3326
|
-
return customized ? "ok" : "unconfigured";
|
|
3327
|
-
}
|
|
3328
|
-
function statusIcon(status) {
|
|
3329
|
-
if (status === "ok") return import_chalk13.default.green("\u2713");
|
|
3330
|
-
if (status === "needs-input") return import_chalk13.default.yellow("?");
|
|
3331
|
-
if (status === "unconfigured") return import_chalk13.default.dim("-");
|
|
3332
|
-
return import_chalk13.default.yellow("~");
|
|
3333
|
-
}
|
|
3334
|
-
function buildMainMenuOptions(config, scanResult, state) {
|
|
3335
|
-
const namingStatus = fileNamingStatus(config);
|
|
3336
|
-
const coverageStatus = config.rules.testCoverage === 0 ? "unconfigured" : !state.hasTestRunner ? "partial" : "ok";
|
|
3337
|
-
const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "unconfigured";
|
|
3338
|
-
const options = [
|
|
3339
|
-
{
|
|
3340
|
-
value: "fileLimits",
|
|
3341
|
-
label: `${statusIcon("ok")} Max file size`,
|
|
3342
|
-
hint: fileLimitsHint(config)
|
|
3343
|
-
},
|
|
3344
|
-
{
|
|
3345
|
-
value: "fileNaming",
|
|
3346
|
-
label: `${statusIcon(namingStatus)} File naming`,
|
|
3347
|
-
hint: fileNamingHint(config, scanResult)
|
|
3348
|
-
},
|
|
3349
|
-
{
|
|
3350
|
-
value: "missingTests",
|
|
3351
|
-
label: `${statusIcon(missingTestsStatus)} Missing tests`,
|
|
3352
|
-
hint: missingTestsHint(config)
|
|
3353
|
-
},
|
|
3354
|
-
{
|
|
3355
|
-
value: "coverage",
|
|
3356
|
-
label: `${statusIcon(coverageStatus)} Coverage`,
|
|
3357
|
-
hint: coverageHint(config, state.hasTestRunner)
|
|
3358
|
-
},
|
|
3359
|
-
{
|
|
3360
|
-
value: "aiContext",
|
|
3361
|
-
label: `${statusIcon(aiContextStatus(config))} AI context`,
|
|
3362
|
-
hint: aiContextHint(config)
|
|
3363
|
-
}
|
|
3364
|
-
];
|
|
3365
|
-
if (config.packages.length > 1) {
|
|
3366
|
-
const bIcon = statusIcon(
|
|
3367
|
-
state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
|
|
3368
|
-
);
|
|
3369
|
-
const poIcon = statusIcon(packageOverridesStatus(config));
|
|
3370
|
-
options.push(
|
|
3371
|
-
{
|
|
3372
|
-
value: "packageOverrides",
|
|
3373
|
-
label: `${poIcon} Per-package overrides`,
|
|
3374
|
-
hint: packageOverridesHint(config)
|
|
3375
|
-
},
|
|
3376
|
-
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3377
|
-
);
|
|
3378
|
-
}
|
|
3379
|
-
options.push(
|
|
3380
|
-
{ value: "reset", label: " Reset all to defaults" },
|
|
3381
|
-
{ value: "review", label: " Review scan details", hint: "detected stack & conventions" },
|
|
3382
|
-
{ value: "done", label: " Done \u2014 write config" }
|
|
3383
|
-
);
|
|
3384
|
-
return options;
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
3401
|
// src/utils/prompt-main-menu.ts
|
|
3388
3402
|
async function promptMainMenu(config, scanResult, opts) {
|
|
3389
3403
|
const originalConfig = structuredClone(config);
|
|
@@ -4325,7 +4339,7 @@ ${import_chalk19.default.bold("Synced:")}`);
|
|
|
4325
4339
|
}
|
|
4326
4340
|
|
|
4327
4341
|
// src/index.ts
|
|
4328
|
-
var VERSION = "0.6.
|
|
4342
|
+
var VERSION = "0.6.13";
|
|
4329
4343
|
var program = new import_commander.Command();
|
|
4330
4344
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
4331
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) => {
|