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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk11 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -66,7 +66,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
66
66
|
];
|
|
67
67
|
packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
68
68
|
}
|
|
69
|
-
const packageNames = new Set(packages.map((
|
|
69
|
+
const packageNames = new Set(packages.map((p3) => p3.name));
|
|
70
70
|
for (const pkg of packages) {
|
|
71
71
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
72
72
|
}
|
|
@@ -96,23 +96,37 @@ async function boundariesCommand(options, cwd) {
|
|
|
96
96
|
}
|
|
97
97
|
displayRules(config);
|
|
98
98
|
}
|
|
99
|
+
function countBoundaries(boundaries) {
|
|
100
|
+
if (!boundaries) return 0;
|
|
101
|
+
if (Array.isArray(boundaries)) return boundaries.length;
|
|
102
|
+
return Object.values(boundaries).reduce((sum, denied) => sum + denied.length, 0);
|
|
103
|
+
}
|
|
99
104
|
function displayRules(config) {
|
|
100
|
-
|
|
105
|
+
const total = countBoundaries(config.boundaries);
|
|
106
|
+
if (total === 0) {
|
|
101
107
|
console.log(chalk.yellow("No boundary rules configured."));
|
|
102
108
|
console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
103
109
|
return;
|
|
104
110
|
}
|
|
105
|
-
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
106
|
-
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
107
111
|
console.log(`
|
|
108
|
-
${chalk.bold(`Boundary rules (${
|
|
112
|
+
${chalk.bold(`Boundary rules (${total} rules):`)}
|
|
109
113
|
`);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
if (Array.isArray(config.boundaries)) {
|
|
115
|
+
const allowRules = config.boundaries.filter((r) => r.allow);
|
|
116
|
+
const denyRules = config.boundaries.filter((r) => !r.allow);
|
|
117
|
+
for (const r of allowRules) {
|
|
118
|
+
console.log(` ${chalk.green("\u2713")} ${r.from} \u2192 ${r.to}`);
|
|
119
|
+
}
|
|
120
|
+
for (const r of denyRules) {
|
|
121
|
+
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
122
|
+
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
123
|
+
}
|
|
124
|
+
} else if (config.boundaries) {
|
|
125
|
+
for (const [from, denied] of Object.entries(config.boundaries)) {
|
|
126
|
+
for (const to of denied) {
|
|
127
|
+
console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
116
130
|
}
|
|
117
131
|
console.log(
|
|
118
132
|
`
|
|
@@ -129,31 +143,29 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
129
143
|
});
|
|
130
144
|
console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
131
145
|
const inferred = inferBoundaries(graph);
|
|
132
|
-
|
|
146
|
+
const entries = Object.entries(inferred);
|
|
147
|
+
if (entries.length === 0) {
|
|
133
148
|
console.log(chalk.yellow("No boundary rules could be inferred."));
|
|
134
149
|
return;
|
|
135
150
|
}
|
|
136
|
-
const
|
|
137
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
151
|
+
const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
|
|
138
152
|
console.log(`
|
|
139
153
|
${chalk.bold("Inferred boundary rules:")}
|
|
140
154
|
`);
|
|
141
|
-
for (const
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
146
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
155
|
+
for (const [from, denied] of entries) {
|
|
156
|
+
for (const to of denied) {
|
|
157
|
+
console.log(` ${chalk.red("\u2717")} ${from} \u2192 ${to}`);
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
console.log(`
|
|
149
|
-
${
|
|
161
|
+
${totalRules} deny rules`);
|
|
150
162
|
const shouldSave = await confirm("\nSave to viberails.config.json?");
|
|
151
163
|
if (shouldSave) {
|
|
152
164
|
config.boundaries = inferred;
|
|
153
165
|
config.rules.enforceBoundaries = true;
|
|
154
166
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
155
167
|
`);
|
|
156
|
-
console.log(`${chalk.green("\u2713")} Saved ${
|
|
168
|
+
console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
async function showGraph(projectRoot, config) {
|
|
@@ -234,6 +246,10 @@ var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
234
246
|
".svelte-kit",
|
|
235
247
|
".turbo",
|
|
236
248
|
"coverage",
|
|
249
|
+
"public",
|
|
250
|
+
"vendor",
|
|
251
|
+
"__generated__",
|
|
252
|
+
"generated",
|
|
237
253
|
".viberails"
|
|
238
254
|
]);
|
|
239
255
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
@@ -255,12 +271,19 @@ var NAMING_PATTERNS = {
|
|
|
255
271
|
};
|
|
256
272
|
function isIgnored(relPath, ignorePatterns) {
|
|
257
273
|
for (const pattern of ignorePatterns) {
|
|
258
|
-
|
|
274
|
+
const startsGlob = pattern.startsWith("**/");
|
|
275
|
+
const endsGlob = pattern.endsWith("/**");
|
|
276
|
+
if (startsGlob && endsGlob) {
|
|
277
|
+
const middle = pattern.slice(3, -3);
|
|
278
|
+
if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
} else if (endsGlob) {
|
|
259
282
|
const prefix = pattern.slice(0, -3);
|
|
260
283
|
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
261
|
-
} else if (
|
|
284
|
+
} else if (startsGlob) {
|
|
262
285
|
const suffix = pattern.slice(3);
|
|
263
|
-
if (relPath.endsWith(suffix)) return true;
|
|
286
|
+
if (relPath.endsWith(suffix) || relPath === suffix) return true;
|
|
264
287
|
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
265
288
|
return true;
|
|
266
289
|
}
|
|
@@ -380,13 +403,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
380
403
|
const testSuffix = testPattern.replace("*", "");
|
|
381
404
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
382
405
|
for (const relFile of sourceFiles) {
|
|
383
|
-
const
|
|
384
|
-
if (
|
|
406
|
+
const basename7 = path5.basename(relFile);
|
|
407
|
+
if (basename7.includes(".test.") || basename7.includes(".spec.") || basename7.startsWith("index.") || basename7.endsWith(".d.ts")) {
|
|
385
408
|
continue;
|
|
386
409
|
}
|
|
387
|
-
const ext = path5.extname(
|
|
410
|
+
const ext = path5.extname(basename7);
|
|
388
411
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
389
|
-
const stem =
|
|
412
|
+
const stem = basename7.slice(0, basename7.indexOf("."));
|
|
390
413
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
391
414
|
const dir = path5.dirname(path5.join(projectRoot, relFile));
|
|
392
415
|
const colocatedTest = path5.join(dir, expectedTestFile);
|
|
@@ -407,6 +430,50 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
407
430
|
|
|
408
431
|
// src/commands/check.ts
|
|
409
432
|
var CONFIG_FILE2 = "viberails.config.json";
|
|
433
|
+
function isTestFile(relPath) {
|
|
434
|
+
const filename = path6.basename(relPath);
|
|
435
|
+
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
436
|
+
}
|
|
437
|
+
function printGroupedViolations(violations, limit) {
|
|
438
|
+
const groups = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const v of violations) {
|
|
440
|
+
const existing = groups.get(v.rule) ?? [];
|
|
441
|
+
existing.push(v);
|
|
442
|
+
groups.set(v.rule, existing);
|
|
443
|
+
}
|
|
444
|
+
const ruleOrder = ["file-size", "file-naming", "missing-test", "boundary-violation"];
|
|
445
|
+
const sortedKeys = [...groups.keys()].sort(
|
|
446
|
+
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
447
|
+
);
|
|
448
|
+
let totalShown = 0;
|
|
449
|
+
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
450
|
+
for (const rule of sortedKeys) {
|
|
451
|
+
const group = groups.get(rule);
|
|
452
|
+
if (!group) continue;
|
|
453
|
+
const remaining = totalLimit - totalShown;
|
|
454
|
+
if (remaining <= 0) break;
|
|
455
|
+
const toShow = group.slice(0, remaining);
|
|
456
|
+
const hidden = group.length - toShow.length;
|
|
457
|
+
for (const v of toShow) {
|
|
458
|
+
const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
459
|
+
console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
460
|
+
}
|
|
461
|
+
totalShown += toShow.length;
|
|
462
|
+
if (hidden > 0) {
|
|
463
|
+
console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function printSummary(violations) {
|
|
468
|
+
const counts = /* @__PURE__ */ new Map();
|
|
469
|
+
for (const v of violations) {
|
|
470
|
+
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
471
|
+
}
|
|
472
|
+
const word = violations.length === 1 ? "violation" : "violations";
|
|
473
|
+
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
474
|
+
console.log(`
|
|
475
|
+
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
476
|
+
}
|
|
410
477
|
async function checkCommand(options, cwd) {
|
|
411
478
|
const startDir = cwd ?? process.cwd();
|
|
412
479
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -443,13 +510,15 @@ async function checkCommand(options, cwd) {
|
|
|
443
510
|
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
444
511
|
if (!fs6.existsSync(absPath)) continue;
|
|
445
512
|
const resolved = resolveConfigForFile(relPath, config);
|
|
446
|
-
|
|
513
|
+
const testFile = isTestFile(relPath);
|
|
514
|
+
const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
|
|
515
|
+
if (maxLines > 0) {
|
|
447
516
|
const lines = countFileLines(absPath);
|
|
448
|
-
if (lines !== null && lines >
|
|
517
|
+
if (lines !== null && lines > maxLines) {
|
|
449
518
|
violations.push({
|
|
450
519
|
file: relPath,
|
|
451
520
|
rule: "file-size",
|
|
452
|
-
message: `${lines} lines (max ${
|
|
521
|
+
message: `${lines} lines (max ${maxLines}). Split into focused modules.`,
|
|
453
522
|
severity
|
|
454
523
|
});
|
|
455
524
|
}
|
|
@@ -470,7 +539,8 @@ async function checkCommand(options, cwd) {
|
|
|
470
539
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
471
540
|
violations.push(...testViolations);
|
|
472
541
|
}
|
|
473
|
-
|
|
542
|
+
const hasBoundaries = config.boundaries ? Array.isArray(config.boundaries) ? config.boundaries.length > 0 : Object.keys(config.boundaries).length > 0 : false;
|
|
543
|
+
if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
|
|
474
544
|
const startTime = Date.now();
|
|
475
545
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
476
546
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -497,13 +567,10 @@ async function checkCommand(options, cwd) {
|
|
|
497
567
|
console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
498
568
|
return 0;
|
|
499
569
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
570
|
+
if (!options.quiet) {
|
|
571
|
+
printGroupedViolations(violations, options.limit);
|
|
503
572
|
}
|
|
504
|
-
|
|
505
|
-
console.log(`
|
|
506
|
-
${violations.length} ${word} found.`);
|
|
573
|
+
printSummary(violations);
|
|
507
574
|
if (config.enforcement === "enforce") {
|
|
508
575
|
console.log(chalk2.red("Fix violations before committing."));
|
|
509
576
|
return 1;
|
|
@@ -751,8 +818,8 @@ import * as path9 from "path";
|
|
|
751
818
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
752
819
|
const { testPattern } = config.structure;
|
|
753
820
|
if (!testPattern) return null;
|
|
754
|
-
const
|
|
755
|
-
const stem =
|
|
821
|
+
const basename7 = path9.basename(sourceRelPath);
|
|
822
|
+
const stem = basename7.slice(0, basename7.indexOf("."));
|
|
756
823
|
const testSuffix = testPattern.replace("*", "");
|
|
757
824
|
const testFilename = `${stem}${testSuffix}`;
|
|
758
825
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
@@ -878,223 +945,10 @@ async function fixCommand(options, cwd) {
|
|
|
878
945
|
// src/commands/init.ts
|
|
879
946
|
import * as fs12 from "fs";
|
|
880
947
|
import * as path13 from "path";
|
|
948
|
+
import * as p2 from "@clack/prompts";
|
|
881
949
|
import { generateConfig } from "@viberails/config";
|
|
882
950
|
import { scan } from "@viberails/scanner";
|
|
883
|
-
import
|
|
884
|
-
|
|
885
|
-
// src/display.ts
|
|
886
|
-
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
|
|
887
|
-
import chalk6 from "chalk";
|
|
888
|
-
|
|
889
|
-
// src/display-helpers.ts
|
|
890
|
-
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
891
|
-
function groupByRole(directories) {
|
|
892
|
-
const map = /* @__PURE__ */ new Map();
|
|
893
|
-
for (const dir of directories) {
|
|
894
|
-
if (dir.role === "unknown") continue;
|
|
895
|
-
const existing = map.get(dir.role);
|
|
896
|
-
if (existing) {
|
|
897
|
-
existing.dirs.push(dir);
|
|
898
|
-
} else {
|
|
899
|
-
map.set(dir.role, { dirs: [dir] });
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const groups = [];
|
|
903
|
-
for (const [role, { dirs }] of map) {
|
|
904
|
-
const label = ROLE_DESCRIPTIONS[role] ?? role;
|
|
905
|
-
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
906
|
-
groups.push({
|
|
907
|
-
role,
|
|
908
|
-
label,
|
|
909
|
-
dirCount: dirs.length,
|
|
910
|
-
totalFiles,
|
|
911
|
-
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
return groups;
|
|
915
|
-
}
|
|
916
|
-
function formatSummary(stats, packageCount) {
|
|
917
|
-
const parts = [];
|
|
918
|
-
if (packageCount && packageCount > 1) {
|
|
919
|
-
parts.push(`${packageCount} packages`);
|
|
920
|
-
}
|
|
921
|
-
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
922
|
-
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
923
|
-
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
924
|
-
return parts.join(" \xB7 ");
|
|
925
|
-
}
|
|
926
|
-
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
927
|
-
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
928
|
-
}
|
|
929
|
-
function formatRoleGroup(group) {
|
|
930
|
-
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
931
|
-
if (group.singlePath) {
|
|
932
|
-
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
933
|
-
}
|
|
934
|
-
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
935
|
-
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// src/display-monorepo.ts
|
|
939
|
-
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
940
|
-
import chalk5 from "chalk";
|
|
941
|
-
function formatPackageSummary(pkg) {
|
|
942
|
-
const parts = [];
|
|
943
|
-
if (pkg.stack.framework) {
|
|
944
|
-
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
|
|
945
|
-
}
|
|
946
|
-
if (pkg.stack.styling) {
|
|
947
|
-
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
948
|
-
}
|
|
949
|
-
const files = `${pkg.statistics.totalFiles} files`;
|
|
950
|
-
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
951
|
-
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
952
|
-
}
|
|
953
|
-
function displayMonorepoResults(scanResult) {
|
|
954
|
-
const { stack, packages } = scanResult;
|
|
955
|
-
console.log(`
|
|
956
|
-
${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
957
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
|
|
958
|
-
if (stack.packageManager) {
|
|
959
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
960
|
-
}
|
|
961
|
-
if (stack.linter) {
|
|
962
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
963
|
-
}
|
|
964
|
-
if (stack.formatter) {
|
|
965
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
966
|
-
}
|
|
967
|
-
if (stack.testRunner) {
|
|
968
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
969
|
-
}
|
|
970
|
-
console.log("");
|
|
971
|
-
for (const pkg of packages) {
|
|
972
|
-
console.log(formatPackageSummary(pkg));
|
|
973
|
-
}
|
|
974
|
-
const packagesWithDirs = packages.filter(
|
|
975
|
-
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
976
|
-
);
|
|
977
|
-
if (packagesWithDirs.length > 0) {
|
|
978
|
-
console.log(`
|
|
979
|
-
${chalk5.bold("Structure:")}`);
|
|
980
|
-
for (const pkg of packagesWithDirs) {
|
|
981
|
-
const groups = groupByRole(pkg.structure.directories);
|
|
982
|
-
if (groups.length === 0) continue;
|
|
983
|
-
console.log(` ${pkg.relativePath}:`);
|
|
984
|
-
for (const group of groups) {
|
|
985
|
-
console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
displayConventions(scanResult);
|
|
990
|
-
displaySummarySection(scanResult);
|
|
991
|
-
console.log("");
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// src/display.ts
|
|
995
|
-
var CONVENTION_LABELS = {
|
|
996
|
-
fileNaming: "File naming",
|
|
997
|
-
componentNaming: "Component naming",
|
|
998
|
-
hookNaming: "Hook naming",
|
|
999
|
-
importAlias: "Import alias"
|
|
1000
|
-
};
|
|
1001
|
-
function formatItem(item, nameMap) {
|
|
1002
|
-
const name = nameMap?.[item.name] ?? item.name;
|
|
1003
|
-
return item.version ? `${name} ${item.version}` : name;
|
|
1004
|
-
}
|
|
1005
|
-
function confidenceLabel(convention) {
|
|
1006
|
-
const pct = Math.round(convention.consistency);
|
|
1007
|
-
if (convention.confidence === "high") {
|
|
1008
|
-
return `${pct}% \u2014 high confidence, will enforce`;
|
|
1009
|
-
}
|
|
1010
|
-
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
1011
|
-
}
|
|
1012
|
-
function displayConventions(scanResult) {
|
|
1013
|
-
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1014
|
-
if (conventionEntries.length === 0) return;
|
|
1015
|
-
console.log(`
|
|
1016
|
-
${chalk6.bold("Conventions:")}`);
|
|
1017
|
-
for (const [key, convention] of conventionEntries) {
|
|
1018
|
-
if (convention.confidence === "low") continue;
|
|
1019
|
-
const label = CONVENTION_LABELS[key] ?? key;
|
|
1020
|
-
if (scanResult.packages.length > 1) {
|
|
1021
|
-
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1022
|
-
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1023
|
-
if (allSame || pkgValues.length <= 1) {
|
|
1024
|
-
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1025
|
-
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1026
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1027
|
-
} else {
|
|
1028
|
-
console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
|
|
1029
|
-
for (const pv of pkgValues) {
|
|
1030
|
-
const pct = Math.round(pv.convention.consistency);
|
|
1031
|
-
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1036
|
-
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1037
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
function displaySummarySection(scanResult) {
|
|
1042
|
-
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1043
|
-
console.log(`
|
|
1044
|
-
${chalk6.bold("Summary:")}`);
|
|
1045
|
-
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1046
|
-
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1047
|
-
if (ext) {
|
|
1048
|
-
console.log(` ${ext}`);
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
function displayScanResults(scanResult) {
|
|
1052
|
-
if (scanResult.packages.length > 1) {
|
|
1053
|
-
displayMonorepoResults(scanResult);
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
const { stack } = scanResult;
|
|
1057
|
-
console.log(`
|
|
1058
|
-
${chalk6.bold("Detected:")}`);
|
|
1059
|
-
if (stack.framework) {
|
|
1060
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
|
|
1061
|
-
}
|
|
1062
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1063
|
-
if (stack.styling) {
|
|
1064
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
|
|
1065
|
-
}
|
|
1066
|
-
if (stack.backend) {
|
|
1067
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
|
|
1068
|
-
}
|
|
1069
|
-
if (stack.linter) {
|
|
1070
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1071
|
-
}
|
|
1072
|
-
if (stack.formatter) {
|
|
1073
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1074
|
-
}
|
|
1075
|
-
if (stack.testRunner) {
|
|
1076
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1077
|
-
}
|
|
1078
|
-
if (stack.packageManager) {
|
|
1079
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1080
|
-
}
|
|
1081
|
-
if (stack.libraries.length > 0) {
|
|
1082
|
-
for (const lib of stack.libraries) {
|
|
1083
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
const groups = groupByRole(scanResult.structure.directories);
|
|
1087
|
-
if (groups.length > 0) {
|
|
1088
|
-
console.log(`
|
|
1089
|
-
${chalk6.bold("Structure:")}`);
|
|
1090
|
-
for (const group of groups) {
|
|
1091
|
-
console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
displayConventions(scanResult);
|
|
1095
|
-
displaySummarySection(scanResult);
|
|
1096
|
-
console.log("");
|
|
1097
|
-
}
|
|
951
|
+
import chalk9 from "chalk";
|
|
1098
952
|
|
|
1099
953
|
// src/utils/write-generated-files.ts
|
|
1100
954
|
import * as fs10 from "fs";
|
|
@@ -1125,18 +979,60 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1125
979
|
// src/commands/init-hooks.ts
|
|
1126
980
|
import * as fs11 from "fs";
|
|
1127
981
|
import * as path12 from "path";
|
|
1128
|
-
import
|
|
982
|
+
import chalk5 from "chalk";
|
|
983
|
+
function setupClaudeCodeHook(projectRoot) {
|
|
984
|
+
const claudeDir = path12.join(projectRoot, ".claude");
|
|
985
|
+
if (!fs11.existsSync(claudeDir)) {
|
|
986
|
+
fs11.mkdirSync(claudeDir, { recursive: true });
|
|
987
|
+
}
|
|
988
|
+
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
989
|
+
let settings = {};
|
|
990
|
+
if (fs11.existsSync(settingsPath)) {
|
|
991
|
+
try {
|
|
992
|
+
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const hooks = settings.hooks ?? {};
|
|
997
|
+
const postToolUse = hooks.PostToolUse;
|
|
998
|
+
if (Array.isArray(postToolUse)) {
|
|
999
|
+
const hasViberails = postToolUse.some(
|
|
1000
|
+
(entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.some(
|
|
1001
|
+
(h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes("viberails")
|
|
1002
|
+
)
|
|
1003
|
+
);
|
|
1004
|
+
if (hasViberails) return;
|
|
1005
|
+
}
|
|
1006
|
+
const viberailsHook = {
|
|
1007
|
+
matcher: "Edit|Write",
|
|
1008
|
+
hooks: [
|
|
1009
|
+
{
|
|
1010
|
+
type: "command",
|
|
1011
|
+
command: "jq -r '.tool_input.file_path' | xargs npx viberails check --files"
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
};
|
|
1015
|
+
if (!hooks.PostToolUse) {
|
|
1016
|
+
hooks.PostToolUse = [viberailsHook];
|
|
1017
|
+
} else if (Array.isArray(hooks.PostToolUse)) {
|
|
1018
|
+
hooks.PostToolUse.push(viberailsHook);
|
|
1019
|
+
}
|
|
1020
|
+
settings.hooks = hooks;
|
|
1021
|
+
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1022
|
+
`);
|
|
1023
|
+
console.log(` ${chalk5.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1024
|
+
}
|
|
1129
1025
|
function setupPreCommitHook(projectRoot) {
|
|
1130
1026
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1131
1027
|
if (fs11.existsSync(lefthookPath)) {
|
|
1132
1028
|
addLefthookPreCommit(lefthookPath);
|
|
1133
|
-
console.log(` ${
|
|
1029
|
+
console.log(` ${chalk5.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1134
1030
|
return;
|
|
1135
1031
|
}
|
|
1136
1032
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1137
1033
|
if (fs11.existsSync(huskyDir)) {
|
|
1138
1034
|
writeHuskyPreCommit(huskyDir);
|
|
1139
|
-
console.log(` ${
|
|
1035
|
+
console.log(` ${chalk5.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1140
1036
|
return;
|
|
1141
1037
|
}
|
|
1142
1038
|
const gitDir = path12.join(projectRoot, ".git");
|
|
@@ -1146,7 +1042,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1146
1042
|
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1147
1043
|
}
|
|
1148
1044
|
writeGitHookPreCommit(hooksDir);
|
|
1149
|
-
console.log(` ${
|
|
1045
|
+
console.log(` ${chalk5.green("\u2713")} .git/hooks/pre-commit`);
|
|
1150
1046
|
}
|
|
1151
1047
|
}
|
|
1152
1048
|
function writeGitHookPreCommit(hooksDir) {
|
|
@@ -1195,6 +1091,187 @@ npx viberails check --staged
|
|
|
1195
1091
|
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1196
1092
|
}
|
|
1197
1093
|
|
|
1094
|
+
// src/commands/init-wizard.ts
|
|
1095
|
+
import * as p from "@clack/prompts";
|
|
1096
|
+
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES3, LIBRARY_NAMES as LIBRARY_NAMES2, STYLING_NAMES as STYLING_NAMES3 } from "@viberails/types";
|
|
1097
|
+
import chalk8 from "chalk";
|
|
1098
|
+
|
|
1099
|
+
// src/display.ts
|
|
1100
|
+
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES2, LIBRARY_NAMES, STYLING_NAMES as STYLING_NAMES2 } from "@viberails/types";
|
|
1101
|
+
import chalk7 from "chalk";
|
|
1102
|
+
|
|
1103
|
+
// src/display-helpers.ts
|
|
1104
|
+
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
1105
|
+
function groupByRole(directories) {
|
|
1106
|
+
const map = /* @__PURE__ */ new Map();
|
|
1107
|
+
for (const dir of directories) {
|
|
1108
|
+
if (dir.role === "unknown") continue;
|
|
1109
|
+
const existing = map.get(dir.role);
|
|
1110
|
+
if (existing) {
|
|
1111
|
+
existing.dirs.push(dir);
|
|
1112
|
+
} else {
|
|
1113
|
+
map.set(dir.role, { dirs: [dir] });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const groups = [];
|
|
1117
|
+
for (const [role, { dirs }] of map) {
|
|
1118
|
+
const label = ROLE_DESCRIPTIONS[role] ?? role;
|
|
1119
|
+
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1120
|
+
groups.push({
|
|
1121
|
+
role,
|
|
1122
|
+
label,
|
|
1123
|
+
dirCount: dirs.length,
|
|
1124
|
+
totalFiles,
|
|
1125
|
+
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
return groups;
|
|
1129
|
+
}
|
|
1130
|
+
function formatSummary(stats, packageCount) {
|
|
1131
|
+
const parts = [];
|
|
1132
|
+
if (packageCount && packageCount > 1) {
|
|
1133
|
+
parts.push(`${packageCount} packages`);
|
|
1134
|
+
}
|
|
1135
|
+
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1136
|
+
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1137
|
+
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1138
|
+
return parts.join(" \xB7 ");
|
|
1139
|
+
}
|
|
1140
|
+
function formatRoleGroup(group) {
|
|
1141
|
+
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1142
|
+
if (group.singlePath) {
|
|
1143
|
+
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1144
|
+
}
|
|
1145
|
+
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1146
|
+
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/display-monorepo.ts
|
|
1150
|
+
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1151
|
+
import chalk6 from "chalk";
|
|
1152
|
+
|
|
1153
|
+
// src/display.ts
|
|
1154
|
+
function formatItem(item, nameMap) {
|
|
1155
|
+
const name = nameMap?.[item.name] ?? item.name;
|
|
1156
|
+
return item.version ? `${name} ${item.version}` : name;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/commands/init-wizard.ts
|
|
1160
|
+
var DEFAULT_WIZARD_RESULT = {
|
|
1161
|
+
enforcement: "warn",
|
|
1162
|
+
checks: {
|
|
1163
|
+
fileSize: true,
|
|
1164
|
+
naming: true,
|
|
1165
|
+
tests: true,
|
|
1166
|
+
boundaries: false
|
|
1167
|
+
},
|
|
1168
|
+
integration: ["pre-commit"]
|
|
1169
|
+
};
|
|
1170
|
+
async function runWizard(scanResult) {
|
|
1171
|
+
const isMonorepo = scanResult.packages.length > 1;
|
|
1172
|
+
displayScanSummary(scanResult);
|
|
1173
|
+
const enforcement = await p.select({
|
|
1174
|
+
message: "How strict should viberails be?",
|
|
1175
|
+
initialValue: "warn",
|
|
1176
|
+
options: [
|
|
1177
|
+
{ value: "warn", label: "Warn", hint: "show issues, never block commits" },
|
|
1178
|
+
{ value: "enforce", label: "Enforce", hint: "block commits with violations" }
|
|
1179
|
+
]
|
|
1180
|
+
});
|
|
1181
|
+
if (p.isCancel(enforcement)) {
|
|
1182
|
+
p.cancel("Setup cancelled.");
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const checkOptions = [
|
|
1186
|
+
{ value: "fileSize", label: "File size limit (300 lines)" },
|
|
1187
|
+
{ value: "naming", label: "File naming conventions" },
|
|
1188
|
+
{ value: "tests", label: "Missing test files" }
|
|
1189
|
+
];
|
|
1190
|
+
if (isMonorepo) {
|
|
1191
|
+
checkOptions.push({ value: "boundaries", label: "Import boundaries" });
|
|
1192
|
+
}
|
|
1193
|
+
const enabledChecks = await p.multiselect({
|
|
1194
|
+
message: "Which checks should viberails run?",
|
|
1195
|
+
options: checkOptions,
|
|
1196
|
+
initialValues: ["fileSize", "naming", "tests"],
|
|
1197
|
+
required: false
|
|
1198
|
+
});
|
|
1199
|
+
if (p.isCancel(enabledChecks)) {
|
|
1200
|
+
p.cancel("Setup cancelled.");
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
const checks = {
|
|
1204
|
+
fileSize: enabledChecks.includes("fileSize"),
|
|
1205
|
+
naming: enabledChecks.includes("naming"),
|
|
1206
|
+
tests: enabledChecks.includes("tests"),
|
|
1207
|
+
boundaries: enabledChecks.includes("boundaries")
|
|
1208
|
+
};
|
|
1209
|
+
const integrationOptions = [
|
|
1210
|
+
{ value: "pre-commit", label: "Git pre-commit hook", hint: "runs on every commit" },
|
|
1211
|
+
{
|
|
1212
|
+
value: "claude-hook",
|
|
1213
|
+
label: "Claude Code hook",
|
|
1214
|
+
hint: "checks files as Claude edits them"
|
|
1215
|
+
},
|
|
1216
|
+
{ value: "context-only", label: "Context files only", hint: "no hooks" }
|
|
1217
|
+
];
|
|
1218
|
+
const integration = await p.multiselect({
|
|
1219
|
+
message: "Where should checks run?",
|
|
1220
|
+
options: integrationOptions,
|
|
1221
|
+
initialValues: ["pre-commit"],
|
|
1222
|
+
required: true
|
|
1223
|
+
});
|
|
1224
|
+
if (p.isCancel(integration)) {
|
|
1225
|
+
p.cancel("Setup cancelled.");
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
const finalIntegration = integration.includes("context-only") ? ["context-only"] : integration;
|
|
1229
|
+
return {
|
|
1230
|
+
enforcement,
|
|
1231
|
+
checks,
|
|
1232
|
+
integration: finalIntegration
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function displayScanSummary(scanResult) {
|
|
1236
|
+
const { stack } = scanResult;
|
|
1237
|
+
const parts = [];
|
|
1238
|
+
if (stack.framework) parts.push(formatItem(stack.framework, FRAMEWORK_NAMES3));
|
|
1239
|
+
parts.push(formatItem(stack.language));
|
|
1240
|
+
if (stack.styling) parts.push(formatItem(stack.styling, STYLING_NAMES3));
|
|
1241
|
+
if (stack.backend) parts.push(formatItem(stack.backend, FRAMEWORK_NAMES3));
|
|
1242
|
+
p.log.info(`${chalk8.bold("Stack:")} ${parts.join(", ")}`);
|
|
1243
|
+
if (stack.linter || stack.formatter || stack.testRunner || stack.packageManager) {
|
|
1244
|
+
const tools = [];
|
|
1245
|
+
if (stack.linter) tools.push(formatItem(stack.linter));
|
|
1246
|
+
if (stack.formatter && stack.formatter !== stack.linter)
|
|
1247
|
+
tools.push(formatItem(stack.formatter));
|
|
1248
|
+
if (stack.testRunner) tools.push(formatItem(stack.testRunner));
|
|
1249
|
+
if (stack.packageManager) tools.push(formatItem(stack.packageManager));
|
|
1250
|
+
p.log.info(`${chalk8.bold("Tools:")} ${tools.join(", ")}`);
|
|
1251
|
+
}
|
|
1252
|
+
if (stack.libraries.length > 0) {
|
|
1253
|
+
const libs = stack.libraries.map((lib) => formatItem(lib, LIBRARY_NAMES2)).join(", ");
|
|
1254
|
+
p.log.info(`${chalk8.bold("Libraries:")} ${libs}`);
|
|
1255
|
+
}
|
|
1256
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1257
|
+
if (groups.length > 0) {
|
|
1258
|
+
const structParts = groups.map((g) => formatRoleGroup(g));
|
|
1259
|
+
p.log.info(`${chalk8.bold("Structure:")} ${structParts.join(", ")}`);
|
|
1260
|
+
}
|
|
1261
|
+
const conventionEntries = Object.entries(scanResult.conventions).filter(
|
|
1262
|
+
([, c]) => c.confidence !== "low"
|
|
1263
|
+
);
|
|
1264
|
+
if (conventionEntries.length > 0) {
|
|
1265
|
+
const convParts = conventionEntries.map(([, c]) => {
|
|
1266
|
+
const pct = Math.round(c.consistency);
|
|
1267
|
+
return `${c.value} (${pct}%)`;
|
|
1268
|
+
});
|
|
1269
|
+
p.log.info(`${chalk8.bold("Conventions:")} ${convParts.join(", ")}`);
|
|
1270
|
+
}
|
|
1271
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1272
|
+
p.log.info(`${chalk8.bold("Summary:")} ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1198
1275
|
// src/commands/init.ts
|
|
1199
1276
|
var CONFIG_FILE4 = "viberails.config.json";
|
|
1200
1277
|
function filterHighConfidence(conventions) {
|
|
@@ -1209,6 +1286,13 @@ function filterHighConfidence(conventions) {
|
|
|
1209
1286
|
}
|
|
1210
1287
|
return filtered;
|
|
1211
1288
|
}
|
|
1289
|
+
function applyWizardResult(config, wizard) {
|
|
1290
|
+
config.enforcement = wizard.enforcement;
|
|
1291
|
+
if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
|
|
1292
|
+
config.rules.enforceNaming = wizard.checks.naming;
|
|
1293
|
+
config.rules.requireTests = wizard.checks.tests;
|
|
1294
|
+
config.rules.enforceBoundaries = wizard.checks.boundaries;
|
|
1295
|
+
}
|
|
1212
1296
|
async function initCommand(options, cwd) {
|
|
1213
1297
|
const startDir = cwd ?? process.cwd();
|
|
1214
1298
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1220,64 +1304,69 @@ async function initCommand(options, cwd) {
|
|
|
1220
1304
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1221
1305
|
if (fs12.existsSync(configPath)) {
|
|
1222
1306
|
console.log(
|
|
1223
|
-
|
|
1307
|
+
chalk9.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk9.cyan("viberails sync") + " to update the generated files."
|
|
1224
1308
|
);
|
|
1225
1309
|
return;
|
|
1226
1310
|
}
|
|
1227
|
-
|
|
1311
|
+
p2.intro("viberails");
|
|
1312
|
+
const s = p2.spinner();
|
|
1313
|
+
s.start("Scanning project...");
|
|
1228
1314
|
const scanResult = await scan(projectRoot);
|
|
1229
|
-
|
|
1315
|
+
s.stop("Scan complete");
|
|
1230
1316
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1317
|
+
p2.log.warn(
|
|
1318
|
+
`No source files detected. viberails will generate context with minimal content.
|
|
1319
|
+
Run ${chalk9.cyan("viberails sync")} after adding source files.`
|
|
1233
1320
|
);
|
|
1234
1321
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1322
|
+
let wizard;
|
|
1323
|
+
if (options.yes) {
|
|
1324
|
+
wizard = { ...DEFAULT_WIZARD_RESULT };
|
|
1325
|
+
} else {
|
|
1326
|
+
const result = await runWizard(scanResult);
|
|
1327
|
+
if (!result) return;
|
|
1328
|
+
wizard = result;
|
|
1241
1329
|
}
|
|
1242
1330
|
const config = generateConfig(scanResult);
|
|
1243
1331
|
if (options.yes) {
|
|
1244
1332
|
config.conventions = filterHighConfidence(config.conventions);
|
|
1245
1333
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
console.log(` ${chalk8.green("\u2713")} Inferred ${inferred.length} boundary rules`);
|
|
1261
|
-
}
|
|
1334
|
+
applyWizardResult(config, wizard);
|
|
1335
|
+
if (wizard.checks.boundaries && config.workspace && config.workspace.packages.length > 0) {
|
|
1336
|
+
s.start("Inferring boundary rules...");
|
|
1337
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1338
|
+
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
1339
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
1340
|
+
const inferred = inferBoundaries(graph);
|
|
1341
|
+
const ruleCount = Object.values(inferred).reduce((sum, denied) => sum + denied.length, 0);
|
|
1342
|
+
if (ruleCount > 0) {
|
|
1343
|
+
config.boundaries = inferred;
|
|
1344
|
+
s.stop(`Inferred ${ruleCount} boundary rules`);
|
|
1345
|
+
} else {
|
|
1346
|
+
s.stop("No boundary rules could be inferred");
|
|
1347
|
+
config.rules.enforceBoundaries = false;
|
|
1262
1348
|
}
|
|
1263
1349
|
}
|
|
1264
1350
|
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1265
1351
|
`);
|
|
1266
1352
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1267
1353
|
updateGitignore(projectRoot);
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
${
|
|
1276
|
-
console.log(`
|
|
1277
|
-
console.log(
|
|
1278
|
-
|
|
1354
|
+
if (wizard.integration.includes("pre-commit")) {
|
|
1355
|
+
setupPreCommitHook(projectRoot);
|
|
1356
|
+
}
|
|
1357
|
+
if (wizard.integration.includes("claude-hook")) {
|
|
1358
|
+
setupClaudeCodeHook(projectRoot);
|
|
1359
|
+
}
|
|
1360
|
+
p2.log.success(`${chalk9.bold("Created:")}`);
|
|
1361
|
+
console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1362
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
|
|
1363
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
|
|
1364
|
+
p2.outro(
|
|
1365
|
+
`${chalk9.bold("Next steps:")}
|
|
1366
|
+
1. Review ${chalk9.cyan("viberails.config.json")} and adjust rules
|
|
1367
|
+
2. Commit ${chalk9.cyan("viberails.config.json")} and ${chalk9.cyan(".viberails/context.md")}
|
|
1368
|
+
3. Run ${chalk9.cyan("viberails check")} to verify your project passes`
|
|
1279
1369
|
);
|
|
1280
|
-
console.log(` 3. Run ${chalk8.cyan("viberails check")} to verify your project passes`);
|
|
1281
1370
|
}
|
|
1282
1371
|
function updateGitignore(projectRoot) {
|
|
1283
1372
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
@@ -1297,7 +1386,7 @@ import * as fs13 from "fs";
|
|
|
1297
1386
|
import * as path14 from "path";
|
|
1298
1387
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1299
1388
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1300
|
-
import
|
|
1389
|
+
import chalk10 from "chalk";
|
|
1301
1390
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1302
1391
|
async function syncCommand(cwd) {
|
|
1303
1392
|
const startDir = cwd ?? process.cwd();
|
|
@@ -1309,21 +1398,21 @@ async function syncCommand(cwd) {
|
|
|
1309
1398
|
}
|
|
1310
1399
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1311
1400
|
const existing = await loadConfig4(configPath);
|
|
1312
|
-
console.log(
|
|
1401
|
+
console.log(chalk10.dim("Scanning project..."));
|
|
1313
1402
|
const scanResult = await scan2(projectRoot);
|
|
1314
1403
|
const merged = mergeConfig(existing, scanResult);
|
|
1315
1404
|
fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
|
|
1316
1405
|
`);
|
|
1317
1406
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1318
1407
|
console.log(`
|
|
1319
|
-
${
|
|
1320
|
-
console.log(` ${
|
|
1321
|
-
console.log(` ${
|
|
1322
|
-
console.log(` ${
|
|
1408
|
+
${chalk10.bold("Synced:")}`);
|
|
1409
|
+
console.log(` ${chalk10.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
|
|
1410
|
+
console.log(` ${chalk10.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1411
|
+
console.log(` ${chalk10.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1323
1412
|
}
|
|
1324
1413
|
|
|
1325
1414
|
// src/index.ts
|
|
1326
|
-
var VERSION = "0.
|
|
1415
|
+
var VERSION = "0.3.0";
|
|
1327
1416
|
var program = new Command();
|
|
1328
1417
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1329
1418
|
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) => {
|
|
@@ -1331,7 +1420,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
1331
1420
|
await initCommand(options);
|
|
1332
1421
|
} catch (err) {
|
|
1333
1422
|
const message = err instanceof Error ? err.message : String(err);
|
|
1334
|
-
console.error(`${
|
|
1423
|
+
console.error(`${chalk11.red("Error:")} ${message}`);
|
|
1335
1424
|
process.exit(1);
|
|
1336
1425
|
}
|
|
1337
1426
|
});
|
|
@@ -1340,30 +1429,32 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1340
1429
|
await syncCommand();
|
|
1341
1430
|
} catch (err) {
|
|
1342
1431
|
const message = err instanceof Error ? err.message : String(err);
|
|
1343
|
-
console.error(`${
|
|
1432
|
+
console.error(`${chalk11.red("Error:")} ${message}`);
|
|
1344
1433
|
process.exit(1);
|
|
1345
1434
|
}
|
|
1346
1435
|
});
|
|
1347
|
-
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").
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1436
|
+
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).action(
|
|
1437
|
+
async (options) => {
|
|
1438
|
+
try {
|
|
1439
|
+
const exitCode = await checkCommand({
|
|
1440
|
+
...options,
|
|
1441
|
+
noBoundaries: options.boundaries === false
|
|
1442
|
+
});
|
|
1443
|
+
process.exit(exitCode);
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1446
|
+
console.error(`${chalk11.red("Error:")} ${message}`);
|
|
1447
|
+
process.exit(1);
|
|
1448
|
+
}
|
|
1358
1449
|
}
|
|
1359
|
-
|
|
1450
|
+
);
|
|
1360
1451
|
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) => {
|
|
1361
1452
|
try {
|
|
1362
1453
|
const exitCode = await fixCommand(options);
|
|
1363
1454
|
process.exit(exitCode);
|
|
1364
1455
|
} catch (err) {
|
|
1365
1456
|
const message = err instanceof Error ? err.message : String(err);
|
|
1366
|
-
console.error(`${
|
|
1457
|
+
console.error(`${chalk11.red("Error:")} ${message}`);
|
|
1367
1458
|
process.exit(1);
|
|
1368
1459
|
}
|
|
1369
1460
|
});
|
|
@@ -1372,7 +1463,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
1372
1463
|
await boundariesCommand(options);
|
|
1373
1464
|
} catch (err) {
|
|
1374
1465
|
const message = err instanceof Error ? err.message : String(err);
|
|
1375
|
-
console.error(`${
|
|
1466
|
+
console.error(`${chalk11.red("Error:")} ${message}`);
|
|
1376
1467
|
process.exit(1);
|
|
1377
1468
|
}
|
|
1378
1469
|
});
|