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.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
VERSION: () => VERSION
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -61,19 +61,104 @@ function findProjectRoot(startDir) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// src/utils/prompt.ts
|
|
64
|
-
var
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
var clack = __toESM(require("@clack/prompts"), 1);
|
|
65
|
+
function assertNotCancelled(value) {
|
|
66
|
+
if (clack.isCancel(value)) {
|
|
67
|
+
clack.cancel("Setup cancelled.");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function confirm2(message) {
|
|
72
|
+
const result = await clack.confirm({ message, initialValue: true });
|
|
73
|
+
assertNotCancelled(result);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
async function confirmDangerous(message) {
|
|
77
|
+
const result = await clack.confirm({ message, initialValue: false });
|
|
78
|
+
assertNotCancelled(result);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
async function promptInitDecision() {
|
|
82
|
+
const result = await clack.select({
|
|
83
|
+
message: "Accept these settings?",
|
|
84
|
+
options: [
|
|
85
|
+
{ value: "accept", label: "Yes, looks good", hint: "recommended" },
|
|
86
|
+
{ value: "customize", label: "Let me customize" }
|
|
87
|
+
]
|
|
69
88
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
assertNotCancelled(result);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async function promptRuleCustomization(defaults) {
|
|
93
|
+
const maxFileLinesResult = await clack.text({
|
|
94
|
+
message: "Maximum lines per source file?",
|
|
95
|
+
placeholder: String(defaults.maxFileLines),
|
|
96
|
+
initialValue: String(defaults.maxFileLines),
|
|
97
|
+
validate: (v) => {
|
|
98
|
+
const n = Number.parseInt(v, 10);
|
|
99
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
assertNotCancelled(maxFileLinesResult);
|
|
103
|
+
const requireTestsResult = await clack.confirm({
|
|
104
|
+
message: "Require matching test files for source files?",
|
|
105
|
+
initialValue: defaults.requireTests
|
|
106
|
+
});
|
|
107
|
+
assertNotCancelled(requireTestsResult);
|
|
108
|
+
const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
|
|
109
|
+
const enforceNamingResult = await clack.confirm({
|
|
110
|
+
message: namingLabel,
|
|
111
|
+
initialValue: defaults.enforceNaming
|
|
112
|
+
});
|
|
113
|
+
assertNotCancelled(enforceNamingResult);
|
|
114
|
+
const enforcementResult = await clack.select({
|
|
115
|
+
message: "Enforcement mode",
|
|
116
|
+
options: [
|
|
117
|
+
{
|
|
118
|
+
value: "warn",
|
|
119
|
+
label: "warn",
|
|
120
|
+
hint: "show violations but don't block commits (recommended)"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
value: "enforce",
|
|
124
|
+
label: "enforce",
|
|
125
|
+
hint: "block commits with violations"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
initialValue: defaults.enforcement
|
|
129
|
+
});
|
|
130
|
+
assertNotCancelled(enforcementResult);
|
|
131
|
+
return {
|
|
132
|
+
maxFileLines: Number.parseInt(maxFileLinesResult, 10),
|
|
133
|
+
requireTests: requireTestsResult,
|
|
134
|
+
enforceNaming: enforceNamingResult,
|
|
135
|
+
enforcement: enforcementResult
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function promptIntegrations(hookManager) {
|
|
139
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
|
|
140
|
+
const result = await clack.multiselect({
|
|
141
|
+
message: "Set up integrations?",
|
|
142
|
+
options: [
|
|
143
|
+
{
|
|
144
|
+
value: "preCommit",
|
|
145
|
+
label: hookLabel,
|
|
146
|
+
hint: "runs checks when you commit"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
value: "claude",
|
|
150
|
+
label: "Claude Code hook",
|
|
151
|
+
hint: "checks files when Claude edits them"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
initialValues: ["preCommit", "claude"],
|
|
155
|
+
required: false
|
|
76
156
|
});
|
|
157
|
+
assertNotCancelled(result);
|
|
158
|
+
return {
|
|
159
|
+
preCommitHook: result.includes("preCommit"),
|
|
160
|
+
claudeCodeHook: result.includes("claude")
|
|
161
|
+
};
|
|
77
162
|
}
|
|
78
163
|
|
|
79
164
|
// src/utils/resolve-workspace-packages.ts
|
|
@@ -99,7 +184,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
99
184
|
];
|
|
100
185
|
packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
101
186
|
}
|
|
102
|
-
const packageNames = new Set(packages.map((
|
|
187
|
+
const packageNames = new Set(packages.map((p) => p.name));
|
|
103
188
|
for (const pkg of packages) {
|
|
104
189
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
105
190
|
}
|
|
@@ -129,37 +214,23 @@ async function boundariesCommand(options, cwd) {
|
|
|
129
214
|
}
|
|
130
215
|
displayRules(config);
|
|
131
216
|
}
|
|
132
|
-
function countBoundaries(boundaries) {
|
|
133
|
-
if (!boundaries) return 0;
|
|
134
|
-
if (Array.isArray(boundaries)) return boundaries.length;
|
|
135
|
-
return Object.values(boundaries).reduce((sum, denied) => sum + denied.length, 0);
|
|
136
|
-
}
|
|
137
217
|
function displayRules(config) {
|
|
138
|
-
|
|
139
|
-
if (total === 0) {
|
|
218
|
+
if (!config.boundaries || config.boundaries.length === 0) {
|
|
140
219
|
console.log(import_chalk.default.yellow("No boundary rules configured."));
|
|
141
220
|
console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
142
221
|
return;
|
|
143
222
|
}
|
|
223
|
+
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
224
|
+
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
144
225
|
console.log(`
|
|
145
|
-
${import_chalk.default.bold(`Boundary rules (${
|
|
226
|
+
${import_chalk.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
|
|
146
227
|
`);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
for (const r of denyRules) {
|
|
154
|
-
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
155
|
-
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
156
|
-
}
|
|
157
|
-
} else if (config.boundaries) {
|
|
158
|
-
for (const [from, denied] of Object.entries(config.boundaries)) {
|
|
159
|
-
for (const to of denied) {
|
|
160
|
-
console.log(` ${import_chalk.default.red("\u2717")} ${from} \u2192 ${to}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
228
|
+
for (const r of allowRules) {
|
|
229
|
+
console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
230
|
+
}
|
|
231
|
+
for (const r of denyRules) {
|
|
232
|
+
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
233
|
+
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
163
234
|
}
|
|
164
235
|
console.log(
|
|
165
236
|
`
|
|
@@ -176,29 +247,32 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
176
247
|
});
|
|
177
248
|
console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
178
249
|
const inferred = inferBoundaries(graph);
|
|
179
|
-
|
|
180
|
-
if (entries.length === 0) {
|
|
250
|
+
if (inferred.length === 0) {
|
|
181
251
|
console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
|
|
182
252
|
return;
|
|
183
253
|
}
|
|
184
|
-
const
|
|
254
|
+
const allow = inferred.filter((r) => r.allow);
|
|
255
|
+
const deny = inferred.filter((r) => !r.allow);
|
|
185
256
|
console.log(`
|
|
186
257
|
${import_chalk.default.bold("Inferred boundary rules:")}
|
|
187
258
|
`);
|
|
188
|
-
for (const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
259
|
+
for (const r of allow) {
|
|
260
|
+
console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
261
|
+
}
|
|
262
|
+
for (const r of deny) {
|
|
263
|
+
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
264
|
+
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
192
265
|
}
|
|
193
266
|
console.log(`
|
|
194
|
-
${
|
|
195
|
-
|
|
267
|
+
${allow.length} allowed, ${deny.length} denied`);
|
|
268
|
+
console.log("");
|
|
269
|
+
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
196
270
|
if (shouldSave) {
|
|
197
271
|
config.boundaries = inferred;
|
|
198
272
|
config.rules.enforceBoundaries = true;
|
|
199
273
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
200
274
|
`);
|
|
201
|
-
console.log(`${import_chalk.default.green("\u2713")} Saved ${
|
|
275
|
+
console.log(`${import_chalk.default.green("\u2713")} Saved ${inferred.length} rules`);
|
|
202
276
|
}
|
|
203
277
|
}
|
|
204
278
|
async function showGraph(projectRoot, config) {
|
|
@@ -268,6 +342,7 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
268
342
|
var import_node_child_process = require("child_process");
|
|
269
343
|
var fs4 = __toESM(require("fs"), 1);
|
|
270
344
|
var path4 = __toESM(require("path"), 1);
|
|
345
|
+
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
271
346
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
272
347
|
"node_modules",
|
|
273
348
|
".git",
|
|
@@ -303,25 +378,9 @@ var NAMING_PATTERNS = {
|
|
|
303
378
|
snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
|
|
304
379
|
};
|
|
305
380
|
function isIgnored(relPath, ignorePatterns) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (startsGlob && endsGlob) {
|
|
310
|
-
const middle = pattern.slice(3, -3);
|
|
311
|
-
if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
} else if (endsGlob) {
|
|
315
|
-
const prefix = pattern.slice(0, -3);
|
|
316
|
-
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
317
|
-
} else if (startsGlob) {
|
|
318
|
-
const suffix = pattern.slice(3);
|
|
319
|
-
if (relPath.endsWith(suffix) || relPath === suffix) return true;
|
|
320
|
-
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return false;
|
|
381
|
+
if (ignorePatterns.length === 0) return false;
|
|
382
|
+
const isMatch = (0, import_picomatch.default)(ignorePatterns, { dot: true });
|
|
383
|
+
return isMatch(relPath);
|
|
325
384
|
}
|
|
326
385
|
function countFileLines(filePath) {
|
|
327
386
|
try {
|
|
@@ -572,8 +631,7 @@ async function checkCommand(options, cwd) {
|
|
|
572
631
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
573
632
|
violations.push(...testViolations);
|
|
574
633
|
}
|
|
575
|
-
|
|
576
|
-
if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
|
|
634
|
+
if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
|
|
577
635
|
const startTime = Date.now();
|
|
578
636
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
579
637
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -596,6 +654,16 @@ async function checkCommand(options, cwd) {
|
|
|
596
654
|
const elapsed = Date.now() - startTime;
|
|
597
655
|
console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
598
656
|
}
|
|
657
|
+
if (options.format === "json") {
|
|
658
|
+
console.log(
|
|
659
|
+
JSON.stringify({
|
|
660
|
+
violations,
|
|
661
|
+
checkedFiles: filesToCheck.length,
|
|
662
|
+
enforcement: config.enforcement
|
|
663
|
+
})
|
|
664
|
+
);
|
|
665
|
+
return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
|
|
666
|
+
}
|
|
599
667
|
if (violations.length === 0) {
|
|
600
668
|
console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
601
669
|
return 0;
|
|
@@ -619,7 +687,6 @@ var import_chalk4 = __toESM(require("chalk"), 1);
|
|
|
619
687
|
|
|
620
688
|
// src/commands/fix-helpers.ts
|
|
621
689
|
var import_node_child_process2 = require("child_process");
|
|
622
|
-
var import_node_readline = require("readline");
|
|
623
690
|
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
624
691
|
function printPlan(renames, stubs) {
|
|
625
692
|
if (renames.length > 0) {
|
|
@@ -653,15 +720,6 @@ function getConventionValue(convention) {
|
|
|
653
720
|
}
|
|
654
721
|
return void 0;
|
|
655
722
|
}
|
|
656
|
-
function promptConfirm(question) {
|
|
657
|
-
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
658
|
-
return new Promise((resolve4) => {
|
|
659
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
660
|
-
rl.close();
|
|
661
|
-
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
723
|
|
|
666
724
|
// src/commands/fix-imports.ts
|
|
667
725
|
var path7 = __toESM(require("path"), 1);
|
|
@@ -695,7 +753,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
695
753
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
696
754
|
for (const sourceFile of project.getSourceFiles()) {
|
|
697
755
|
const filePath = sourceFile.getFilePath();
|
|
698
|
-
|
|
756
|
+
const segments = filePath.split(path7.sep);
|
|
757
|
+
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
699
758
|
const fileDir = path7.dirname(filePath);
|
|
700
759
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
701
760
|
const specifier = decl.getModuleSpecifierValue();
|
|
@@ -935,7 +994,7 @@ async function fixCommand(options, cwd) {
|
|
|
935
994
|
return 0;
|
|
936
995
|
}
|
|
937
996
|
if (!options.yes) {
|
|
938
|
-
const confirmed = await
|
|
997
|
+
const confirmed = await confirmDangerous("Apply these fixes?");
|
|
939
998
|
if (!confirmed) {
|
|
940
999
|
console.log("Aborted.");
|
|
941
1000
|
return 0;
|
|
@@ -978,10 +1037,422 @@ async function fixCommand(options, cwd) {
|
|
|
978
1037
|
// src/commands/init.ts
|
|
979
1038
|
var fs12 = __toESM(require("fs"), 1);
|
|
980
1039
|
var path13 = __toESM(require("path"), 1);
|
|
981
|
-
var
|
|
1040
|
+
var clack2 = __toESM(require("@clack/prompts"), 1);
|
|
982
1041
|
var import_config4 = require("@viberails/config");
|
|
983
1042
|
var import_scanner = require("@viberails/scanner");
|
|
984
|
-
var
|
|
1043
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
1044
|
+
|
|
1045
|
+
// src/display.ts
|
|
1046
|
+
var import_types3 = require("@viberails/types");
|
|
1047
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1048
|
+
|
|
1049
|
+
// src/display-helpers.ts
|
|
1050
|
+
var import_types = require("@viberails/types");
|
|
1051
|
+
function groupByRole(directories) {
|
|
1052
|
+
const map = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const dir of directories) {
|
|
1054
|
+
if (dir.role === "unknown") continue;
|
|
1055
|
+
const existing = map.get(dir.role);
|
|
1056
|
+
if (existing) {
|
|
1057
|
+
existing.dirs.push(dir);
|
|
1058
|
+
} else {
|
|
1059
|
+
map.set(dir.role, { dirs: [dir] });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const groups = [];
|
|
1063
|
+
for (const [role, { dirs }] of map) {
|
|
1064
|
+
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
1065
|
+
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1066
|
+
groups.push({
|
|
1067
|
+
role,
|
|
1068
|
+
label,
|
|
1069
|
+
dirCount: dirs.length,
|
|
1070
|
+
totalFiles,
|
|
1071
|
+
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return groups;
|
|
1075
|
+
}
|
|
1076
|
+
function formatSummary(stats, packageCount) {
|
|
1077
|
+
const parts = [];
|
|
1078
|
+
if (packageCount && packageCount > 1) {
|
|
1079
|
+
parts.push(`${packageCount} packages`);
|
|
1080
|
+
}
|
|
1081
|
+
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1082
|
+
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1083
|
+
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1084
|
+
return parts.join(" \xB7 ");
|
|
1085
|
+
}
|
|
1086
|
+
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
1087
|
+
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
1088
|
+
}
|
|
1089
|
+
function formatRoleGroup(group) {
|
|
1090
|
+
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1091
|
+
if (group.singlePath) {
|
|
1092
|
+
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1093
|
+
}
|
|
1094
|
+
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1095
|
+
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/display-monorepo.ts
|
|
1099
|
+
var import_types2 = require("@viberails/types");
|
|
1100
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1101
|
+
function formatPackageSummary(pkg) {
|
|
1102
|
+
const parts = [];
|
|
1103
|
+
if (pkg.stack.framework) {
|
|
1104
|
+
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
1105
|
+
}
|
|
1106
|
+
if (pkg.stack.styling) {
|
|
1107
|
+
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1108
|
+
}
|
|
1109
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1110
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1111
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1112
|
+
}
|
|
1113
|
+
function displayMonorepoResults(scanResult) {
|
|
1114
|
+
const { stack, packages } = scanResult;
|
|
1115
|
+
console.log(`
|
|
1116
|
+
${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1117
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1118
|
+
if (stack.packageManager) {
|
|
1119
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1120
|
+
}
|
|
1121
|
+
if (stack.linter) {
|
|
1122
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (stack.formatter) {
|
|
1125
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1126
|
+
}
|
|
1127
|
+
if (stack.testRunner) {
|
|
1128
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1129
|
+
}
|
|
1130
|
+
console.log("");
|
|
1131
|
+
for (const pkg of packages) {
|
|
1132
|
+
console.log(formatPackageSummary(pkg));
|
|
1133
|
+
}
|
|
1134
|
+
const packagesWithDirs = packages.filter(
|
|
1135
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1136
|
+
);
|
|
1137
|
+
if (packagesWithDirs.length > 0) {
|
|
1138
|
+
console.log(`
|
|
1139
|
+
${import_chalk5.default.bold("Structure:")}`);
|
|
1140
|
+
for (const pkg of packagesWithDirs) {
|
|
1141
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1142
|
+
if (groups.length === 0) continue;
|
|
1143
|
+
console.log(` ${pkg.relativePath}:`);
|
|
1144
|
+
for (const group of groups) {
|
|
1145
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
displayConventions(scanResult);
|
|
1150
|
+
displaySummarySection(scanResult);
|
|
1151
|
+
console.log("");
|
|
1152
|
+
}
|
|
1153
|
+
function formatPackageSummaryPlain(pkg) {
|
|
1154
|
+
const parts = [];
|
|
1155
|
+
if (pkg.stack.framework) {
|
|
1156
|
+
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
1157
|
+
}
|
|
1158
|
+
if (pkg.stack.styling) {
|
|
1159
|
+
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1160
|
+
}
|
|
1161
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1162
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1163
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1164
|
+
}
|
|
1165
|
+
function formatMonorepoResultsText(scanResult, config) {
|
|
1166
|
+
const lines = [];
|
|
1167
|
+
const { stack, packages } = scanResult;
|
|
1168
|
+
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1169
|
+
const sharedParts = [formatItem(stack.language)];
|
|
1170
|
+
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1171
|
+
if (stack.linter) sharedParts.push(formatItem(stack.linter));
|
|
1172
|
+
if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
|
|
1173
|
+
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1174
|
+
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1175
|
+
lines.push("");
|
|
1176
|
+
for (const pkg of packages) {
|
|
1177
|
+
lines.push(formatPackageSummaryPlain(pkg));
|
|
1178
|
+
}
|
|
1179
|
+
const packagesWithDirs = packages.filter(
|
|
1180
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1181
|
+
);
|
|
1182
|
+
if (packagesWithDirs.length > 0) {
|
|
1183
|
+
lines.push("");
|
|
1184
|
+
lines.push("Structure:");
|
|
1185
|
+
for (const pkg of packagesWithDirs) {
|
|
1186
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1187
|
+
if (groups.length === 0) continue;
|
|
1188
|
+
lines.push(` ${pkg.relativePath}:`);
|
|
1189
|
+
for (const group of groups) {
|
|
1190
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1195
|
+
const pkgCount = packages.length > 1 ? packages.length : void 0;
|
|
1196
|
+
lines.push("");
|
|
1197
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1198
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1199
|
+
if (ext) {
|
|
1200
|
+
lines.push(ext);
|
|
1201
|
+
}
|
|
1202
|
+
lines.push(...formatRulesText(config));
|
|
1203
|
+
return lines.join("\n");
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/display.ts
|
|
1207
|
+
var CONVENTION_LABELS = {
|
|
1208
|
+
fileNaming: "File naming",
|
|
1209
|
+
componentNaming: "Component naming",
|
|
1210
|
+
hookNaming: "Hook naming",
|
|
1211
|
+
importAlias: "Import alias"
|
|
1212
|
+
};
|
|
1213
|
+
function formatItem(item, nameMap) {
|
|
1214
|
+
const name = nameMap?.[item.name] ?? item.name;
|
|
1215
|
+
return item.version ? `${name} ${item.version}` : name;
|
|
1216
|
+
}
|
|
1217
|
+
function confidenceLabel(convention) {
|
|
1218
|
+
const pct = Math.round(convention.consistency);
|
|
1219
|
+
if (convention.confidence === "high") {
|
|
1220
|
+
return `${pct}% \u2014 high confidence, will enforce`;
|
|
1221
|
+
}
|
|
1222
|
+
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
1223
|
+
}
|
|
1224
|
+
function displayConventions(scanResult) {
|
|
1225
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1226
|
+
if (conventionEntries.length === 0) return;
|
|
1227
|
+
console.log(`
|
|
1228
|
+
${import_chalk6.default.bold("Conventions:")}`);
|
|
1229
|
+
for (const [key, convention] of conventionEntries) {
|
|
1230
|
+
if (convention.confidence === "low") continue;
|
|
1231
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1232
|
+
if (scanResult.packages.length > 1) {
|
|
1233
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1234
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1235
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1236
|
+
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1237
|
+
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1238
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1239
|
+
} else {
|
|
1240
|
+
console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
|
|
1241
|
+
for (const pv of pkgValues) {
|
|
1242
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1243
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} else {
|
|
1247
|
+
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1248
|
+
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1249
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function displaySummarySection(scanResult) {
|
|
1254
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1255
|
+
console.log(`
|
|
1256
|
+
${import_chalk6.default.bold("Summary:")}`);
|
|
1257
|
+
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1258
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1259
|
+
if (ext) {
|
|
1260
|
+
console.log(` ${ext}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function displayScanResults(scanResult) {
|
|
1264
|
+
if (scanResult.packages.length > 1) {
|
|
1265
|
+
displayMonorepoResults(scanResult);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const { stack } = scanResult;
|
|
1269
|
+
console.log(`
|
|
1270
|
+
${import_chalk6.default.bold("Detected:")}`);
|
|
1271
|
+
if (stack.framework) {
|
|
1272
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
1273
|
+
}
|
|
1274
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1275
|
+
if (stack.styling) {
|
|
1276
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
1277
|
+
}
|
|
1278
|
+
if (stack.backend) {
|
|
1279
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
1280
|
+
}
|
|
1281
|
+
if (stack.orm) {
|
|
1282
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
|
|
1283
|
+
}
|
|
1284
|
+
if (stack.linter) {
|
|
1285
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1286
|
+
}
|
|
1287
|
+
if (stack.formatter) {
|
|
1288
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1289
|
+
}
|
|
1290
|
+
if (stack.testRunner) {
|
|
1291
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1292
|
+
}
|
|
1293
|
+
if (stack.packageManager) {
|
|
1294
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1295
|
+
}
|
|
1296
|
+
if (stack.libraries.length > 0) {
|
|
1297
|
+
for (const lib of stack.libraries) {
|
|
1298
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1302
|
+
if (groups.length > 0) {
|
|
1303
|
+
console.log(`
|
|
1304
|
+
${import_chalk6.default.bold("Structure:")}`);
|
|
1305
|
+
for (const group of groups) {
|
|
1306
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
displayConventions(scanResult);
|
|
1310
|
+
displaySummarySection(scanResult);
|
|
1311
|
+
console.log("");
|
|
1312
|
+
}
|
|
1313
|
+
function getConventionStr(cv) {
|
|
1314
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1315
|
+
}
|
|
1316
|
+
function displayRulesPreview(config) {
|
|
1317
|
+
console.log(`${import_chalk6.default.bold("Rules:")}`);
|
|
1318
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1319
|
+
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1320
|
+
console.log(
|
|
1321
|
+
` ${import_chalk6.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
|
|
1322
|
+
);
|
|
1323
|
+
} else if (config.rules.requireTests) {
|
|
1324
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: yes`);
|
|
1325
|
+
} else {
|
|
1326
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Require test files: no`);
|
|
1327
|
+
}
|
|
1328
|
+
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1329
|
+
console.log(
|
|
1330
|
+
` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
|
|
1331
|
+
);
|
|
1332
|
+
} else {
|
|
1333
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
|
|
1334
|
+
}
|
|
1335
|
+
console.log(
|
|
1336
|
+
` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1337
|
+
);
|
|
1338
|
+
console.log("");
|
|
1339
|
+
if (config.enforcement === "enforce") {
|
|
1340
|
+
console.log(`${import_chalk6.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
|
|
1341
|
+
} else {
|
|
1342
|
+
console.log(
|
|
1343
|
+
`${import_chalk6.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
console.log("");
|
|
1347
|
+
}
|
|
1348
|
+
function plainConfidenceLabel(convention) {
|
|
1349
|
+
const pct = Math.round(convention.consistency);
|
|
1350
|
+
if (convention.confidence === "high") {
|
|
1351
|
+
return `${pct}%`;
|
|
1352
|
+
}
|
|
1353
|
+
return `${pct}%, suggested only`;
|
|
1354
|
+
}
|
|
1355
|
+
function formatConventionsText(scanResult) {
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1358
|
+
if (conventionEntries.length === 0) return lines;
|
|
1359
|
+
lines.push("");
|
|
1360
|
+
lines.push("Conventions:");
|
|
1361
|
+
for (const [key, convention] of conventionEntries) {
|
|
1362
|
+
if (convention.confidence === "low") continue;
|
|
1363
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1364
|
+
if (scanResult.packages.length > 1) {
|
|
1365
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1366
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1367
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1368
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1369
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1370
|
+
} else {
|
|
1371
|
+
lines.push(` ~ ${label}: varies by package`);
|
|
1372
|
+
for (const pv of pkgValues) {
|
|
1373
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1374
|
+
lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
} else {
|
|
1378
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1379
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return lines;
|
|
1383
|
+
}
|
|
1384
|
+
function formatRulesText(config) {
|
|
1385
|
+
const lines = [];
|
|
1386
|
+
lines.push("");
|
|
1387
|
+
lines.push("Rules:");
|
|
1388
|
+
lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
|
|
1389
|
+
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1390
|
+
lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
|
|
1391
|
+
} else if (config.rules.requireTests) {
|
|
1392
|
+
lines.push(" \u2022 Require test files: yes");
|
|
1393
|
+
} else {
|
|
1394
|
+
lines.push(" \u2022 Require test files: no");
|
|
1395
|
+
}
|
|
1396
|
+
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1397
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`);
|
|
1398
|
+
} else {
|
|
1399
|
+
lines.push(" \u2022 Enforce file naming: no");
|
|
1400
|
+
}
|
|
1401
|
+
lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
|
|
1402
|
+
return lines;
|
|
1403
|
+
}
|
|
1404
|
+
function formatScanResultsText(scanResult, config) {
|
|
1405
|
+
if (scanResult.packages.length > 1) {
|
|
1406
|
+
return formatMonorepoResultsText(scanResult, config);
|
|
1407
|
+
}
|
|
1408
|
+
const lines = [];
|
|
1409
|
+
const { stack } = scanResult;
|
|
1410
|
+
lines.push("Detected:");
|
|
1411
|
+
if (stack.framework) {
|
|
1412
|
+
lines.push(` \u2713 ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
1413
|
+
}
|
|
1414
|
+
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1415
|
+
if (stack.styling) {
|
|
1416
|
+
lines.push(` \u2713 ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
1417
|
+
}
|
|
1418
|
+
if (stack.backend) {
|
|
1419
|
+
lines.push(` \u2713 ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
1420
|
+
}
|
|
1421
|
+
if (stack.orm) {
|
|
1422
|
+
lines.push(` \u2713 ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
|
|
1423
|
+
}
|
|
1424
|
+
const secondaryParts = [];
|
|
1425
|
+
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
1426
|
+
if (stack.linter) secondaryParts.push(formatItem(stack.linter));
|
|
1427
|
+
if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
|
|
1428
|
+
if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
|
|
1429
|
+
if (secondaryParts.length > 0) {
|
|
1430
|
+
lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
|
|
1431
|
+
}
|
|
1432
|
+
if (stack.libraries.length > 0) {
|
|
1433
|
+
for (const lib of stack.libraries) {
|
|
1434
|
+
lines.push(` \u2713 ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1438
|
+
if (groups.length > 0) {
|
|
1439
|
+
lines.push("");
|
|
1440
|
+
lines.push("Structure:");
|
|
1441
|
+
for (const group of groups) {
|
|
1442
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1446
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1447
|
+
lines.push("");
|
|
1448
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1449
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1450
|
+
if (ext) {
|
|
1451
|
+
lines.push(ext);
|
|
1452
|
+
}
|
|
1453
|
+
lines.push(...formatRulesText(config));
|
|
1454
|
+
return lines.join("\n");
|
|
1455
|
+
}
|
|
985
1456
|
|
|
986
1457
|
// src/utils/write-generated-files.ts
|
|
987
1458
|
var fs10 = __toESM(require("fs"), 1);
|
|
@@ -1012,60 +1483,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1012
1483
|
// src/commands/init-hooks.ts
|
|
1013
1484
|
var fs11 = __toESM(require("fs"), 1);
|
|
1014
1485
|
var path12 = __toESM(require("path"), 1);
|
|
1015
|
-
var
|
|
1016
|
-
function setupClaudeCodeHook(projectRoot) {
|
|
1017
|
-
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1018
|
-
if (!fs11.existsSync(claudeDir)) {
|
|
1019
|
-
fs11.mkdirSync(claudeDir, { recursive: true });
|
|
1020
|
-
}
|
|
1021
|
-
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1022
|
-
let settings = {};
|
|
1023
|
-
if (fs11.existsSync(settingsPath)) {
|
|
1024
|
-
try {
|
|
1025
|
-
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
1026
|
-
} catch {
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
const hooks = settings.hooks ?? {};
|
|
1030
|
-
const postToolUse = hooks.PostToolUse;
|
|
1031
|
-
if (Array.isArray(postToolUse)) {
|
|
1032
|
-
const hasViberails = postToolUse.some(
|
|
1033
|
-
(entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.some(
|
|
1034
|
-
(h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes("viberails")
|
|
1035
|
-
)
|
|
1036
|
-
);
|
|
1037
|
-
if (hasViberails) return;
|
|
1038
|
-
}
|
|
1039
|
-
const viberailsHook = {
|
|
1040
|
-
matcher: "Edit|Write",
|
|
1041
|
-
hooks: [
|
|
1042
|
-
{
|
|
1043
|
-
type: "command",
|
|
1044
|
-
command: "jq -r '.tool_input.file_path' | xargs npx viberails check --files"
|
|
1045
|
-
}
|
|
1046
|
-
]
|
|
1047
|
-
};
|
|
1048
|
-
if (!hooks.PostToolUse) {
|
|
1049
|
-
hooks.PostToolUse = [viberailsHook];
|
|
1050
|
-
} else if (Array.isArray(hooks.PostToolUse)) {
|
|
1051
|
-
hooks.PostToolUse.push(viberailsHook);
|
|
1052
|
-
}
|
|
1053
|
-
settings.hooks = hooks;
|
|
1054
|
-
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1055
|
-
`);
|
|
1056
|
-
console.log(` ${import_chalk5.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1057
|
-
}
|
|
1486
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
1058
1487
|
function setupPreCommitHook(projectRoot) {
|
|
1059
1488
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1060
1489
|
if (fs11.existsSync(lefthookPath)) {
|
|
1061
1490
|
addLefthookPreCommit(lefthookPath);
|
|
1062
|
-
console.log(` ${
|
|
1491
|
+
console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1063
1492
|
return;
|
|
1064
1493
|
}
|
|
1065
1494
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1066
1495
|
if (fs11.existsSync(huskyDir)) {
|
|
1067
1496
|
writeHuskyPreCommit(huskyDir);
|
|
1068
|
-
console.log(` ${
|
|
1497
|
+
console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1069
1498
|
return;
|
|
1070
1499
|
}
|
|
1071
1500
|
const gitDir = path12.join(projectRoot, ".git");
|
|
@@ -1075,7 +1504,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1075
1504
|
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1076
1505
|
}
|
|
1077
1506
|
writeGitHookPreCommit(hooksDir);
|
|
1078
|
-
console.log(` ${
|
|
1507
|
+
console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
1079
1508
|
}
|
|
1080
1509
|
}
|
|
1081
1510
|
function writeGitHookPreCommit(hooksDir) {
|
|
@@ -1105,10 +1534,72 @@ npx viberails check --staged
|
|
|
1105
1534
|
function addLefthookPreCommit(lefthookPath) {
|
|
1106
1535
|
const content = fs11.readFileSync(lefthookPath, "utf-8");
|
|
1107
1536
|
if (content.includes("viberails")) return;
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1537
|
+
const hasPreCommit = /^pre-commit:/m.test(content);
|
|
1538
|
+
if (hasPreCommit) {
|
|
1539
|
+
const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
|
|
1540
|
+
"\n"
|
|
1541
|
+
);
|
|
1542
|
+
const updated = `${content.trimEnd()}
|
|
1543
|
+
${commandBlock}
|
|
1544
|
+
`;
|
|
1545
|
+
fs11.writeFileSync(lefthookPath, updated);
|
|
1546
|
+
} else {
|
|
1547
|
+
const section = [
|
|
1548
|
+
"",
|
|
1549
|
+
"pre-commit:",
|
|
1550
|
+
" commands:",
|
|
1551
|
+
" viberails:",
|
|
1552
|
+
" run: npx viberails check --staged"
|
|
1553
|
+
].join("\n");
|
|
1554
|
+
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1555
|
+
${section}
|
|
1556
|
+
`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function detectHookManager(projectRoot) {
|
|
1560
|
+
if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
1561
|
+
if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
|
|
1562
|
+
if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
|
|
1563
|
+
return void 0;
|
|
1564
|
+
}
|
|
1565
|
+
function setupClaudeCodeHook(projectRoot) {
|
|
1566
|
+
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1567
|
+
if (!fs11.existsSync(claudeDir)) {
|
|
1568
|
+
fs11.mkdirSync(claudeDir, { recursive: true });
|
|
1569
|
+
}
|
|
1570
|
+
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1571
|
+
let settings = {};
|
|
1572
|
+
if (fs11.existsSync(settingsPath)) {
|
|
1573
|
+
try {
|
|
1574
|
+
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
1575
|
+
} catch {
|
|
1576
|
+
console.warn(
|
|
1577
|
+
` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
|
|
1578
|
+
);
|
|
1579
|
+
settings = {};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
const hooks = settings.hooks ?? {};
|
|
1583
|
+
const existing = hooks.PostToolUse ?? [];
|
|
1584
|
+
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1585
|
+
const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
|
|
1586
|
+
const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
|
|
1587
|
+
hooks.PostToolUse = [
|
|
1588
|
+
...existing,
|
|
1589
|
+
{
|
|
1590
|
+
matcher: "Edit|Write",
|
|
1591
|
+
hooks: [
|
|
1592
|
+
{
|
|
1593
|
+
type: "command",
|
|
1594
|
+
command: hookCommand
|
|
1595
|
+
}
|
|
1596
|
+
]
|
|
1597
|
+
}
|
|
1598
|
+
];
|
|
1599
|
+
settings.hooks = hooks;
|
|
1600
|
+
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1111
1601
|
`);
|
|
1602
|
+
console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1112
1603
|
}
|
|
1113
1604
|
function writeHuskyPreCommit(huskyDir) {
|
|
1114
1605
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
@@ -1124,187 +1615,6 @@ npx viberails check --staged
|
|
|
1124
1615
|
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1125
1616
|
}
|
|
1126
1617
|
|
|
1127
|
-
// src/commands/init-wizard.ts
|
|
1128
|
-
var p = __toESM(require("@clack/prompts"), 1);
|
|
1129
|
-
var import_types4 = require("@viberails/types");
|
|
1130
|
-
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
1131
|
-
|
|
1132
|
-
// src/display.ts
|
|
1133
|
-
var import_types3 = require("@viberails/types");
|
|
1134
|
-
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
1135
|
-
|
|
1136
|
-
// src/display-helpers.ts
|
|
1137
|
-
var import_types = require("@viberails/types");
|
|
1138
|
-
function groupByRole(directories) {
|
|
1139
|
-
const map = /* @__PURE__ */ new Map();
|
|
1140
|
-
for (const dir of directories) {
|
|
1141
|
-
if (dir.role === "unknown") continue;
|
|
1142
|
-
const existing = map.get(dir.role);
|
|
1143
|
-
if (existing) {
|
|
1144
|
-
existing.dirs.push(dir);
|
|
1145
|
-
} else {
|
|
1146
|
-
map.set(dir.role, { dirs: [dir] });
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
const groups = [];
|
|
1150
|
-
for (const [role, { dirs }] of map) {
|
|
1151
|
-
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
1152
|
-
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1153
|
-
groups.push({
|
|
1154
|
-
role,
|
|
1155
|
-
label,
|
|
1156
|
-
dirCount: dirs.length,
|
|
1157
|
-
totalFiles,
|
|
1158
|
-
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
return groups;
|
|
1162
|
-
}
|
|
1163
|
-
function formatSummary(stats, packageCount) {
|
|
1164
|
-
const parts = [];
|
|
1165
|
-
if (packageCount && packageCount > 1) {
|
|
1166
|
-
parts.push(`${packageCount} packages`);
|
|
1167
|
-
}
|
|
1168
|
-
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1169
|
-
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1170
|
-
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1171
|
-
return parts.join(" \xB7 ");
|
|
1172
|
-
}
|
|
1173
|
-
function formatRoleGroup(group) {
|
|
1174
|
-
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1175
|
-
if (group.singlePath) {
|
|
1176
|
-
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1177
|
-
}
|
|
1178
|
-
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1179
|
-
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// src/display-monorepo.ts
|
|
1183
|
-
var import_types2 = require("@viberails/types");
|
|
1184
|
-
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1185
|
-
|
|
1186
|
-
// src/display.ts
|
|
1187
|
-
function formatItem(item, nameMap) {
|
|
1188
|
-
const name = nameMap?.[item.name] ?? item.name;
|
|
1189
|
-
return item.version ? `${name} ${item.version}` : name;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// src/commands/init-wizard.ts
|
|
1193
|
-
var DEFAULT_WIZARD_RESULT = {
|
|
1194
|
-
enforcement: "warn",
|
|
1195
|
-
checks: {
|
|
1196
|
-
fileSize: true,
|
|
1197
|
-
naming: true,
|
|
1198
|
-
tests: true,
|
|
1199
|
-
boundaries: false
|
|
1200
|
-
},
|
|
1201
|
-
integration: ["pre-commit"]
|
|
1202
|
-
};
|
|
1203
|
-
async function runWizard(scanResult) {
|
|
1204
|
-
const isMonorepo = scanResult.packages.length > 1;
|
|
1205
|
-
displayScanSummary(scanResult);
|
|
1206
|
-
const enforcement = await p.select({
|
|
1207
|
-
message: "How strict should viberails be?",
|
|
1208
|
-
initialValue: "warn",
|
|
1209
|
-
options: [
|
|
1210
|
-
{ value: "warn", label: "Warn", hint: "show issues, never block commits" },
|
|
1211
|
-
{ value: "enforce", label: "Enforce", hint: "block commits with violations" }
|
|
1212
|
-
]
|
|
1213
|
-
});
|
|
1214
|
-
if (p.isCancel(enforcement)) {
|
|
1215
|
-
p.cancel("Setup cancelled.");
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
const checkOptions = [
|
|
1219
|
-
{ value: "fileSize", label: "File size limit (300 lines)" },
|
|
1220
|
-
{ value: "naming", label: "File naming conventions" },
|
|
1221
|
-
{ value: "tests", label: "Missing test files" }
|
|
1222
|
-
];
|
|
1223
|
-
if (isMonorepo) {
|
|
1224
|
-
checkOptions.push({ value: "boundaries", label: "Import boundaries" });
|
|
1225
|
-
}
|
|
1226
|
-
const enabledChecks = await p.multiselect({
|
|
1227
|
-
message: "Which checks should viberails run?",
|
|
1228
|
-
options: checkOptions,
|
|
1229
|
-
initialValues: ["fileSize", "naming", "tests"],
|
|
1230
|
-
required: false
|
|
1231
|
-
});
|
|
1232
|
-
if (p.isCancel(enabledChecks)) {
|
|
1233
|
-
p.cancel("Setup cancelled.");
|
|
1234
|
-
return null;
|
|
1235
|
-
}
|
|
1236
|
-
const checks = {
|
|
1237
|
-
fileSize: enabledChecks.includes("fileSize"),
|
|
1238
|
-
naming: enabledChecks.includes("naming"),
|
|
1239
|
-
tests: enabledChecks.includes("tests"),
|
|
1240
|
-
boundaries: enabledChecks.includes("boundaries")
|
|
1241
|
-
};
|
|
1242
|
-
const integrationOptions = [
|
|
1243
|
-
{ value: "pre-commit", label: "Git pre-commit hook", hint: "runs on every commit" },
|
|
1244
|
-
{
|
|
1245
|
-
value: "claude-hook",
|
|
1246
|
-
label: "Claude Code hook",
|
|
1247
|
-
hint: "checks files as Claude edits them"
|
|
1248
|
-
},
|
|
1249
|
-
{ value: "context-only", label: "Context files only", hint: "no hooks" }
|
|
1250
|
-
];
|
|
1251
|
-
const integration = await p.multiselect({
|
|
1252
|
-
message: "Where should checks run?",
|
|
1253
|
-
options: integrationOptions,
|
|
1254
|
-
initialValues: ["pre-commit"],
|
|
1255
|
-
required: true
|
|
1256
|
-
});
|
|
1257
|
-
if (p.isCancel(integration)) {
|
|
1258
|
-
p.cancel("Setup cancelled.");
|
|
1259
|
-
return null;
|
|
1260
|
-
}
|
|
1261
|
-
const finalIntegration = integration.includes("context-only") ? ["context-only"] : integration;
|
|
1262
|
-
return {
|
|
1263
|
-
enforcement,
|
|
1264
|
-
checks,
|
|
1265
|
-
integration: finalIntegration
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
function displayScanSummary(scanResult) {
|
|
1269
|
-
const { stack } = scanResult;
|
|
1270
|
-
const parts = [];
|
|
1271
|
-
if (stack.framework) parts.push(formatItem(stack.framework, import_types4.FRAMEWORK_NAMES));
|
|
1272
|
-
parts.push(formatItem(stack.language));
|
|
1273
|
-
if (stack.styling) parts.push(formatItem(stack.styling, import_types4.STYLING_NAMES));
|
|
1274
|
-
if (stack.backend) parts.push(formatItem(stack.backend, import_types4.FRAMEWORK_NAMES));
|
|
1275
|
-
p.log.info(`${import_chalk8.default.bold("Stack:")} ${parts.join(", ")}`);
|
|
1276
|
-
if (stack.linter || stack.formatter || stack.testRunner || stack.packageManager) {
|
|
1277
|
-
const tools = [];
|
|
1278
|
-
if (stack.linter) tools.push(formatItem(stack.linter));
|
|
1279
|
-
if (stack.formatter && stack.formatter !== stack.linter)
|
|
1280
|
-
tools.push(formatItem(stack.formatter));
|
|
1281
|
-
if (stack.testRunner) tools.push(formatItem(stack.testRunner));
|
|
1282
|
-
if (stack.packageManager) tools.push(formatItem(stack.packageManager));
|
|
1283
|
-
p.log.info(`${import_chalk8.default.bold("Tools:")} ${tools.join(", ")}`);
|
|
1284
|
-
}
|
|
1285
|
-
if (stack.libraries.length > 0) {
|
|
1286
|
-
const libs = stack.libraries.map((lib) => formatItem(lib, import_types4.LIBRARY_NAMES)).join(", ");
|
|
1287
|
-
p.log.info(`${import_chalk8.default.bold("Libraries:")} ${libs}`);
|
|
1288
|
-
}
|
|
1289
|
-
const groups = groupByRole(scanResult.structure.directories);
|
|
1290
|
-
if (groups.length > 0) {
|
|
1291
|
-
const structParts = groups.map((g) => formatRoleGroup(g));
|
|
1292
|
-
p.log.info(`${import_chalk8.default.bold("Structure:")} ${structParts.join(", ")}`);
|
|
1293
|
-
}
|
|
1294
|
-
const conventionEntries = Object.entries(scanResult.conventions).filter(
|
|
1295
|
-
([, c]) => c.confidence !== "low"
|
|
1296
|
-
);
|
|
1297
|
-
if (conventionEntries.length > 0) {
|
|
1298
|
-
const convParts = conventionEntries.map(([, c]) => {
|
|
1299
|
-
const pct = Math.round(c.consistency);
|
|
1300
|
-
return `${c.value} (${pct}%)`;
|
|
1301
|
-
});
|
|
1302
|
-
p.log.info(`${import_chalk8.default.bold("Conventions:")} ${convParts.join(", ")}`);
|
|
1303
|
-
}
|
|
1304
|
-
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1305
|
-
p.log.info(`${import_chalk8.default.bold("Summary:")} ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
1618
|
// src/commands/init.ts
|
|
1309
1619
|
var CONFIG_FILE4 = "viberails.config.json";
|
|
1310
1620
|
function filterHighConfidence(conventions) {
|
|
@@ -1319,12 +1629,13 @@ function filterHighConfidence(conventions) {
|
|
|
1319
1629
|
}
|
|
1320
1630
|
return filtered;
|
|
1321
1631
|
}
|
|
1322
|
-
function
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
config.
|
|
1632
|
+
function getConventionStr2(cv) {
|
|
1633
|
+
if (!cv) return void 0;
|
|
1634
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1635
|
+
}
|
|
1636
|
+
function hasConventionOverrides(config) {
|
|
1637
|
+
if (!config.packages || config.packages.length === 0) return false;
|
|
1638
|
+
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1328
1639
|
}
|
|
1329
1640
|
async function initCommand(options, cwd) {
|
|
1330
1641
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1337,69 +1648,137 @@ async function initCommand(options, cwd) {
|
|
|
1337
1648
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1338
1649
|
if (fs12.existsSync(configPath)) {
|
|
1339
1650
|
console.log(
|
|
1340
|
-
|
|
1651
|
+
`${import_chalk8.default.yellow("!")} viberails is already initialized.
|
|
1652
|
+
Run ${import_chalk8.default.cyan("viberails sync")} to update, or delete viberails.config.json to start fresh.`
|
|
1341
1653
|
);
|
|
1342
1654
|
return;
|
|
1343
1655
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1656
|
+
if (options.yes) {
|
|
1657
|
+
console.log(import_chalk8.default.dim("Scanning project..."));
|
|
1658
|
+
const scanResult2 = await (0, import_scanner.scan)(projectRoot);
|
|
1659
|
+
const config2 = (0, import_config4.generateConfig)(scanResult2);
|
|
1660
|
+
config2.conventions = filterHighConfidence(config2.conventions);
|
|
1661
|
+
displayScanResults(scanResult2);
|
|
1662
|
+
displayRulesPreview(config2);
|
|
1663
|
+
if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
|
|
1664
|
+
console.log(import_chalk8.default.dim("Building import graph..."));
|
|
1665
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1666
|
+
const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
|
|
1667
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1668
|
+
packages,
|
|
1669
|
+
ignore: config2.ignore
|
|
1670
|
+
});
|
|
1671
|
+
const inferred = inferBoundaries(graph);
|
|
1672
|
+
if (inferred.length > 0) {
|
|
1673
|
+
config2.boundaries = inferred;
|
|
1674
|
+
config2.rules.enforceBoundaries = true;
|
|
1675
|
+
console.log(` Inferred ${inferred.length} boundary rules`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1679
|
+
`);
|
|
1680
|
+
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1681
|
+
updateGitignore(projectRoot);
|
|
1682
|
+
console.log(`
|
|
1683
|
+
Created:`);
|
|
1684
|
+
console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1685
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .viberails/context.md`);
|
|
1686
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .viberails/scan-result.json`);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
clack2.intro("viberails");
|
|
1690
|
+
const s = clack2.spinner();
|
|
1346
1691
|
s.start("Scanning project...");
|
|
1347
1692
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
1693
|
+
const config = (0, import_config4.generateConfig)(scanResult);
|
|
1348
1694
|
s.stop("Scan complete");
|
|
1349
1695
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
Run ${import_chalk9.default.cyan("viberails sync")} after adding source files.`
|
|
1696
|
+
clack2.log.warn(
|
|
1697
|
+
"No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
|
|
1353
1698
|
);
|
|
1354
1699
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1700
|
+
const resultsText = formatScanResultsText(scanResult, config);
|
|
1701
|
+
clack2.note(resultsText, "Scan results");
|
|
1702
|
+
const decision = await promptInitDecision();
|
|
1703
|
+
if (decision === "customize") {
|
|
1704
|
+
clack2.note(
|
|
1705
|
+
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1706
|
+
"Rules"
|
|
1707
|
+
);
|
|
1708
|
+
const overrides = await promptRuleCustomization({
|
|
1709
|
+
maxFileLines: config.rules.maxFileLines,
|
|
1710
|
+
requireTests: config.rules.requireTests,
|
|
1711
|
+
enforceNaming: config.rules.enforceNaming,
|
|
1712
|
+
enforcement: config.enforcement,
|
|
1713
|
+
fileNamingValue: getConventionStr2(config.conventions.fileNaming)
|
|
1714
|
+
});
|
|
1715
|
+
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1716
|
+
config.rules.requireTests = overrides.requireTests;
|
|
1717
|
+
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1718
|
+
config.enforcement = overrides.enforcement;
|
|
1719
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1720
|
+
clack2.note(
|
|
1721
|
+
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1722
|
+
"Per-package overrides"
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1362
1725
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1726
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1727
|
+
clack2.note(
|
|
1728
|
+
"Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
|
|
1729
|
+
"Boundaries"
|
|
1730
|
+
);
|
|
1731
|
+
const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
|
|
1732
|
+
if (shouldInfer) {
|
|
1733
|
+
const bs = clack2.spinner();
|
|
1734
|
+
bs.start("Building import graph...");
|
|
1735
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1736
|
+
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
1737
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1738
|
+
packages,
|
|
1739
|
+
ignore: config.ignore
|
|
1740
|
+
});
|
|
1741
|
+
const inferred = inferBoundaries(graph);
|
|
1742
|
+
if (inferred.length > 0) {
|
|
1743
|
+
config.boundaries = inferred;
|
|
1744
|
+
config.rules.enforceBoundaries = true;
|
|
1745
|
+
bs.stop(`Inferred ${inferred.length} boundary rules`);
|
|
1746
|
+
} else {
|
|
1747
|
+
bs.stop("No boundary rules inferred");
|
|
1748
|
+
}
|
|
1381
1749
|
}
|
|
1382
1750
|
}
|
|
1751
|
+
const hookManager = detectHookManager(projectRoot);
|
|
1752
|
+
const integrations = await promptIntegrations(hookManager);
|
|
1753
|
+
if (hasConventionOverrides(config)) {
|
|
1754
|
+
clack2.note(
|
|
1755
|
+
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1756
|
+
"Per-package conventions"
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1383
1759
|
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1384
1760
|
`);
|
|
1385
1761
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1386
1762
|
updateGitignore(projectRoot);
|
|
1387
|
-
|
|
1763
|
+
const createdFiles = [
|
|
1764
|
+
CONFIG_FILE4,
|
|
1765
|
+
".viberails/context.md",
|
|
1766
|
+
".viberails/scan-result.json"
|
|
1767
|
+
];
|
|
1768
|
+
if (integrations.preCommitHook) {
|
|
1388
1769
|
setupPreCommitHook(projectRoot);
|
|
1770
|
+
const hookMgr = detectHookManager(projectRoot);
|
|
1771
|
+
if (hookMgr) {
|
|
1772
|
+
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1773
|
+
}
|
|
1389
1774
|
}
|
|
1390
|
-
if (
|
|
1775
|
+
if (integrations.claudeCodeHook) {
|
|
1391
1776
|
setupClaudeCodeHook(projectRoot);
|
|
1777
|
+
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1392
1778
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
|
|
1397
|
-
p2.outro(
|
|
1398
|
-
`${import_chalk9.default.bold("Next steps:")}
|
|
1399
|
-
1. Review ${import_chalk9.default.cyan("viberails.config.json")} and adjust rules
|
|
1400
|
-
2. Commit ${import_chalk9.default.cyan("viberails.config.json")} and ${import_chalk9.default.cyan(".viberails/context.md")}
|
|
1401
|
-
3. Run ${import_chalk9.default.cyan("viberails check")} to verify your project passes`
|
|
1402
|
-
);
|
|
1779
|
+
clack2.log.success(`Created:
|
|
1780
|
+
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1781
|
+
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
1403
1782
|
}
|
|
1404
1783
|
function updateGitignore(projectRoot) {
|
|
1405
1784
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
@@ -1409,8 +1788,9 @@ function updateGitignore(projectRoot) {
|
|
|
1409
1788
|
}
|
|
1410
1789
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1411
1790
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1412
|
-
|
|
1413
|
-
|
|
1791
|
+
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1792
|
+
`;
|
|
1793
|
+
fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1414
1794
|
}
|
|
1415
1795
|
}
|
|
1416
1796
|
|
|
@@ -1419,7 +1799,7 @@ var fs13 = __toESM(require("fs"), 1);
|
|
|
1419
1799
|
var path14 = __toESM(require("path"), 1);
|
|
1420
1800
|
var import_config5 = require("@viberails/config");
|
|
1421
1801
|
var import_scanner2 = require("@viberails/scanner");
|
|
1422
|
-
var
|
|
1802
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
1423
1803
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1424
1804
|
async function syncCommand(cwd) {
|
|
1425
1805
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1431,21 +1811,33 @@ async function syncCommand(cwd) {
|
|
|
1431
1811
|
}
|
|
1432
1812
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1433
1813
|
const existing = await (0, import_config5.loadConfig)(configPath);
|
|
1434
|
-
console.log(
|
|
1814
|
+
console.log(import_chalk9.default.dim("Scanning project..."));
|
|
1435
1815
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
1436
1816
|
const merged = (0, import_config5.mergeConfig)(existing, scanResult);
|
|
1437
|
-
|
|
1817
|
+
const existingJson = JSON.stringify(existing, null, 2);
|
|
1818
|
+
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1819
|
+
const configChanged = existingJson !== mergedJson;
|
|
1820
|
+
if (configChanged) {
|
|
1821
|
+
console.log(
|
|
1822
|
+
` ${import_chalk9.default.yellow("!")} Config updated \u2014 review ${import_chalk9.default.cyan(CONFIG_FILE5)} for changes`
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
fs13.writeFileSync(configPath, `${mergedJson}
|
|
1438
1826
|
`);
|
|
1439
1827
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1440
1828
|
console.log(`
|
|
1441
|
-
${
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1829
|
+
${import_chalk9.default.bold("Synced:")}`);
|
|
1830
|
+
if (configChanged) {
|
|
1831
|
+
console.log(` ${import_chalk9.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
|
|
1832
|
+
} else {
|
|
1833
|
+
console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
|
|
1834
|
+
}
|
|
1835
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1836
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1445
1837
|
}
|
|
1446
1838
|
|
|
1447
1839
|
// src/index.ts
|
|
1448
|
-
var VERSION = "0.3.
|
|
1840
|
+
var VERSION = "0.3.2";
|
|
1449
1841
|
var program = new import_commander.Command();
|
|
1450
1842
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1451
1843
|
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) => {
|
|
@@ -1453,7 +1845,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
1453
1845
|
await initCommand(options);
|
|
1454
1846
|
} catch (err) {
|
|
1455
1847
|
const message = err instanceof Error ? err.message : String(err);
|
|
1456
|
-
console.error(`${
|
|
1848
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1457
1849
|
process.exit(1);
|
|
1458
1850
|
}
|
|
1459
1851
|
});
|
|
@@ -1462,21 +1854,22 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1462
1854
|
await syncCommand();
|
|
1463
1855
|
} catch (err) {
|
|
1464
1856
|
const message = err instanceof Error ? err.message : String(err);
|
|
1465
|
-
console.error(`${
|
|
1857
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1466
1858
|
process.exit(1);
|
|
1467
1859
|
}
|
|
1468
1860
|
});
|
|
1469
|
-
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(
|
|
1861
|
+
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(
|
|
1470
1862
|
async (options) => {
|
|
1471
1863
|
try {
|
|
1472
1864
|
const exitCode = await checkCommand({
|
|
1473
1865
|
...options,
|
|
1474
|
-
noBoundaries: options.boundaries === false
|
|
1866
|
+
noBoundaries: options.boundaries === false,
|
|
1867
|
+
format: options.format === "json" ? "json" : "text"
|
|
1475
1868
|
});
|
|
1476
1869
|
process.exit(exitCode);
|
|
1477
1870
|
} catch (err) {
|
|
1478
1871
|
const message = err instanceof Error ? err.message : String(err);
|
|
1479
|
-
console.error(`${
|
|
1872
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1480
1873
|
process.exit(1);
|
|
1481
1874
|
}
|
|
1482
1875
|
}
|
|
@@ -1487,7 +1880,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
1487
1880
|
process.exit(exitCode);
|
|
1488
1881
|
} catch (err) {
|
|
1489
1882
|
const message = err instanceof Error ? err.message : String(err);
|
|
1490
|
-
console.error(`${
|
|
1883
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1491
1884
|
process.exit(1);
|
|
1492
1885
|
}
|
|
1493
1886
|
});
|
|
@@ -1496,7 +1889,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
1496
1889
|
await boundariesCommand(options);
|
|
1497
1890
|
} catch (err) {
|
|
1498
1891
|
const message = err instanceof Error ? err.message : String(err);
|
|
1499
|
-
console.error(`${
|
|
1892
|
+
console.error(`${import_chalk10.default.red("Error:")} ${message}`);
|
|
1500
1893
|
process.exit(1);
|
|
1501
1894
|
}
|
|
1502
1895
|
});
|