viberails 0.2.2 → 0.3.0
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 +414 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +414 -323
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
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_chalk11 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -99,7 +99,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
99
99
|
];
|
|
100
100
|
packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
101
101
|
}
|
|
102
|
-
const packageNames = new Set(packages.map((
|
|
102
|
+
const packageNames = new Set(packages.map((p3) => p3.name));
|
|
103
103
|
for (const pkg of packages) {
|
|
104
104
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
105
105
|
}
|
|
@@ -129,23 +129,37 @@ async function boundariesCommand(options, cwd) {
|
|
|
129
129
|
}
|
|
130
130
|
displayRules(config);
|
|
131
131
|
}
|
|
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
|
+
}
|
|
132
137
|
function displayRules(config) {
|
|
133
|
-
|
|
138
|
+
const total = countBoundaries(config.boundaries);
|
|
139
|
+
if (total === 0) {
|
|
134
140
|
console.log(import_chalk.default.yellow("No boundary rules configured."));
|
|
135
141
|
console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
138
|
-
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
139
|
-
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
140
144
|
console.log(`
|
|
141
|
-
${import_chalk.default.bold(`Boundary rules (${
|
|
145
|
+
${import_chalk.default.bold(`Boundary rules (${total} rules):`)}
|
|
142
146
|
`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
if (Array.isArray(config.boundaries)) {
|
|
148
|
+
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
149
|
+
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
150
|
+
for (const r of allowRules) {
|
|
151
|
+
console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
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
|
+
}
|
|
149
163
|
}
|
|
150
164
|
console.log(
|
|
151
165
|
`
|
|
@@ -162,31 +176,29 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
162
176
|
});
|
|
163
177
|
console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
164
178
|
const inferred = inferBoundaries(graph);
|
|
165
|
-
|
|
179
|
+
const entries = Object.entries(inferred);
|
|
180
|
+
if (entries.length === 0) {
|
|
166
181
|
console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
|
|
167
182
|
return;
|
|
168
183
|
}
|
|
169
|
-
const
|
|
170
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
184
|
+
const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
|
|
171
185
|
console.log(`
|
|
172
186
|
${import_chalk.default.bold("Inferred boundary rules:")}
|
|
173
187
|
`);
|
|
174
|
-
for (const
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
179
|
-
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
188
|
+
for (const [from, denied] of entries) {
|
|
189
|
+
for (const to of denied) {
|
|
190
|
+
console.log(` ${import_chalk.default.red("\u2717")} ${from} \u2192 ${to}`);
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
console.log(`
|
|
182
|
-
${
|
|
194
|
+
${totalRules} deny rules`);
|
|
183
195
|
const shouldSave = await confirm("\nSave to viberails.config.json?");
|
|
184
196
|
if (shouldSave) {
|
|
185
197
|
config.boundaries = inferred;
|
|
186
198
|
config.rules.enforceBoundaries = true;
|
|
187
199
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
188
200
|
`);
|
|
189
|
-
console.log(`${import_chalk.default.green("\u2713")} Saved ${
|
|
201
|
+
console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
|
|
190
202
|
}
|
|
191
203
|
}
|
|
192
204
|
async function showGraph(projectRoot, config) {
|
|
@@ -267,6 +279,10 @@ var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
267
279
|
".svelte-kit",
|
|
268
280
|
".turbo",
|
|
269
281
|
"coverage",
|
|
282
|
+
"public",
|
|
283
|
+
"vendor",
|
|
284
|
+
"__generated__",
|
|
285
|
+
"generated",
|
|
270
286
|
".viberails"
|
|
271
287
|
]);
|
|
272
288
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
@@ -288,12 +304,19 @@ var NAMING_PATTERNS = {
|
|
|
288
304
|
};
|
|
289
305
|
function isIgnored(relPath, ignorePatterns) {
|
|
290
306
|
for (const pattern of ignorePatterns) {
|
|
291
|
-
|
|
307
|
+
const startsGlob = pattern.startsWith("**/");
|
|
308
|
+
const endsGlob = pattern.endsWith("/**");
|
|
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) {
|
|
292
315
|
const prefix = pattern.slice(0, -3);
|
|
293
316
|
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
294
|
-
} else if (
|
|
317
|
+
} else if (startsGlob) {
|
|
295
318
|
const suffix = pattern.slice(3);
|
|
296
|
-
if (relPath.endsWith(suffix)) return true;
|
|
319
|
+
if (relPath.endsWith(suffix) || relPath === suffix) return true;
|
|
297
320
|
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
298
321
|
return true;
|
|
299
322
|
}
|
|
@@ -413,13 +436,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
413
436
|
const testSuffix = testPattern.replace("*", "");
|
|
414
437
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
415
438
|
for (const relFile of sourceFiles) {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
439
|
+
const basename7 = path5.basename(relFile);
|
|
440
|
+
if (basename7.includes(".test.") || basename7.includes(".spec.") || basename7.startsWith("index.") || basename7.endsWith(".d.ts")) {
|
|
418
441
|
continue;
|
|
419
442
|
}
|
|
420
|
-
const ext = path5.extname(
|
|
443
|
+
const ext = path5.extname(basename7);
|
|
421
444
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
422
|
-
const stem =
|
|
445
|
+
const stem = basename7.slice(0, basename7.indexOf("."));
|
|
423
446
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
424
447
|
const dir = path5.dirname(path5.join(projectRoot, relFile));
|
|
425
448
|
const colocatedTest = path5.join(dir, expectedTestFile);
|
|
@@ -440,6 +463,50 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
440
463
|
|
|
441
464
|
// src/commands/check.ts
|
|
442
465
|
var CONFIG_FILE2 = "viberails.config.json";
|
|
466
|
+
function isTestFile(relPath) {
|
|
467
|
+
const filename = path6.basename(relPath);
|
|
468
|
+
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
469
|
+
}
|
|
470
|
+
function printGroupedViolations(violations, limit) {
|
|
471
|
+
const groups = /* @__PURE__ */ new Map();
|
|
472
|
+
for (const v of violations) {
|
|
473
|
+
const existing = groups.get(v.rule) ?? [];
|
|
474
|
+
existing.push(v);
|
|
475
|
+
groups.set(v.rule, existing);
|
|
476
|
+
}
|
|
477
|
+
const ruleOrder = ["file-size", "file-naming", "missing-test", "boundary-violation"];
|
|
478
|
+
const sortedKeys = [...groups.keys()].sort(
|
|
479
|
+
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
480
|
+
);
|
|
481
|
+
let totalShown = 0;
|
|
482
|
+
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
483
|
+
for (const rule of sortedKeys) {
|
|
484
|
+
const group = groups.get(rule);
|
|
485
|
+
if (!group) continue;
|
|
486
|
+
const remaining = totalLimit - totalShown;
|
|
487
|
+
if (remaining <= 0) break;
|
|
488
|
+
const toShow = group.slice(0, remaining);
|
|
489
|
+
const hidden = group.length - toShow.length;
|
|
490
|
+
for (const v of toShow) {
|
|
491
|
+
const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
|
|
492
|
+
console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
493
|
+
}
|
|
494
|
+
totalShown += toShow.length;
|
|
495
|
+
if (hidden > 0) {
|
|
496
|
+
console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function printSummary(violations) {
|
|
501
|
+
const counts = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const v of violations) {
|
|
503
|
+
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
504
|
+
}
|
|
505
|
+
const word = violations.length === 1 ? "violation" : "violations";
|
|
506
|
+
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
507
|
+
console.log(`
|
|
508
|
+
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
509
|
+
}
|
|
443
510
|
async function checkCommand(options, cwd) {
|
|
444
511
|
const startDir = cwd ?? process.cwd();
|
|
445
512
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -476,13 +543,15 @@ async function checkCommand(options, cwd) {
|
|
|
476
543
|
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
477
544
|
if (!fs6.existsSync(absPath)) continue;
|
|
478
545
|
const resolved = resolveConfigForFile(relPath, config);
|
|
479
|
-
|
|
546
|
+
const testFile = isTestFile(relPath);
|
|
547
|
+
const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
|
|
548
|
+
if (maxLines > 0) {
|
|
480
549
|
const lines = countFileLines(absPath);
|
|
481
|
-
if (lines !== null && lines >
|
|
550
|
+
if (lines !== null && lines > maxLines) {
|
|
482
551
|
violations.push({
|
|
483
552
|
file: relPath,
|
|
484
553
|
rule: "file-size",
|
|
485
|
-
message: `${lines} lines (max ${
|
|
554
|
+
message: `${lines} lines (max ${maxLines}). Split into focused modules.`,
|
|
486
555
|
severity
|
|
487
556
|
});
|
|
488
557
|
}
|
|
@@ -503,7 +572,8 @@ async function checkCommand(options, cwd) {
|
|
|
503
572
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
504
573
|
violations.push(...testViolations);
|
|
505
574
|
}
|
|
506
|
-
|
|
575
|
+
const hasBoundaries = config.boundaries ? Array.isArray(config.boundaries) ? config.boundaries.length > 0 : Object.keys(config.boundaries).length > 0 : false;
|
|
576
|
+
if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
|
|
507
577
|
const startTime = Date.now();
|
|
508
578
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
509
579
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -530,13 +600,10 @@ async function checkCommand(options, cwd) {
|
|
|
530
600
|
console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
531
601
|
return 0;
|
|
532
602
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
603
|
+
if (!options.quiet) {
|
|
604
|
+
printGroupedViolations(violations, options.limit);
|
|
536
605
|
}
|
|
537
|
-
|
|
538
|
-
console.log(`
|
|
539
|
-
${violations.length} ${word} found.`);
|
|
606
|
+
printSummary(violations);
|
|
540
607
|
if (config.enforcement === "enforce") {
|
|
541
608
|
console.log(import_chalk2.default.red("Fix violations before committing."));
|
|
542
609
|
return 1;
|
|
@@ -784,8 +851,8 @@ var path9 = __toESM(require("path"), 1);
|
|
|
784
851
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
785
852
|
const { testPattern } = config.structure;
|
|
786
853
|
if (!testPattern) return null;
|
|
787
|
-
const
|
|
788
|
-
const stem =
|
|
854
|
+
const basename7 = path9.basename(sourceRelPath);
|
|
855
|
+
const stem = basename7.slice(0, basename7.indexOf("."));
|
|
789
856
|
const testSuffix = testPattern.replace("*", "");
|
|
790
857
|
const testFilename = `${stem}${testSuffix}`;
|
|
791
858
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
@@ -911,223 +978,10 @@ async function fixCommand(options, cwd) {
|
|
|
911
978
|
// src/commands/init.ts
|
|
912
979
|
var fs12 = __toESM(require("fs"), 1);
|
|
913
980
|
var path13 = __toESM(require("path"), 1);
|
|
981
|
+
var p2 = __toESM(require("@clack/prompts"), 1);
|
|
914
982
|
var import_config4 = require("@viberails/config");
|
|
915
983
|
var import_scanner = require("@viberails/scanner");
|
|
916
|
-
var
|
|
917
|
-
|
|
918
|
-
// src/display.ts
|
|
919
|
-
var import_types3 = require("@viberails/types");
|
|
920
|
-
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
921
|
-
|
|
922
|
-
// src/display-helpers.ts
|
|
923
|
-
var import_types = require("@viberails/types");
|
|
924
|
-
function groupByRole(directories) {
|
|
925
|
-
const map = /* @__PURE__ */ new Map();
|
|
926
|
-
for (const dir of directories) {
|
|
927
|
-
if (dir.role === "unknown") continue;
|
|
928
|
-
const existing = map.get(dir.role);
|
|
929
|
-
if (existing) {
|
|
930
|
-
existing.dirs.push(dir);
|
|
931
|
-
} else {
|
|
932
|
-
map.set(dir.role, { dirs: [dir] });
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
const groups = [];
|
|
936
|
-
for (const [role, { dirs }] of map) {
|
|
937
|
-
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
938
|
-
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
939
|
-
groups.push({
|
|
940
|
-
role,
|
|
941
|
-
label,
|
|
942
|
-
dirCount: dirs.length,
|
|
943
|
-
totalFiles,
|
|
944
|
-
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
return groups;
|
|
948
|
-
}
|
|
949
|
-
function formatSummary(stats, packageCount) {
|
|
950
|
-
const parts = [];
|
|
951
|
-
if (packageCount && packageCount > 1) {
|
|
952
|
-
parts.push(`${packageCount} packages`);
|
|
953
|
-
}
|
|
954
|
-
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
955
|
-
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
956
|
-
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
957
|
-
return parts.join(" \xB7 ");
|
|
958
|
-
}
|
|
959
|
-
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
960
|
-
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
961
|
-
}
|
|
962
|
-
function formatRoleGroup(group) {
|
|
963
|
-
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
964
|
-
if (group.singlePath) {
|
|
965
|
-
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
966
|
-
}
|
|
967
|
-
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
968
|
-
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// src/display-monorepo.ts
|
|
972
|
-
var import_types2 = require("@viberails/types");
|
|
973
|
-
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
974
|
-
function formatPackageSummary(pkg) {
|
|
975
|
-
const parts = [];
|
|
976
|
-
if (pkg.stack.framework) {
|
|
977
|
-
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
978
|
-
}
|
|
979
|
-
if (pkg.stack.styling) {
|
|
980
|
-
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
981
|
-
}
|
|
982
|
-
const files = `${pkg.statistics.totalFiles} files`;
|
|
983
|
-
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
984
|
-
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
985
|
-
}
|
|
986
|
-
function displayMonorepoResults(scanResult) {
|
|
987
|
-
const { stack, packages } = scanResult;
|
|
988
|
-
console.log(`
|
|
989
|
-
${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
990
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
991
|
-
if (stack.packageManager) {
|
|
992
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
993
|
-
}
|
|
994
|
-
if (stack.linter) {
|
|
995
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
996
|
-
}
|
|
997
|
-
if (stack.formatter) {
|
|
998
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
999
|
-
}
|
|
1000
|
-
if (stack.testRunner) {
|
|
1001
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1002
|
-
}
|
|
1003
|
-
console.log("");
|
|
1004
|
-
for (const pkg of packages) {
|
|
1005
|
-
console.log(formatPackageSummary(pkg));
|
|
1006
|
-
}
|
|
1007
|
-
const packagesWithDirs = packages.filter(
|
|
1008
|
-
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1009
|
-
);
|
|
1010
|
-
if (packagesWithDirs.length > 0) {
|
|
1011
|
-
console.log(`
|
|
1012
|
-
${import_chalk5.default.bold("Structure:")}`);
|
|
1013
|
-
for (const pkg of packagesWithDirs) {
|
|
1014
|
-
const groups = groupByRole(pkg.structure.directories);
|
|
1015
|
-
if (groups.length === 0) continue;
|
|
1016
|
-
console.log(` ${pkg.relativePath}:`);
|
|
1017
|
-
for (const group of groups) {
|
|
1018
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
displayConventions(scanResult);
|
|
1023
|
-
displaySummarySection(scanResult);
|
|
1024
|
-
console.log("");
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// src/display.ts
|
|
1028
|
-
var CONVENTION_LABELS = {
|
|
1029
|
-
fileNaming: "File naming",
|
|
1030
|
-
componentNaming: "Component naming",
|
|
1031
|
-
hookNaming: "Hook naming",
|
|
1032
|
-
importAlias: "Import alias"
|
|
1033
|
-
};
|
|
1034
|
-
function formatItem(item, nameMap) {
|
|
1035
|
-
const name = nameMap?.[item.name] ?? item.name;
|
|
1036
|
-
return item.version ? `${name} ${item.version}` : name;
|
|
1037
|
-
}
|
|
1038
|
-
function confidenceLabel(convention) {
|
|
1039
|
-
const pct = Math.round(convention.consistency);
|
|
1040
|
-
if (convention.confidence === "high") {
|
|
1041
|
-
return `${pct}% \u2014 high confidence, will enforce`;
|
|
1042
|
-
}
|
|
1043
|
-
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
1044
|
-
}
|
|
1045
|
-
function displayConventions(scanResult) {
|
|
1046
|
-
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1047
|
-
if (conventionEntries.length === 0) return;
|
|
1048
|
-
console.log(`
|
|
1049
|
-
${import_chalk6.default.bold("Conventions:")}`);
|
|
1050
|
-
for (const [key, convention] of conventionEntries) {
|
|
1051
|
-
if (convention.confidence === "low") continue;
|
|
1052
|
-
const label = CONVENTION_LABELS[key] ?? key;
|
|
1053
|
-
if (scanResult.packages.length > 1) {
|
|
1054
|
-
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1055
|
-
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1056
|
-
if (allSame || pkgValues.length <= 1) {
|
|
1057
|
-
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1058
|
-
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1059
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1060
|
-
} else {
|
|
1061
|
-
console.log(` ${import_chalk6.default.yellow("~")} ${label}: varies by package`);
|
|
1062
|
-
for (const pv of pkgValues) {
|
|
1063
|
-
const pct = Math.round(pv.convention.consistency);
|
|
1064
|
-
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
} else {
|
|
1068
|
-
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1069
|
-
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1070
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
function displaySummarySection(scanResult) {
|
|
1075
|
-
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1076
|
-
console.log(`
|
|
1077
|
-
${import_chalk6.default.bold("Summary:")}`);
|
|
1078
|
-
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1079
|
-
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1080
|
-
if (ext) {
|
|
1081
|
-
console.log(` ${ext}`);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
function displayScanResults(scanResult) {
|
|
1085
|
-
if (scanResult.packages.length > 1) {
|
|
1086
|
-
displayMonorepoResults(scanResult);
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
const { stack } = scanResult;
|
|
1090
|
-
console.log(`
|
|
1091
|
-
${import_chalk6.default.bold("Detected:")}`);
|
|
1092
|
-
if (stack.framework) {
|
|
1093
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
1094
|
-
}
|
|
1095
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1096
|
-
if (stack.styling) {
|
|
1097
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
1098
|
-
}
|
|
1099
|
-
if (stack.backend) {
|
|
1100
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
1101
|
-
}
|
|
1102
|
-
if (stack.linter) {
|
|
1103
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1104
|
-
}
|
|
1105
|
-
if (stack.formatter) {
|
|
1106
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1107
|
-
}
|
|
1108
|
-
if (stack.testRunner) {
|
|
1109
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1110
|
-
}
|
|
1111
|
-
if (stack.packageManager) {
|
|
1112
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1113
|
-
}
|
|
1114
|
-
if (stack.libraries.length > 0) {
|
|
1115
|
-
for (const lib of stack.libraries) {
|
|
1116
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
const groups = groupByRole(scanResult.structure.directories);
|
|
1120
|
-
if (groups.length > 0) {
|
|
1121
|
-
console.log(`
|
|
1122
|
-
${import_chalk6.default.bold("Structure:")}`);
|
|
1123
|
-
for (const group of groups) {
|
|
1124
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
displayConventions(scanResult);
|
|
1128
|
-
displaySummarySection(scanResult);
|
|
1129
|
-
console.log("");
|
|
1130
|
-
}
|
|
984
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
1131
985
|
|
|
1132
986
|
// src/utils/write-generated-files.ts
|
|
1133
987
|
var fs10 = __toESM(require("fs"), 1);
|
|
@@ -1158,18 +1012,60 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1158
1012
|
// src/commands/init-hooks.ts
|
|
1159
1013
|
var fs11 = __toESM(require("fs"), 1);
|
|
1160
1014
|
var path12 = __toESM(require("path"), 1);
|
|
1161
|
-
var
|
|
1015
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
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
|
+
}
|
|
1162
1058
|
function setupPreCommitHook(projectRoot) {
|
|
1163
1059
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1164
1060
|
if (fs11.existsSync(lefthookPath)) {
|
|
1165
1061
|
addLefthookPreCommit(lefthookPath);
|
|
1166
|
-
console.log(` ${
|
|
1062
|
+
console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1167
1063
|
return;
|
|
1168
1064
|
}
|
|
1169
1065
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1170
1066
|
if (fs11.existsSync(huskyDir)) {
|
|
1171
1067
|
writeHuskyPreCommit(huskyDir);
|
|
1172
|
-
console.log(` ${
|
|
1068
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1173
1069
|
return;
|
|
1174
1070
|
}
|
|
1175
1071
|
const gitDir = path12.join(projectRoot, ".git");
|
|
@@ -1179,7 +1075,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1179
1075
|
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1180
1076
|
}
|
|
1181
1077
|
writeGitHookPreCommit(hooksDir);
|
|
1182
|
-
console.log(` ${
|
|
1078
|
+
console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
1183
1079
|
}
|
|
1184
1080
|
}
|
|
1185
1081
|
function writeGitHookPreCommit(hooksDir) {
|
|
@@ -1228,6 +1124,187 @@ npx viberails check --staged
|
|
|
1228
1124
|
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1229
1125
|
}
|
|
1230
1126
|
|
|
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
|
+
|
|
1231
1308
|
// src/commands/init.ts
|
|
1232
1309
|
var CONFIG_FILE4 = "viberails.config.json";
|
|
1233
1310
|
function filterHighConfidence(conventions) {
|
|
@@ -1242,6 +1319,13 @@ function filterHighConfidence(conventions) {
|
|
|
1242
1319
|
}
|
|
1243
1320
|
return filtered;
|
|
1244
1321
|
}
|
|
1322
|
+
function applyWizardResult(config, wizard) {
|
|
1323
|
+
config.enforcement = wizard.enforcement;
|
|
1324
|
+
if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
|
|
1325
|
+
config.rules.enforceNaming = wizard.checks.naming;
|
|
1326
|
+
config.rules.requireTests = wizard.checks.tests;
|
|
1327
|
+
config.rules.enforceBoundaries = wizard.checks.boundaries;
|
|
1328
|
+
}
|
|
1245
1329
|
async function initCommand(options, cwd) {
|
|
1246
1330
|
const startDir = cwd ?? process.cwd();
|
|
1247
1331
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1253,64 +1337,69 @@ async function initCommand(options, cwd) {
|
|
|
1253
1337
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1254
1338
|
if (fs12.existsSync(configPath)) {
|
|
1255
1339
|
console.log(
|
|
1256
|
-
|
|
1340
|
+
import_chalk9.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk9.default.cyan("viberails sync") + " to update the generated files."
|
|
1257
1341
|
);
|
|
1258
1342
|
return;
|
|
1259
1343
|
}
|
|
1260
|
-
|
|
1344
|
+
p2.intro("viberails");
|
|
1345
|
+
const s = p2.spinner();
|
|
1346
|
+
s.start("Scanning project...");
|
|
1261
1347
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
1262
|
-
|
|
1348
|
+
s.stop("Scan complete");
|
|
1263
1349
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1350
|
+
p2.log.warn(
|
|
1351
|
+
`No source files detected. viberails will generate context with minimal content.
|
|
1352
|
+
Run ${import_chalk9.default.cyan("viberails sync")} after adding source files.`
|
|
1266
1353
|
);
|
|
1267
1354
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1355
|
+
let wizard;
|
|
1356
|
+
if (options.yes) {
|
|
1357
|
+
wizard = { ...DEFAULT_WIZARD_RESULT };
|
|
1358
|
+
} else {
|
|
1359
|
+
const result = await runWizard(scanResult);
|
|
1360
|
+
if (!result) return;
|
|
1361
|
+
wizard = result;
|
|
1274
1362
|
}
|
|
1275
1363
|
const config = (0, import_config4.generateConfig)(scanResult);
|
|
1276
1364
|
if (options.yes) {
|
|
1277
1365
|
config.conventions = filterHighConfidence(config.conventions);
|
|
1278
1366
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
console.log(` ${import_chalk8.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
|
|
1294
|
-
}
|
|
1367
|
+
applyWizardResult(config, wizard);
|
|
1368
|
+
if (wizard.checks.boundaries && config.workspace && config.workspace.packages.length > 0) {
|
|
1369
|
+
s.start("Inferring boundary rules...");
|
|
1370
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1371
|
+
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
1372
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
1373
|
+
const inferred = inferBoundaries(graph);
|
|
1374
|
+
const ruleCount = Object.values(inferred).reduce((sum, denied) => sum + denied.length, 0);
|
|
1375
|
+
if (ruleCount > 0) {
|
|
1376
|
+
config.boundaries = inferred;
|
|
1377
|
+
s.stop(`Inferred ${ruleCount} boundary rules`);
|
|
1378
|
+
} else {
|
|
1379
|
+
s.stop("No boundary rules could be inferred");
|
|
1380
|
+
config.rules.enforceBoundaries = false;
|
|
1295
1381
|
}
|
|
1296
1382
|
}
|
|
1297
1383
|
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1298
1384
|
`);
|
|
1299
1385
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1300
1386
|
updateGitignore(projectRoot);
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
${
|
|
1309
|
-
console.log(`
|
|
1310
|
-
console.log(
|
|
1311
|
-
|
|
1387
|
+
if (wizard.integration.includes("pre-commit")) {
|
|
1388
|
+
setupPreCommitHook(projectRoot);
|
|
1389
|
+
}
|
|
1390
|
+
if (wizard.integration.includes("claude-hook")) {
|
|
1391
|
+
setupClaudeCodeHook(projectRoot);
|
|
1392
|
+
}
|
|
1393
|
+
p2.log.success(`${import_chalk9.default.bold("Created:")}`);
|
|
1394
|
+
console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1395
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
|
|
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`
|
|
1312
1402
|
);
|
|
1313
|
-
console.log(` 3. Run ${import_chalk8.default.cyan("viberails check")} to verify your project passes`);
|
|
1314
1403
|
}
|
|
1315
1404
|
function updateGitignore(projectRoot) {
|
|
1316
1405
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
@@ -1330,7 +1419,7 @@ var fs13 = __toESM(require("fs"), 1);
|
|
|
1330
1419
|
var path14 = __toESM(require("path"), 1);
|
|
1331
1420
|
var import_config5 = require("@viberails/config");
|
|
1332
1421
|
var import_scanner2 = require("@viberails/scanner");
|
|
1333
|
-
var
|
|
1422
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
1334
1423
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1335
1424
|
async function syncCommand(cwd) {
|
|
1336
1425
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1342,21 +1431,21 @@ async function syncCommand(cwd) {
|
|
|
1342
1431
|
}
|
|
1343
1432
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1344
1433
|
const existing = await (0, import_config5.loadConfig)(configPath);
|
|
1345
|
-
console.log(
|
|
1434
|
+
console.log(import_chalk10.default.dim("Scanning project..."));
|
|
1346
1435
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
1347
1436
|
const merged = (0, import_config5.mergeConfig)(existing, scanResult);
|
|
1348
1437
|
fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
|
|
1349
1438
|
`);
|
|
1350
1439
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1351
1440
|
console.log(`
|
|
1352
|
-
${
|
|
1353
|
-
console.log(` ${
|
|
1354
|
-
console.log(` ${
|
|
1355
|
-
console.log(` ${
|
|
1441
|
+
${import_chalk10.default.bold("Synced:")}`);
|
|
1442
|
+
console.log(` ${import_chalk10.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
|
|
1443
|
+
console.log(` ${import_chalk10.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1444
|
+
console.log(` ${import_chalk10.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1356
1445
|
}
|
|
1357
1446
|
|
|
1358
1447
|
// src/index.ts
|
|
1359
|
-
var VERSION = "0.
|
|
1448
|
+
var VERSION = "0.3.0";
|
|
1360
1449
|
var program = new import_commander.Command();
|
|
1361
1450
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1362
1451
|
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) => {
|
|
@@ -1364,7 +1453,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
1364
1453
|
await initCommand(options);
|
|
1365
1454
|
} catch (err) {
|
|
1366
1455
|
const message = err instanceof Error ? err.message : String(err);
|
|
1367
|
-
console.error(`${
|
|
1456
|
+
console.error(`${import_chalk11.default.red("Error:")} ${message}`);
|
|
1368
1457
|
process.exit(1);
|
|
1369
1458
|
}
|
|
1370
1459
|
});
|
|
@@ -1373,30 +1462,32 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1373
1462
|
await syncCommand();
|
|
1374
1463
|
} catch (err) {
|
|
1375
1464
|
const message = err instanceof Error ? err.message : String(err);
|
|
1376
|
-
console.error(`${
|
|
1465
|
+
console.error(`${import_chalk11.default.red("Error:")} ${message}`);
|
|
1377
1466
|
process.exit(1);
|
|
1378
1467
|
}
|
|
1379
1468
|
});
|
|
1380
|
-
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").
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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(
|
|
1470
|
+
async (options) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const exitCode = await checkCommand({
|
|
1473
|
+
...options,
|
|
1474
|
+
noBoundaries: options.boundaries === false
|
|
1475
|
+
});
|
|
1476
|
+
process.exit(exitCode);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1479
|
+
console.error(`${import_chalk11.default.red("Error:")} ${message}`);
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1391
1482
|
}
|
|
1392
|
-
|
|
1483
|
+
);
|
|
1393
1484
|
program.command("fix").description("Auto-fix file naming violations and generate missing test stubs").option("--dry-run", "Show planned fixes without applying them").option("--rule <rules...>", "Fix only specific rules (file-naming, missing-test)").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
1394
1485
|
try {
|
|
1395
1486
|
const exitCode = await fixCommand(options);
|
|
1396
1487
|
process.exit(exitCode);
|
|
1397
1488
|
} catch (err) {
|
|
1398
1489
|
const message = err instanceof Error ? err.message : String(err);
|
|
1399
|
-
console.error(`${
|
|
1490
|
+
console.error(`${import_chalk11.default.red("Error:")} ${message}`);
|
|
1400
1491
|
process.exit(1);
|
|
1401
1492
|
}
|
|
1402
1493
|
});
|
|
@@ -1405,7 +1496,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
1405
1496
|
await boundariesCommand(options);
|
|
1406
1497
|
} catch (err) {
|
|
1407
1498
|
const message = err instanceof Error ? err.message : String(err);
|
|
1408
|
-
console.error(`${
|
|
1499
|
+
console.error(`${import_chalk11.default.red("Error:")} ${message}`);
|
|
1409
1500
|
process.exit(1);
|
|
1410
1501
|
}
|
|
1411
1502
|
});
|