viberails 0.3.0 → 0.3.2
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 +771 -378
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +771 -378
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk10 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -28,19 +28,104 @@ function findProjectRoot(startDir) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// src/utils/prompt.ts
|
|
31
|
-
import * as
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
import * as clack from "@clack/prompts";
|
|
32
|
+
function assertNotCancelled(value) {
|
|
33
|
+
if (clack.isCancel(value)) {
|
|
34
|
+
clack.cancel("Setup cancelled.");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function confirm2(message) {
|
|
39
|
+
const result = await clack.confirm({ message, initialValue: true });
|
|
40
|
+
assertNotCancelled(result);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
async function confirmDangerous(message) {
|
|
44
|
+
const result = await clack.confirm({ message, initialValue: false });
|
|
45
|
+
assertNotCancelled(result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
async function promptInitDecision() {
|
|
49
|
+
const result = await clack.select({
|
|
50
|
+
message: "Accept these settings?",
|
|
51
|
+
options: [
|
|
52
|
+
{ value: "accept", label: "Yes, looks good", hint: "recommended" },
|
|
53
|
+
{ value: "customize", label: "Let me customize" }
|
|
54
|
+
]
|
|
36
55
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
assertNotCancelled(result);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
async function promptRuleCustomization(defaults) {
|
|
60
|
+
const maxFileLinesResult = await clack.text({
|
|
61
|
+
message: "Maximum lines per source file?",
|
|
62
|
+
placeholder: String(defaults.maxFileLines),
|
|
63
|
+
initialValue: String(defaults.maxFileLines),
|
|
64
|
+
validate: (v) => {
|
|
65
|
+
const n = Number.parseInt(v, 10);
|
|
66
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
assertNotCancelled(maxFileLinesResult);
|
|
70
|
+
const requireTestsResult = await clack.confirm({
|
|
71
|
+
message: "Require matching test files for source files?",
|
|
72
|
+
initialValue: defaults.requireTests
|
|
73
|
+
});
|
|
74
|
+
assertNotCancelled(requireTestsResult);
|
|
75
|
+
const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
|
|
76
|
+
const enforceNamingResult = await clack.confirm({
|
|
77
|
+
message: namingLabel,
|
|
78
|
+
initialValue: defaults.enforceNaming
|
|
79
|
+
});
|
|
80
|
+
assertNotCancelled(enforceNamingResult);
|
|
81
|
+
const enforcementResult = await clack.select({
|
|
82
|
+
message: "Enforcement mode",
|
|
83
|
+
options: [
|
|
84
|
+
{
|
|
85
|
+
value: "warn",
|
|
86
|
+
label: "warn",
|
|
87
|
+
hint: "show violations but don't block commits (recommended)"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
value: "enforce",
|
|
91
|
+
label: "enforce",
|
|
92
|
+
hint: "block commits with violations"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
initialValue: defaults.enforcement
|
|
96
|
+
});
|
|
97
|
+
assertNotCancelled(enforcementResult);
|
|
98
|
+
return {
|
|
99
|
+
maxFileLines: Number.parseInt(maxFileLinesResult, 10),
|
|
100
|
+
requireTests: requireTestsResult,
|
|
101
|
+
enforceNaming: enforceNamingResult,
|
|
102
|
+
enforcement: enforcementResult
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function promptIntegrations(hookManager) {
|
|
106
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
|
|
107
|
+
const result = await clack.multiselect({
|
|
108
|
+
message: "Set up integrations?",
|
|
109
|
+
options: [
|
|
110
|
+
{
|
|
111
|
+
value: "preCommit",
|
|
112
|
+
label: hookLabel,
|
|
113
|
+
hint: "runs checks when you commit"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: "claude",
|
|
117
|
+
label: "Claude Code hook",
|
|
118
|
+
hint: "checks files when Claude edits them"
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
initialValues: ["preCommit", "claude"],
|
|
122
|
+
required: false
|
|
43
123
|
});
|
|
124
|
+
assertNotCancelled(result);
|
|
125
|
+
return {
|
|
126
|
+
preCommitHook: result.includes("preCommit"),
|
|
127
|
+
claudeCodeHook: result.includes("claude")
|
|
128
|
+
};
|
|
44
129
|
}
|
|
45
130
|
|
|
46
131
|
// src/utils/resolve-workspace-packages.ts
|
|
@@ -66,7 +151,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
66
151
|
];
|
|
67
152
|
packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
68
153
|
}
|
|
69
|
-
const packageNames = new Set(packages.map((
|
|
154
|
+
const packageNames = new Set(packages.map((p) => p.name));
|
|
70
155
|
for (const pkg of packages) {
|
|
71
156
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
72
157
|
}
|
|
@@ -96,37 +181,23 @@ async function boundariesCommand(options, cwd) {
|
|
|
96
181
|
}
|
|
97
182
|
displayRules(config);
|
|
98
183
|
}
|
|
99
|
-
function countBoundaries(boundaries) {
|
|
100
|
-
if (!boundaries) return 0;
|
|
101
|
-
if (Array.isArray(boundaries)) return boundaries.length;
|
|
102
|
-
return Object.values(boundaries).reduce((sum, denied) => sum + denied.length, 0);
|
|
103
|
-
}
|
|
104
184
|
function displayRules(config) {
|
|
105
|
-
|
|
106
|
-
if (total === 0) {
|
|
185
|
+
if (!config.boundaries || config.boundaries.length === 0) {
|
|
107
186
|
console.log(chalk.yellow("No boundary rules configured."));
|
|
108
187
|
console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
109
188
|
return;
|
|
110
189
|
}
|
|
190
|
+
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
191
|
+
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
111
192
|
console.log(`
|
|
112
|
-
${chalk.bold(`Boundary rules (${
|
|
193
|
+
${chalk.bold(`Boundary rules (${config.boundaries.length} rules):`)}
|
|
113
194
|
`);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
for (const r of denyRules) {
|
|
121
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
122
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
123
|
-
}
|
|
124
|
-
} else if (config.boundaries) {
|
|
125
|
-
for (const [from, denied] of Object.entries(config.boundaries)) {
|
|
126
|
-
for (const to of denied) {
|
|
127
|
-
console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
195
|
+
for (const r of allowRules) {
|
|
196
|
+
console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
197
|
+
}
|
|
198
|
+
for (const r of denyRules) {
|
|
199
|
+
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
200
|
+
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
130
201
|
}
|
|
131
202
|
console.log(
|
|
132
203
|
`
|
|
@@ -143,29 +214,32 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
143
214
|
});
|
|
144
215
|
console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
145
216
|
const inferred = inferBoundaries(graph);
|
|
146
|
-
|
|
147
|
-
if (entries.length === 0) {
|
|
217
|
+
if (inferred.length === 0) {
|
|
148
218
|
console.log(chalk.yellow("No boundary rules could be inferred."));
|
|
149
219
|
return;
|
|
150
220
|
}
|
|
151
|
-
const
|
|
221
|
+
const allow = inferred.filter((r) => r.allow);
|
|
222
|
+
const deny = inferred.filter((r) => !r.allow);
|
|
152
223
|
console.log(`
|
|
153
224
|
${chalk.bold("Inferred boundary rules:")}
|
|
154
225
|
`);
|
|
155
|
-
for (const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
226
|
+
for (const r of allow) {
|
|
227
|
+
console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
228
|
+
}
|
|
229
|
+
for (const r of deny) {
|
|
230
|
+
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
231
|
+
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
159
232
|
}
|
|
160
233
|
console.log(`
|
|
161
|
-
${
|
|
162
|
-
|
|
234
|
+
${allow.length} allowed, ${deny.length} denied`);
|
|
235
|
+
console.log("");
|
|
236
|
+
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
163
237
|
if (shouldSave) {
|
|
164
238
|
config.boundaries = inferred;
|
|
165
239
|
config.rules.enforceBoundaries = true;
|
|
166
240
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
167
241
|
`);
|
|
168
|
-
console.log(`${chalk.green("\u2713")} Saved ${
|
|
242
|
+
console.log(`${chalk.green("\u2713")} Saved ${inferred.length} rules`);
|
|
169
243
|
}
|
|
170
244
|
}
|
|
171
245
|
async function showGraph(projectRoot, config) {
|
|
@@ -235,6 +309,7 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
235
309
|
import { execSync } from "child_process";
|
|
236
310
|
import * as fs4 from "fs";
|
|
237
311
|
import * as path4 from "path";
|
|
312
|
+
import picomatch from "picomatch";
|
|
238
313
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
239
314
|
"node_modules",
|
|
240
315
|
".git",
|
|
@@ -270,25 +345,9 @@ var NAMING_PATTERNS = {
|
|
|
270
345
|
snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
|
|
271
346
|
};
|
|
272
347
|
function isIgnored(relPath, ignorePatterns) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (startsGlob && endsGlob) {
|
|
277
|
-
const middle = pattern.slice(3, -3);
|
|
278
|
-
if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
} else if (endsGlob) {
|
|
282
|
-
const prefix = pattern.slice(0, -3);
|
|
283
|
-
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
284
|
-
} else if (startsGlob) {
|
|
285
|
-
const suffix = pattern.slice(3);
|
|
286
|
-
if (relPath.endsWith(suffix) || relPath === suffix) return true;
|
|
287
|
-
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return false;
|
|
348
|
+
if (ignorePatterns.length === 0) return false;
|
|
349
|
+
const isMatch = picomatch(ignorePatterns, { dot: true });
|
|
350
|
+
return isMatch(relPath);
|
|
292
351
|
}
|
|
293
352
|
function countFileLines(filePath) {
|
|
294
353
|
try {
|
|
@@ -539,8 +598,7 @@ async function checkCommand(options, cwd) {
|
|
|
539
598
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
540
599
|
violations.push(...testViolations);
|
|
541
600
|
}
|
|
542
|
-
|
|
543
|
-
if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
|
|
601
|
+
if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
|
|
544
602
|
const startTime = Date.now();
|
|
545
603
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
546
604
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -563,6 +621,16 @@ async function checkCommand(options, cwd) {
|
|
|
563
621
|
const elapsed = Date.now() - startTime;
|
|
564
622
|
console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
565
623
|
}
|
|
624
|
+
if (options.format === "json") {
|
|
625
|
+
console.log(
|
|
626
|
+
JSON.stringify({
|
|
627
|
+
violations,
|
|
628
|
+
checkedFiles: filesToCheck.length,
|
|
629
|
+
enforcement: config.enforcement
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
|
|
633
|
+
}
|
|
566
634
|
if (violations.length === 0) {
|
|
567
635
|
console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
568
636
|
return 0;
|
|
@@ -586,7 +654,6 @@ import chalk4 from "chalk";
|
|
|
586
654
|
|
|
587
655
|
// src/commands/fix-helpers.ts
|
|
588
656
|
import { execSync as execSync2 } from "child_process";
|
|
589
|
-
import { createInterface as createInterface2 } from "readline";
|
|
590
657
|
import chalk3 from "chalk";
|
|
591
658
|
function printPlan(renames, stubs) {
|
|
592
659
|
if (renames.length > 0) {
|
|
@@ -620,15 +687,6 @@ function getConventionValue(convention) {
|
|
|
620
687
|
}
|
|
621
688
|
return void 0;
|
|
622
689
|
}
|
|
623
|
-
function promptConfirm(question) {
|
|
624
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
625
|
-
return new Promise((resolve4) => {
|
|
626
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
627
|
-
rl.close();
|
|
628
|
-
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
690
|
|
|
633
691
|
// src/commands/fix-imports.ts
|
|
634
692
|
import * as path7 from "path";
|
|
@@ -662,7 +720,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
662
720
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
663
721
|
for (const sourceFile of project.getSourceFiles()) {
|
|
664
722
|
const filePath = sourceFile.getFilePath();
|
|
665
|
-
|
|
723
|
+
const segments = filePath.split(path7.sep);
|
|
724
|
+
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
666
725
|
const fileDir = path7.dirname(filePath);
|
|
667
726
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
668
727
|
const specifier = decl.getModuleSpecifierValue();
|
|
@@ -902,7 +961,7 @@ async function fixCommand(options, cwd) {
|
|
|
902
961
|
return 0;
|
|
903
962
|
}
|
|
904
963
|
if (!options.yes) {
|
|
905
|
-
const confirmed = await
|
|
964
|
+
const confirmed = await confirmDangerous("Apply these fixes?");
|
|
906
965
|
if (!confirmed) {
|
|
907
966
|
console.log("Aborted.");
|
|
908
967
|
return 0;
|
|
@@ -945,10 +1004,422 @@ async function fixCommand(options, cwd) {
|
|
|
945
1004
|
// src/commands/init.ts
|
|
946
1005
|
import * as fs12 from "fs";
|
|
947
1006
|
import * as path13 from "path";
|
|
948
|
-
import * as
|
|
1007
|
+
import * as clack2 from "@clack/prompts";
|
|
949
1008
|
import { generateConfig } from "@viberails/config";
|
|
950
1009
|
import { scan } from "@viberails/scanner";
|
|
951
|
-
import
|
|
1010
|
+
import chalk8 from "chalk";
|
|
1011
|
+
|
|
1012
|
+
// src/display.ts
|
|
1013
|
+
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, ORM_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
|
|
1014
|
+
import chalk6 from "chalk";
|
|
1015
|
+
|
|
1016
|
+
// src/display-helpers.ts
|
|
1017
|
+
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
1018
|
+
function groupByRole(directories) {
|
|
1019
|
+
const map = /* @__PURE__ */ new Map();
|
|
1020
|
+
for (const dir of directories) {
|
|
1021
|
+
if (dir.role === "unknown") continue;
|
|
1022
|
+
const existing = map.get(dir.role);
|
|
1023
|
+
if (existing) {
|
|
1024
|
+
existing.dirs.push(dir);
|
|
1025
|
+
} else {
|
|
1026
|
+
map.set(dir.role, { dirs: [dir] });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const groups = [];
|
|
1030
|
+
for (const [role, { dirs }] of map) {
|
|
1031
|
+
const label = ROLE_DESCRIPTIONS[role] ?? role;
|
|
1032
|
+
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1033
|
+
groups.push({
|
|
1034
|
+
role,
|
|
1035
|
+
label,
|
|
1036
|
+
dirCount: dirs.length,
|
|
1037
|
+
totalFiles,
|
|
1038
|
+
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return groups;
|
|
1042
|
+
}
|
|
1043
|
+
function formatSummary(stats, packageCount) {
|
|
1044
|
+
const parts = [];
|
|
1045
|
+
if (packageCount && packageCount > 1) {
|
|
1046
|
+
parts.push(`${packageCount} packages`);
|
|
1047
|
+
}
|
|
1048
|
+
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1049
|
+
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1050
|
+
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1051
|
+
return parts.join(" \xB7 ");
|
|
1052
|
+
}
|
|
1053
|
+
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
1054
|
+
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
1055
|
+
}
|
|
1056
|
+
function formatRoleGroup(group) {
|
|
1057
|
+
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1058
|
+
if (group.singlePath) {
|
|
1059
|
+
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1060
|
+
}
|
|
1061
|
+
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1062
|
+
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/display-monorepo.ts
|
|
1066
|
+
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1067
|
+
import chalk5 from "chalk";
|
|
1068
|
+
function formatPackageSummary(pkg) {
|
|
1069
|
+
const parts = [];
|
|
1070
|
+
if (pkg.stack.framework) {
|
|
1071
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
|
|
1072
|
+
}
|
|
1073
|
+
if (pkg.stack.styling) {
|
|
1074
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
1075
|
+
}
|
|
1076
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1077
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1078
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1079
|
+
}
|
|
1080
|
+
function displayMonorepoResults(scanResult) {
|
|
1081
|
+
const { stack, packages } = scanResult;
|
|
1082
|
+
console.log(`
|
|
1083
|
+
${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1084
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1085
|
+
if (stack.packageManager) {
|
|
1086
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (stack.linter) {
|
|
1089
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1090
|
+
}
|
|
1091
|
+
if (stack.formatter) {
|
|
1092
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1093
|
+
}
|
|
1094
|
+
if (stack.testRunner) {
|
|
1095
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1096
|
+
}
|
|
1097
|
+
console.log("");
|
|
1098
|
+
for (const pkg of packages) {
|
|
1099
|
+
console.log(formatPackageSummary(pkg));
|
|
1100
|
+
}
|
|
1101
|
+
const packagesWithDirs = packages.filter(
|
|
1102
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1103
|
+
);
|
|
1104
|
+
if (packagesWithDirs.length > 0) {
|
|
1105
|
+
console.log(`
|
|
1106
|
+
${chalk5.bold("Structure:")}`);
|
|
1107
|
+
for (const pkg of packagesWithDirs) {
|
|
1108
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1109
|
+
if (groups.length === 0) continue;
|
|
1110
|
+
console.log(` ${pkg.relativePath}:`);
|
|
1111
|
+
for (const group of groups) {
|
|
1112
|
+
console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
displayConventions(scanResult);
|
|
1117
|
+
displaySummarySection(scanResult);
|
|
1118
|
+
console.log("");
|
|
1119
|
+
}
|
|
1120
|
+
function formatPackageSummaryPlain(pkg) {
|
|
1121
|
+
const parts = [];
|
|
1122
|
+
if (pkg.stack.framework) {
|
|
1123
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
|
|
1124
|
+
}
|
|
1125
|
+
if (pkg.stack.styling) {
|
|
1126
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
1127
|
+
}
|
|
1128
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1129
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1130
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1131
|
+
}
|
|
1132
|
+
function formatMonorepoResultsText(scanResult, config) {
|
|
1133
|
+
const lines = [];
|
|
1134
|
+
const { stack, packages } = scanResult;
|
|
1135
|
+
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1136
|
+
const sharedParts = [formatItem(stack.language)];
|
|
1137
|
+
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1138
|
+
if (stack.linter) sharedParts.push(formatItem(stack.linter));
|
|
1139
|
+
if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
|
|
1140
|
+
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1141
|
+
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1142
|
+
lines.push("");
|
|
1143
|
+
for (const pkg of packages) {
|
|
1144
|
+
lines.push(formatPackageSummaryPlain(pkg));
|
|
1145
|
+
}
|
|
1146
|
+
const packagesWithDirs = packages.filter(
|
|
1147
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1148
|
+
);
|
|
1149
|
+
if (packagesWithDirs.length > 0) {
|
|
1150
|
+
lines.push("");
|
|
1151
|
+
lines.push("Structure:");
|
|
1152
|
+
for (const pkg of packagesWithDirs) {
|
|
1153
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1154
|
+
if (groups.length === 0) continue;
|
|
1155
|
+
lines.push(` ${pkg.relativePath}:`);
|
|
1156
|
+
for (const group of groups) {
|
|
1157
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1162
|
+
const pkgCount = packages.length > 1 ? packages.length : void 0;
|
|
1163
|
+
lines.push("");
|
|
1164
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1165
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1166
|
+
if (ext) {
|
|
1167
|
+
lines.push(ext);
|
|
1168
|
+
}
|
|
1169
|
+
lines.push(...formatRulesText(config));
|
|
1170
|
+
return lines.join("\n");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/display.ts
|
|
1174
|
+
var CONVENTION_LABELS = {
|
|
1175
|
+
fileNaming: "File naming",
|
|
1176
|
+
componentNaming: "Component naming",
|
|
1177
|
+
hookNaming: "Hook naming",
|
|
1178
|
+
importAlias: "Import alias"
|
|
1179
|
+
};
|
|
1180
|
+
function formatItem(item, nameMap) {
|
|
1181
|
+
const name = nameMap?.[item.name] ?? item.name;
|
|
1182
|
+
return item.version ? `${name} ${item.version}` : name;
|
|
1183
|
+
}
|
|
1184
|
+
function confidenceLabel(convention) {
|
|
1185
|
+
const pct = Math.round(convention.consistency);
|
|
1186
|
+
if (convention.confidence === "high") {
|
|
1187
|
+
return `${pct}% \u2014 high confidence, will enforce`;
|
|
1188
|
+
}
|
|
1189
|
+
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
1190
|
+
}
|
|
1191
|
+
function displayConventions(scanResult) {
|
|
1192
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1193
|
+
if (conventionEntries.length === 0) return;
|
|
1194
|
+
console.log(`
|
|
1195
|
+
${chalk6.bold("Conventions:")}`);
|
|
1196
|
+
for (const [key, convention] of conventionEntries) {
|
|
1197
|
+
if (convention.confidence === "low") continue;
|
|
1198
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1199
|
+
if (scanResult.packages.length > 1) {
|
|
1200
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1201
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1202
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1203
|
+
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1204
|
+
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1205
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1206
|
+
} else {
|
|
1207
|
+
console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
|
|
1208
|
+
for (const pv of pkgValues) {
|
|
1209
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1210
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1215
|
+
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1216
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function displaySummarySection(scanResult) {
|
|
1221
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1222
|
+
console.log(`
|
|
1223
|
+
${chalk6.bold("Summary:")}`);
|
|
1224
|
+
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1225
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1226
|
+
if (ext) {
|
|
1227
|
+
console.log(` ${ext}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function displayScanResults(scanResult) {
|
|
1231
|
+
if (scanResult.packages.length > 1) {
|
|
1232
|
+
displayMonorepoResults(scanResult);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const { stack } = scanResult;
|
|
1236
|
+
console.log(`
|
|
1237
|
+
${chalk6.bold("Detected:")}`);
|
|
1238
|
+
if (stack.framework) {
|
|
1239
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
|
|
1240
|
+
}
|
|
1241
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1242
|
+
if (stack.styling) {
|
|
1243
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
|
|
1244
|
+
}
|
|
1245
|
+
if (stack.backend) {
|
|
1246
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (stack.orm) {
|
|
1249
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
|
|
1250
|
+
}
|
|
1251
|
+
if (stack.linter) {
|
|
1252
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (stack.formatter) {
|
|
1255
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1256
|
+
}
|
|
1257
|
+
if (stack.testRunner) {
|
|
1258
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (stack.packageManager) {
|
|
1261
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1262
|
+
}
|
|
1263
|
+
if (stack.libraries.length > 0) {
|
|
1264
|
+
for (const lib of stack.libraries) {
|
|
1265
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1269
|
+
if (groups.length > 0) {
|
|
1270
|
+
console.log(`
|
|
1271
|
+
${chalk6.bold("Structure:")}`);
|
|
1272
|
+
for (const group of groups) {
|
|
1273
|
+
console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
displayConventions(scanResult);
|
|
1277
|
+
displaySummarySection(scanResult);
|
|
1278
|
+
console.log("");
|
|
1279
|
+
}
|
|
1280
|
+
function getConventionStr(cv) {
|
|
1281
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1282
|
+
}
|
|
1283
|
+
function displayRulesPreview(config) {
|
|
1284
|
+
console.log(`${chalk6.bold("Rules:")}`);
|
|
1285
|
+
console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1286
|
+
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1287
|
+
console.log(
|
|
1288
|
+
` ${chalk6.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
|
|
1289
|
+
);
|
|
1290
|
+
} else if (config.rules.requireTests) {
|
|
1291
|
+
console.log(` ${chalk6.dim("\u2022")} Require test files: yes`);
|
|
1292
|
+
} else {
|
|
1293
|
+
console.log(` ${chalk6.dim("\u2022")} Require test files: no`);
|
|
1294
|
+
}
|
|
1295
|
+
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1296
|
+
console.log(
|
|
1297
|
+
` ${chalk6.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
|
|
1298
|
+
);
|
|
1299
|
+
} else {
|
|
1300
|
+
console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
|
|
1301
|
+
}
|
|
1302
|
+
console.log(
|
|
1303
|
+
` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1304
|
+
);
|
|
1305
|
+
console.log("");
|
|
1306
|
+
if (config.enforcement === "enforce") {
|
|
1307
|
+
console.log(`${chalk6.bold("Enforcement mode:")} enforce (violations will block commits)`);
|
|
1308
|
+
} else {
|
|
1309
|
+
console.log(
|
|
1310
|
+
`${chalk6.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
console.log("");
|
|
1314
|
+
}
|
|
1315
|
+
function plainConfidenceLabel(convention) {
|
|
1316
|
+
const pct = Math.round(convention.consistency);
|
|
1317
|
+
if (convention.confidence === "high") {
|
|
1318
|
+
return `${pct}%`;
|
|
1319
|
+
}
|
|
1320
|
+
return `${pct}%, suggested only`;
|
|
1321
|
+
}
|
|
1322
|
+
function formatConventionsText(scanResult) {
|
|
1323
|
+
const lines = [];
|
|
1324
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1325
|
+
if (conventionEntries.length === 0) return lines;
|
|
1326
|
+
lines.push("");
|
|
1327
|
+
lines.push("Conventions:");
|
|
1328
|
+
for (const [key, convention] of conventionEntries) {
|
|
1329
|
+
if (convention.confidence === "low") continue;
|
|
1330
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1331
|
+
if (scanResult.packages.length > 1) {
|
|
1332
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1333
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1334
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1335
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1336
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1337
|
+
} else {
|
|
1338
|
+
lines.push(` ~ ${label}: varies by package`);
|
|
1339
|
+
for (const pv of pkgValues) {
|
|
1340
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1341
|
+
lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} else {
|
|
1345
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1346
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return lines;
|
|
1350
|
+
}
|
|
1351
|
+
function formatRulesText(config) {
|
|
1352
|
+
const lines = [];
|
|
1353
|
+
lines.push("");
|
|
1354
|
+
lines.push("Rules:");
|
|
1355
|
+
lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
|
|
1356
|
+
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1357
|
+
lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
|
|
1358
|
+
} else if (config.rules.requireTests) {
|
|
1359
|
+
lines.push(" \u2022 Require test files: yes");
|
|
1360
|
+
} else {
|
|
1361
|
+
lines.push(" \u2022 Require test files: no");
|
|
1362
|
+
}
|
|
1363
|
+
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1364
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
|
|
1365
|
+
} else {
|
|
1366
|
+
lines.push(" \u2022 Enforce file naming: no");
|
|
1367
|
+
}
|
|
1368
|
+
lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
|
|
1369
|
+
return lines;
|
|
1370
|
+
}
|
|
1371
|
+
function formatScanResultsText(scanResult, config) {
|
|
1372
|
+
if (scanResult.packages.length > 1) {
|
|
1373
|
+
return formatMonorepoResultsText(scanResult, config);
|
|
1374
|
+
}
|
|
1375
|
+
const lines = [];
|
|
1376
|
+
const { stack } = scanResult;
|
|
1377
|
+
lines.push("Detected:");
|
|
1378
|
+
if (stack.framework) {
|
|
1379
|
+
lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
|
|
1380
|
+
}
|
|
1381
|
+
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1382
|
+
if (stack.styling) {
|
|
1383
|
+
lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES2)}`);
|
|
1384
|
+
}
|
|
1385
|
+
if (stack.backend) {
|
|
1386
|
+
lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
|
|
1387
|
+
}
|
|
1388
|
+
if (stack.orm) {
|
|
1389
|
+
lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES)}`);
|
|
1390
|
+
}
|
|
1391
|
+
const secondaryParts = [];
|
|
1392
|
+
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
1393
|
+
if (stack.linter) secondaryParts.push(formatItem(stack.linter));
|
|
1394
|
+
if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
|
|
1395
|
+
if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
|
|
1396
|
+
if (secondaryParts.length > 0) {
|
|
1397
|
+
lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
|
|
1398
|
+
}
|
|
1399
|
+
if (stack.libraries.length > 0) {
|
|
1400
|
+
for (const lib of stack.libraries) {
|
|
1401
|
+
lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1405
|
+
if (groups.length > 0) {
|
|
1406
|
+
lines.push("");
|
|
1407
|
+
lines.push("Structure:");
|
|
1408
|
+
for (const group of groups) {
|
|
1409
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1413
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1414
|
+
lines.push("");
|
|
1415
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1416
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1417
|
+
if (ext) {
|
|
1418
|
+
lines.push(ext);
|
|
1419
|
+
}
|
|
1420
|
+
lines.push(...formatRulesText(config));
|
|
1421
|
+
return lines.join("\n");
|
|
1422
|
+
}
|
|
952
1423
|
|
|
953
1424
|
// src/utils/write-generated-files.ts
|
|
954
1425
|
import * as fs10 from "fs";
|
|
@@ -979,60 +1450,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
979
1450
|
// src/commands/init-hooks.ts
|
|
980
1451
|
import * as fs11 from "fs";
|
|
981
1452
|
import * as path12 from "path";
|
|
982
|
-
import
|
|
983
|
-
function setupClaudeCodeHook(projectRoot) {
|
|
984
|
-
const claudeDir = path12.join(projectRoot, ".claude");
|
|
985
|
-
if (!fs11.existsSync(claudeDir)) {
|
|
986
|
-
fs11.mkdirSync(claudeDir, { recursive: true });
|
|
987
|
-
}
|
|
988
|
-
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
989
|
-
let settings = {};
|
|
990
|
-
if (fs11.existsSync(settingsPath)) {
|
|
991
|
-
try {
|
|
992
|
-
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
993
|
-
} catch {
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
const hooks = settings.hooks ?? {};
|
|
997
|
-
const postToolUse = hooks.PostToolUse;
|
|
998
|
-
if (Array.isArray(postToolUse)) {
|
|
999
|
-
const hasViberails = postToolUse.some(
|
|
1000
|
-
(entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.some(
|
|
1001
|
-
(h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes("viberails")
|
|
1002
|
-
)
|
|
1003
|
-
);
|
|
1004
|
-
if (hasViberails) return;
|
|
1005
|
-
}
|
|
1006
|
-
const viberailsHook = {
|
|
1007
|
-
matcher: "Edit|Write",
|
|
1008
|
-
hooks: [
|
|
1009
|
-
{
|
|
1010
|
-
type: "command",
|
|
1011
|
-
command: "jq -r '.tool_input.file_path' | xargs npx viberails check --files"
|
|
1012
|
-
}
|
|
1013
|
-
]
|
|
1014
|
-
};
|
|
1015
|
-
if (!hooks.PostToolUse) {
|
|
1016
|
-
hooks.PostToolUse = [viberailsHook];
|
|
1017
|
-
} else if (Array.isArray(hooks.PostToolUse)) {
|
|
1018
|
-
hooks.PostToolUse.push(viberailsHook);
|
|
1019
|
-
}
|
|
1020
|
-
settings.hooks = hooks;
|
|
1021
|
-
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1022
|
-
`);
|
|
1023
|
-
console.log(` ${chalk5.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1024
|
-
}
|
|
1453
|
+
import chalk7 from "chalk";
|
|
1025
1454
|
function setupPreCommitHook(projectRoot) {
|
|
1026
1455
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1027
1456
|
if (fs11.existsSync(lefthookPath)) {
|
|
1028
1457
|
addLefthookPreCommit(lefthookPath);
|
|
1029
|
-
console.log(` ${
|
|
1458
|
+
console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1030
1459
|
return;
|
|
1031
1460
|
}
|
|
1032
1461
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1033
1462
|
if (fs11.existsSync(huskyDir)) {
|
|
1034
1463
|
writeHuskyPreCommit(huskyDir);
|
|
1035
|
-
console.log(` ${
|
|
1464
|
+
console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1036
1465
|
return;
|
|
1037
1466
|
}
|
|
1038
1467
|
const gitDir = path12.join(projectRoot, ".git");
|
|
@@ -1042,7 +1471,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1042
1471
|
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1043
1472
|
}
|
|
1044
1473
|
writeGitHookPreCommit(hooksDir);
|
|
1045
|
-
console.log(` ${
|
|
1474
|
+
console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
|
|
1046
1475
|
}
|
|
1047
1476
|
}
|
|
1048
1477
|
function writeGitHookPreCommit(hooksDir) {
|
|
@@ -1072,10 +1501,72 @@ npx viberails check --staged
|
|
|
1072
1501
|
function addLefthookPreCommit(lefthookPath) {
|
|
1073
1502
|
const content = fs11.readFileSync(lefthookPath, "utf-8");
|
|
1074
1503
|
if (content.includes("viberails")) return;
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1504
|
+
const hasPreCommit = /^pre-commit:/m.test(content);
|
|
1505
|
+
if (hasPreCommit) {
|
|
1506
|
+
const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
|
|
1507
|
+
"\n"
|
|
1508
|
+
);
|
|
1509
|
+
const updated = `${content.trimEnd()}
|
|
1510
|
+
${commandBlock}
|
|
1511
|
+
`;
|
|
1512
|
+
fs11.writeFileSync(lefthookPath, updated);
|
|
1513
|
+
} else {
|
|
1514
|
+
const section = [
|
|
1515
|
+
"",
|
|
1516
|
+
"pre-commit:",
|
|
1517
|
+
" commands:",
|
|
1518
|
+
" viberails:",
|
|
1519
|
+
" run: npx viberails check --staged"
|
|
1520
|
+
].join("\n");
|
|
1521
|
+
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1522
|
+
${section}
|
|
1078
1523
|
`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function detectHookManager(projectRoot) {
|
|
1527
|
+
if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
1528
|
+
if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
|
|
1529
|
+
if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
|
|
1530
|
+
return void 0;
|
|
1531
|
+
}
|
|
1532
|
+
function setupClaudeCodeHook(projectRoot) {
|
|
1533
|
+
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1534
|
+
if (!fs11.existsSync(claudeDir)) {
|
|
1535
|
+
fs11.mkdirSync(claudeDir, { recursive: true });
|
|
1536
|
+
}
|
|
1537
|
+
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1538
|
+
let settings = {};
|
|
1539
|
+
if (fs11.existsSync(settingsPath)) {
|
|
1540
|
+
try {
|
|
1541
|
+
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
1542
|
+
} catch {
|
|
1543
|
+
console.warn(
|
|
1544
|
+
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
|
|
1545
|
+
);
|
|
1546
|
+
settings = {};
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const hooks = settings.hooks ?? {};
|
|
1550
|
+
const existing = hooks.PostToolUse ?? [];
|
|
1551
|
+
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1552
|
+
const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
|
|
1553
|
+
const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
|
|
1554
|
+
hooks.PostToolUse = [
|
|
1555
|
+
...existing,
|
|
1556
|
+
{
|
|
1557
|
+
matcher: "Edit|Write",
|
|
1558
|
+
hooks: [
|
|
1559
|
+
{
|
|
1560
|
+
type: "command",
|
|
1561
|
+
command: hookCommand
|
|
1562
|
+
}
|
|
1563
|
+
]
|
|
1564
|
+
}
|
|
1565
|
+
];
|
|
1566
|
+
settings.hooks = hooks;
|
|
1567
|
+
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1568
|
+
`);
|
|
1569
|
+
console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1079
1570
|
}
|
|
1080
1571
|
function writeHuskyPreCommit(huskyDir) {
|
|
1081
1572
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
@@ -1091,187 +1582,6 @@ npx viberails check --staged
|
|
|
1091
1582
|
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1092
1583
|
}
|
|
1093
1584
|
|
|
1094
|
-
// src/commands/init-wizard.ts
|
|
1095
|
-
import * as p from "@clack/prompts";
|
|
1096
|
-
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES3, LIBRARY_NAMES as LIBRARY_NAMES2, STYLING_NAMES as STYLING_NAMES3 } from "@viberails/types";
|
|
1097
|
-
import chalk8 from "chalk";
|
|
1098
|
-
|
|
1099
|
-
// src/display.ts
|
|
1100
|
-
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
|
|
1101
|
-
import chalk7 from "chalk";
|
|
1102
|
-
|
|
1103
|
-
// src/display-helpers.ts
|
|
1104
|
-
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
1105
|
-
function groupByRole(directories) {
|
|
1106
|
-
const map = /* @__PURE__ */ new Map();
|
|
1107
|
-
for (const dir of directories) {
|
|
1108
|
-
if (dir.role === "unknown") continue;
|
|
1109
|
-
const existing = map.get(dir.role);
|
|
1110
|
-
if (existing) {
|
|
1111
|
-
existing.dirs.push(dir);
|
|
1112
|
-
} else {
|
|
1113
|
-
map.set(dir.role, { dirs: [dir] });
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
const groups = [];
|
|
1117
|
-
for (const [role, { dirs }] of map) {
|
|
1118
|
-
const label = ROLE_DESCRIPTIONS[role] ?? role;
|
|
1119
|
-
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1120
|
-
groups.push({
|
|
1121
|
-
role,
|
|
1122
|
-
label,
|
|
1123
|
-
dirCount: dirs.length,
|
|
1124
|
-
totalFiles,
|
|
1125
|
-
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
return groups;
|
|
1129
|
-
}
|
|
1130
|
-
function formatSummary(stats, packageCount) {
|
|
1131
|
-
const parts = [];
|
|
1132
|
-
if (packageCount && packageCount > 1) {
|
|
1133
|
-
parts.push(`${packageCount} packages`);
|
|
1134
|
-
}
|
|
1135
|
-
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1136
|
-
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1137
|
-
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1138
|
-
return parts.join(" \xB7 ");
|
|
1139
|
-
}
|
|
1140
|
-
function formatRoleGroup(group) {
|
|
1141
|
-
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1142
|
-
if (group.singlePath) {
|
|
1143
|
-
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1144
|
-
}
|
|
1145
|
-
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1146
|
-
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
// src/display-monorepo.ts
|
|
1150
|
-
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1151
|
-
import chalk6 from "chalk";
|
|
1152
|
-
|
|
1153
|
-
// src/display.ts
|
|
1154
|
-
function formatItem(item, nameMap) {
|
|
1155
|
-
const name = nameMap?.[item.name] ?? item.name;
|
|
1156
|
-
return item.version ? `${name} ${item.version}` : name;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// src/commands/init-wizard.ts
|
|
1160
|
-
var DEFAULT_WIZARD_RESULT = {
|
|
1161
|
-
enforcement: "warn",
|
|
1162
|
-
checks: {
|
|
1163
|
-
fileSize: true,
|
|
1164
|
-
naming: true,
|
|
1165
|
-
tests: true,
|
|
1166
|
-
boundaries: false
|
|
1167
|
-
},
|
|
1168
|
-
integration: ["pre-commit"]
|
|
1169
|
-
};
|
|
1170
|
-
async function runWizard(scanResult) {
|
|
1171
|
-
const isMonorepo = scanResult.packages.length > 1;
|
|
1172
|
-
displayScanSummary(scanResult);
|
|
1173
|
-
const enforcement = await p.select({
|
|
1174
|
-
message: "How strict should viberails be?",
|
|
1175
|
-
initialValue: "warn",
|
|
1176
|
-
options: [
|
|
1177
|
-
{ value: "warn", label: "Warn", hint: "show issues, never block commits" },
|
|
1178
|
-
{ value: "enforce", label: "Enforce", hint: "block commits with violations" }
|
|
1179
|
-
]
|
|
1180
|
-
});
|
|
1181
|
-
if (p.isCancel(enforcement)) {
|
|
1182
|
-
p.cancel("Setup cancelled.");
|
|
1183
|
-
return null;
|
|
1184
|
-
}
|
|
1185
|
-
const checkOptions = [
|
|
1186
|
-
{ value: "fileSize", label: "File size limit (300 lines)" },
|
|
1187
|
-
{ value: "naming", label: "File naming conventions" },
|
|
1188
|
-
{ value: "tests", label: "Missing test files" }
|
|
1189
|
-
];
|
|
1190
|
-
if (isMonorepo) {
|
|
1191
|
-
checkOptions.push({ value: "boundaries", label: "Import boundaries" });
|
|
1192
|
-
}
|
|
1193
|
-
const enabledChecks = await p.multiselect({
|
|
1194
|
-
message: "Which checks should viberails run?",
|
|
1195
|
-
options: checkOptions,
|
|
1196
|
-
initialValues: ["fileSize", "naming", "tests"],
|
|
1197
|
-
required: false
|
|
1198
|
-
});
|
|
1199
|
-
if (p.isCancel(enabledChecks)) {
|
|
1200
|
-
p.cancel("Setup cancelled.");
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
const checks = {
|
|
1204
|
-
fileSize: enabledChecks.includes("fileSize"),
|
|
1205
|
-
naming: enabledChecks.includes("naming"),
|
|
1206
|
-
tests: enabledChecks.includes("tests"),
|
|
1207
|
-
boundaries: enabledChecks.includes("boundaries")
|
|
1208
|
-
};
|
|
1209
|
-
const integrationOptions = [
|
|
1210
|
-
{ value: "pre-commit", label: "Git pre-commit hook", hint: "runs on every commit" },
|
|
1211
|
-
{
|
|
1212
|
-
value: "claude-hook",
|
|
1213
|
-
label: "Claude Code hook",
|
|
1214
|
-
hint: "checks files as Claude edits them"
|
|
1215
|
-
},
|
|
1216
|
-
{ value: "context-only", label: "Context files only", hint: "no hooks" }
|
|
1217
|
-
];
|
|
1218
|
-
const integration = await p.multiselect({
|
|
1219
|
-
message: "Where should checks run?",
|
|
1220
|
-
options: integrationOptions,
|
|
1221
|
-
initialValues: ["pre-commit"],
|
|
1222
|
-
required: true
|
|
1223
|
-
});
|
|
1224
|
-
if (p.isCancel(integration)) {
|
|
1225
|
-
p.cancel("Setup cancelled.");
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
const finalIntegration = integration.includes("context-only") ? ["context-only"] : integration;
|
|
1229
|
-
return {
|
|
1230
|
-
enforcement,
|
|
1231
|
-
checks,
|
|
1232
|
-
integration: finalIntegration
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
function displayScanSummary(scanResult) {
|
|
1236
|
-
const { stack } = scanResult;
|
|
1237
|
-
const parts = [];
|
|
1238
|
-
if (stack.framework) parts.push(formatItem(stack.framework, FRAMEWORK_NAMES3));
|
|
1239
|
-
parts.push(formatItem(stack.language));
|
|
1240
|
-
if (stack.styling) parts.push(formatItem(stack.styling, STYLING_NAMES3));
|
|
1241
|
-
if (stack.backend) parts.push(formatItem(stack.backend, FRAMEWORK_NAMES3));
|
|
1242
|
-
p.log.info(`${chalk8.bold("Stack:")} ${parts.join(", ")}`);
|
|
1243
|
-
if (stack.linter || stack.formatter || stack.testRunner || stack.packageManager) {
|
|
1244
|
-
const tools = [];
|
|
1245
|
-
if (stack.linter) tools.push(formatItem(stack.linter));
|
|
1246
|
-
if (stack.formatter && stack.formatter !== stack.linter)
|
|
1247
|
-
tools.push(formatItem(stack.formatter));
|
|
1248
|
-
if (stack.testRunner) tools.push(formatItem(stack.testRunner));
|
|
1249
|
-
if (stack.packageManager) tools.push(formatItem(stack.packageManager));
|
|
1250
|
-
p.log.info(`${chalk8.bold("Tools:")} ${tools.join(", ")}`);
|
|
1251
|
-
}
|
|
1252
|
-
if (stack.libraries.length > 0) {
|
|
1253
|
-
const libs = stack.libraries.map((lib) => formatItem(lib, LIBRARY_NAMES2)).join(", ");
|
|
1254
|
-
p.log.info(`${chalk8.bold("Libraries:")} ${libs}`);
|
|
1255
|
-
}
|
|
1256
|
-
const groups = groupByRole(scanResult.structure.directories);
|
|
1257
|
-
if (groups.length > 0) {
|
|
1258
|
-
const structParts = groups.map((g) => formatRoleGroup(g));
|
|
1259
|
-
p.log.info(`${chalk8.bold("Structure:")} ${structParts.join(", ")}`);
|
|
1260
|
-
}
|
|
1261
|
-
const conventionEntries = Object.entries(scanResult.conventions).filter(
|
|
1262
|
-
([, c]) => c.confidence !== "low"
|
|
1263
|
-
);
|
|
1264
|
-
if (conventionEntries.length > 0) {
|
|
1265
|
-
const convParts = conventionEntries.map(([, c]) => {
|
|
1266
|
-
const pct = Math.round(c.consistency);
|
|
1267
|
-
return `${c.value} (${pct}%)`;
|
|
1268
|
-
});
|
|
1269
|
-
p.log.info(`${chalk8.bold("Conventions:")} ${convParts.join(", ")}`);
|
|
1270
|
-
}
|
|
1271
|
-
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1272
|
-
p.log.info(`${chalk8.bold("Summary:")} ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
1585
|
// src/commands/init.ts
|
|
1276
1586
|
var CONFIG_FILE4 = "viberails.config.json";
|
|
1277
1587
|
function filterHighConfidence(conventions) {
|
|
@@ -1286,12 +1596,13 @@ function filterHighConfidence(conventions) {
|
|
|
1286
1596
|
}
|
|
1287
1597
|
return filtered;
|
|
1288
1598
|
}
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
config.
|
|
1599
|
+
function getConventionStr2(cv) {
|
|
1600
|
+
if (!cv) return void 0;
|
|
1601
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1602
|
+
}
|
|
1603
|
+
function hasConventionOverrides(config) {
|
|
1604
|
+
if (!config.packages || config.packages.length === 0) return false;
|
|
1605
|
+
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1295
1606
|
}
|
|
1296
1607
|
async function initCommand(options, cwd) {
|
|
1297
1608
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1304,69 +1615,137 @@ async function initCommand(options, cwd) {
|
|
|
1304
1615
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1305
1616
|
if (fs12.existsSync(configPath)) {
|
|
1306
1617
|
console.log(
|
|
1307
|
-
|
|
1618
|
+
`${chalk8.yellow("!")} viberails is already initialized.
|
|
1619
|
+
Run ${chalk8.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
|
|
1308
1620
|
);
|
|
1309
1621
|
return;
|
|
1310
1622
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1623
|
+
if (options.yes) {
|
|
1624
|
+
console.log(chalk8.dim("Scanning project..."));
|
|
1625
|
+
const scanResult2 = await scan(projectRoot);
|
|
1626
|
+
const config2 = generateConfig(scanResult2);
|
|
1627
|
+
config2.conventions = filterHighConfidence(config2.conventions);
|
|
1628
|
+
displayScanResults(scanResult2);
|
|
1629
|
+
displayRulesPreview(config2);
|
|
1630
|
+
if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
|
|
1631
|
+
console.log(chalk8.dim("Building import graph..."));
|
|
1632
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1633
|
+
const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
|
|
1634
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1635
|
+
packages,
|
|
1636
|
+
ignore: config2.ignore
|
|
1637
|
+
});
|
|
1638
|
+
const inferred = inferBoundaries(graph);
|
|
1639
|
+
if (inferred.length > 0) {
|
|
1640
|
+
config2.boundaries = inferred;
|
|
1641
|
+
config2.rules.enforceBoundaries = true;
|
|
1642
|
+
console.log(` Inferred ${inferred.length} boundary rules`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1646
|
+
`);
|
|
1647
|
+
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1648
|
+
updateGitignore(projectRoot);
|
|
1649
|
+
console.log(`
|
|
1650
|
+
Created:`);
|
|
1651
|
+
console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1652
|
+
console.log(` ${chalk8.green("\u2713")} .viberails/context.md`);
|
|
1653
|
+
console.log(` ${chalk8.green("\u2713")} .viberails/scan-result.json`);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
clack2.intro("viberails");
|
|
1657
|
+
const s = clack2.spinner();
|
|
1313
1658
|
s.start("Scanning project...");
|
|
1314
1659
|
const scanResult = await scan(projectRoot);
|
|
1660
|
+
const config = generateConfig(scanResult);
|
|
1315
1661
|
s.stop("Scan complete");
|
|
1316
1662
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
Run ${chalk9.cyan("viberails sync")} after adding source files.`
|
|
1663
|
+
clack2.log.warn(
|
|
1664
|
+
"No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
|
|
1320
1665
|
);
|
|
1321
1666
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1667
|
+
const resultsText = formatScanResultsText(scanResult, config);
|
|
1668
|
+
clack2.note(resultsText, "Scan results");
|
|
1669
|
+
const decision = await promptInitDecision();
|
|
1670
|
+
if (decision === "customize") {
|
|
1671
|
+
clack2.note(
|
|
1672
|
+
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1673
|
+
"Rules"
|
|
1674
|
+
);
|
|
1675
|
+
const overrides = await promptRuleCustomization({
|
|
1676
|
+
maxFileLines: config.rules.maxFileLines,
|
|
1677
|
+
requireTests: config.rules.requireTests,
|
|
1678
|
+
enforceNaming: config.rules.enforceNaming,
|
|
1679
|
+
enforcement: config.enforcement,
|
|
1680
|
+
fileNamingValue: getConventionStr2(config.conventions.fileNaming)
|
|
1681
|
+
});
|
|
1682
|
+
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1683
|
+
config.rules.requireTests = overrides.requireTests;
|
|
1684
|
+
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1685
|
+
config.enforcement = overrides.enforcement;
|
|
1686
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1687
|
+
clack2.note(
|
|
1688
|
+
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1689
|
+
"Per-package overrides"
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1329
1692
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1693
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1694
|
+
clack2.note(
|
|
1695
|
+
"Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
|
|
1696
|
+
"Boundaries"
|
|
1697
|
+
);
|
|
1698
|
+
const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
|
|
1699
|
+
if (shouldInfer) {
|
|
1700
|
+
const bs = clack2.spinner();
|
|
1701
|
+
bs.start("Building import graph...");
|
|
1702
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1703
|
+
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
1704
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1705
|
+
packages,
|
|
1706
|
+
ignore: config.ignore
|
|
1707
|
+
});
|
|
1708
|
+
const inferred = inferBoundaries(graph);
|
|
1709
|
+
if (inferred.length > 0) {
|
|
1710
|
+
config.boundaries = inferred;
|
|
1711
|
+
config.rules.enforceBoundaries = true;
|
|
1712
|
+
bs.stop(`Inferred ${inferred.length} boundary rules`);
|
|
1713
|
+
} else {
|
|
1714
|
+
bs.stop("No boundary rules inferred");
|
|
1715
|
+
}
|
|
1348
1716
|
}
|
|
1349
1717
|
}
|
|
1718
|
+
const hookManager = detectHookManager(projectRoot);
|
|
1719
|
+
const integrations = await promptIntegrations(hookManager);
|
|
1720
|
+
if (hasConventionOverrides(config)) {
|
|
1721
|
+
clack2.note(
|
|
1722
|
+
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1723
|
+
"Per-package conventions"
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1350
1726
|
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1351
1727
|
`);
|
|
1352
1728
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1353
1729
|
updateGitignore(projectRoot);
|
|
1354
|
-
|
|
1730
|
+
const createdFiles = [
|
|
1731
|
+
CONFIG_FILE4,
|
|
1732
|
+
".viberails/context.md",
|
|
1733
|
+
".viberails/scan-result.json"
|
|
1734
|
+
];
|
|
1735
|
+
if (integrations.preCommitHook) {
|
|
1355
1736
|
setupPreCommitHook(projectRoot);
|
|
1737
|
+
const hookMgr = detectHookManager(projectRoot);
|
|
1738
|
+
if (hookMgr) {
|
|
1739
|
+
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1740
|
+
}
|
|
1356
1741
|
}
|
|
1357
|
-
if (
|
|
1742
|
+
if (integrations.claudeCodeHook) {
|
|
1358
1743
|
setupClaudeCodeHook(projectRoot);
|
|
1744
|
+
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1359
1745
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
|
|
1364
|
-
p2.outro(
|
|
1365
|
-
`${chalk9.bold("Next steps:")}
|
|
1366
|
-
1. Review ${chalk9.cyan("viberails.config.json")} and adjust rules
|
|
1367
|
-
2. Commit ${chalk9.cyan("viberails.config.json")} and ${chalk9.cyan(".viberails/context.md")}
|
|
1368
|
-
3. Run ${chalk9.cyan("viberails check")} to verify your project passes`
|
|
1369
|
-
);
|
|
1746
|
+
clack2.log.success(`Created:
|
|
1747
|
+
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1748
|
+
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
1370
1749
|
}
|
|
1371
1750
|
function updateGitignore(projectRoot) {
|
|
1372
1751
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
@@ -1376,8 +1755,9 @@ function updateGitignore(projectRoot) {
|
|
|
1376
1755
|
}
|
|
1377
1756
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1378
1757
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1379
|
-
|
|
1380
|
-
|
|
1758
|
+
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1759
|
+
`;
|
|
1760
|
+
fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1381
1761
|
}
|
|
1382
1762
|
}
|
|
1383
1763
|
|
|
@@ -1386,7 +1766,7 @@ import * as fs13 from "fs";
|
|
|
1386
1766
|
import * as path14 from "path";
|
|
1387
1767
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1388
1768
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1389
|
-
import
|
|
1769
|
+
import chalk9 from "chalk";
|
|
1390
1770
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1391
1771
|
async function syncCommand(cwd) {
|
|
1392
1772
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1398,21 +1778,33 @@ async function syncCommand(cwd) {
|
|
|
1398
1778
|
}
|
|
1399
1779
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1400
1780
|
const existing = await loadConfig4(configPath);
|
|
1401
|
-
console.log(
|
|
1781
|
+
console.log(chalk9.dim("Scanning project..."));
|
|
1402
1782
|
const scanResult = await scan2(projectRoot);
|
|
1403
1783
|
const merged = mergeConfig(existing, scanResult);
|
|
1404
|
-
|
|
1784
|
+
const existingJson = JSON.stringify(existing, null, 2);
|
|
1785
|
+
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1786
|
+
const configChanged = existingJson !== mergedJson;
|
|
1787
|
+
if (configChanged) {
|
|
1788
|
+
console.log(
|
|
1789
|
+
` ${chalk9.yellow("!")} Config updated \u2014 review ${chalk9.cyan(CONFIG_FILE5)} for changes`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
fs13.writeFileSync(configPath, `${mergedJson}
|
|
1405
1793
|
`);
|
|
1406
1794
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1407
1795
|
console.log(`
|
|
1408
|
-
${
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1796
|
+
${chalk9.bold("Synced:")}`);
|
|
1797
|
+
if (configChanged) {
|
|
1798
|
+
console.log(` ${chalk9.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
|
|
1799
|
+
} else {
|
|
1800
|
+
console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
|
|
1801
|
+
}
|
|
1802
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1803
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1412
1804
|
}
|
|
1413
1805
|
|
|
1414
1806
|
// src/index.ts
|
|
1415
|
-
var VERSION = "0.3.
|
|
1807
|
+
var VERSION = "0.3.2";
|
|
1416
1808
|
var program = new Command();
|
|
1417
1809
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1418
1810
|
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)").action(async (options) => {
|
|
@@ -1420,7 +1812,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
1420
1812
|
await initCommand(options);
|
|
1421
1813
|
} catch (err) {
|
|
1422
1814
|
const message = err instanceof Error ? err.message : String(err);
|
|
1423
|
-
console.error(`${
|
|
1815
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1424
1816
|
process.exit(1);
|
|
1425
1817
|
}
|
|
1426
1818
|
});
|
|
@@ -1429,21 +1821,22 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1429
1821
|
await syncCommand();
|
|
1430
1822
|
} catch (err) {
|
|
1431
1823
|
const message = err instanceof Error ? err.message : String(err);
|
|
1432
|
-
console.error(`${
|
|
1824
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1433
1825
|
process.exit(1);
|
|
1434
1826
|
}
|
|
1435
1827
|
});
|
|
1436
|
-
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).action(
|
|
1828
|
+
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").action(
|
|
1437
1829
|
async (options) => {
|
|
1438
1830
|
try {
|
|
1439
1831
|
const exitCode = await checkCommand({
|
|
1440
1832
|
...options,
|
|
1441
|
-
noBoundaries: options.boundaries === false
|
|
1833
|
+
noBoundaries: options.boundaries === false,
|
|
1834
|
+
format: options.format === "json" ? "json" : "text"
|
|
1442
1835
|
});
|
|
1443
1836
|
process.exit(exitCode);
|
|
1444
1837
|
} catch (err) {
|
|
1445
1838
|
const message = err instanceof Error ? err.message : String(err);
|
|
1446
|
-
console.error(`${
|
|
1839
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1447
1840
|
process.exit(1);
|
|
1448
1841
|
}
|
|
1449
1842
|
}
|
|
@@ -1454,7 +1847,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
1454
1847
|
process.exit(exitCode);
|
|
1455
1848
|
} catch (err) {
|
|
1456
1849
|
const message = err instanceof Error ? err.message : String(err);
|
|
1457
|
-
console.error(`${
|
|
1850
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1458
1851
|
process.exit(1);
|
|
1459
1852
|
}
|
|
1460
1853
|
});
|
|
@@ -1463,7 +1856,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
1463
1856
|
await boundariesCommand(options);
|
|
1464
1857
|
} catch (err) {
|
|
1465
1858
|
const message = err instanceof Error ? err.message : String(err);
|
|
1466
|
-
console.error(`${
|
|
1859
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1467
1860
|
process.exit(1);
|
|
1468
1861
|
}
|
|
1469
1862
|
});
|