viberails 0.3.2 → 0.4.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 +460 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +471 -194
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -56,50 +56,115 @@ async function promptInitDecision() {
|
|
|
56
56
|
assertNotCancelled(result);
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
|
-
async function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
assertNotCancelled(maxFileLinesResult);
|
|
70
|
-
const requireTestsResult = await clack.confirm({
|
|
71
|
-
message: "Require matching test files for source files?",
|
|
72
|
-
initialValue: defaults.requireTests
|
|
73
|
-
});
|
|
74
|
-
assertNotCancelled(requireTestsResult);
|
|
75
|
-
const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
|
|
76
|
-
const enforceNamingResult = await clack.confirm({
|
|
77
|
-
message: namingLabel,
|
|
78
|
-
initialValue: defaults.enforceNaming
|
|
79
|
-
});
|
|
80
|
-
assertNotCancelled(enforceNamingResult);
|
|
81
|
-
const enforcementResult = await clack.select({
|
|
82
|
-
message: "Enforcement mode",
|
|
83
|
-
options: [
|
|
59
|
+
async function promptRuleMenu(defaults) {
|
|
60
|
+
const state = { ...defaults };
|
|
61
|
+
while (true) {
|
|
62
|
+
const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
|
|
63
|
+
const enforcementHint = state.enforcement === "warn" ? "warn \u2014 violations shown but commits allowed" : "enforce \u2014 commits blocked on violation";
|
|
64
|
+
const options = [
|
|
65
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
84
66
|
{
|
|
85
|
-
value: "
|
|
86
|
-
label: "
|
|
87
|
-
hint:
|
|
67
|
+
value: "requireTests",
|
|
68
|
+
label: "Require test files",
|
|
69
|
+
hint: state.requireTests ? "yes" : "no"
|
|
88
70
|
},
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
{ value: "enforceNaming", label: "Enforce file naming", hint: namingHint },
|
|
72
|
+
{ value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
|
|
73
|
+
];
|
|
74
|
+
if (state.packageOverrides && state.packageOverrides.length > 0) {
|
|
75
|
+
const count = state.packageOverrides.length;
|
|
76
|
+
options.push({
|
|
77
|
+
value: "packageOverrides",
|
|
78
|
+
label: "Per-package overrides",
|
|
79
|
+
hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
options.push({ value: "done", label: "Done" });
|
|
83
|
+
const choice = await clack.select({
|
|
84
|
+
message: "Customize rules",
|
|
85
|
+
options
|
|
86
|
+
});
|
|
87
|
+
assertNotCancelled(choice);
|
|
88
|
+
if (choice === "done") break;
|
|
89
|
+
if (choice === "packageOverrides" && state.packageOverrides) {
|
|
90
|
+
const lines = state.packageOverrides.map((pkg) => {
|
|
91
|
+
const diffs = [];
|
|
92
|
+
if (pkg.conventions) {
|
|
93
|
+
for (const [key, val] of Object.entries(pkg.conventions)) {
|
|
94
|
+
const v = typeof val === "string" ? val : val?.value;
|
|
95
|
+
if (v) diffs.push(`${key}: ${v}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (pkg.stack) {
|
|
99
|
+
for (const [key, val] of Object.entries(pkg.stack)) {
|
|
100
|
+
if (val) diffs.push(`${key}: ${val}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return `${pkg.path}
|
|
104
|
+
${diffs.join(", ") || "minor differences"}`;
|
|
105
|
+
});
|
|
106
|
+
clack.note(
|
|
107
|
+
`${lines.join("\n\n")}
|
|
108
|
+
|
|
109
|
+
Edit the "packages" section in viberails.config.json to adjust.`,
|
|
110
|
+
"Per-package overrides"
|
|
111
|
+
);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (choice === "maxFileLines") {
|
|
115
|
+
const result = await clack.text({
|
|
116
|
+
message: "Maximum lines per source file?",
|
|
117
|
+
initialValue: String(state.maxFileLines),
|
|
118
|
+
validate: (v) => {
|
|
119
|
+
const n = Number.parseInt(v, 10);
|
|
120
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
assertNotCancelled(result);
|
|
124
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
125
|
+
}
|
|
126
|
+
if (choice === "requireTests") {
|
|
127
|
+
const result = await clack.confirm({
|
|
128
|
+
message: "Require matching test files for source files?",
|
|
129
|
+
initialValue: state.requireTests
|
|
130
|
+
});
|
|
131
|
+
assertNotCancelled(result);
|
|
132
|
+
state.requireTests = result;
|
|
133
|
+
}
|
|
134
|
+
if (choice === "enforceNaming") {
|
|
135
|
+
const result = await clack.confirm({
|
|
136
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
137
|
+
initialValue: state.enforceNaming
|
|
138
|
+
});
|
|
139
|
+
assertNotCancelled(result);
|
|
140
|
+
state.enforceNaming = result;
|
|
141
|
+
}
|
|
142
|
+
if (choice === "enforcement") {
|
|
143
|
+
const result = await clack.select({
|
|
144
|
+
message: "Enforcement mode",
|
|
145
|
+
options: [
|
|
146
|
+
{
|
|
147
|
+
value: "warn",
|
|
148
|
+
label: "warn",
|
|
149
|
+
hint: "show violations but don't block commits (recommended)"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
value: "enforce",
|
|
153
|
+
label: "enforce",
|
|
154
|
+
hint: "block commits with violations"
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
initialValue: state.enforcement
|
|
158
|
+
});
|
|
159
|
+
assertNotCancelled(result);
|
|
160
|
+
state.enforcement = result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
98
163
|
return {
|
|
99
|
-
maxFileLines:
|
|
100
|
-
requireTests:
|
|
101
|
-
enforceNaming:
|
|
102
|
-
enforcement:
|
|
164
|
+
maxFileLines: state.maxFileLines,
|
|
165
|
+
requireTests: state.requireTests,
|
|
166
|
+
enforceNaming: state.enforceNaming,
|
|
167
|
+
enforcement: state.enforcement
|
|
103
168
|
};
|
|
104
169
|
}
|
|
105
170
|
async function promptIntegrations(hookManager) {
|
|
@@ -116,15 +181,21 @@ async function promptIntegrations(hookManager) {
|
|
|
116
181
|
value: "claude",
|
|
117
182
|
label: "Claude Code hook",
|
|
118
183
|
hint: "checks files when Claude edits them"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
value: "claudeMd",
|
|
187
|
+
label: "CLAUDE.md reference",
|
|
188
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
119
189
|
}
|
|
120
190
|
],
|
|
121
|
-
initialValues: ["preCommit", "claude"],
|
|
191
|
+
initialValues: ["preCommit", "claude", "claudeMd"],
|
|
122
192
|
required: false
|
|
123
193
|
});
|
|
124
194
|
assertNotCancelled(result);
|
|
125
195
|
return {
|
|
126
196
|
preCommitHook: result.includes("preCommit"),
|
|
127
|
-
claudeCodeHook: result.includes("claude")
|
|
197
|
+
claudeCodeHook: result.includes("claude"),
|
|
198
|
+
claudeMdRef: result.includes("claudeMd")
|
|
128
199
|
};
|
|
129
200
|
}
|
|
130
201
|
|
|
@@ -182,22 +253,21 @@ async function boundariesCommand(options, cwd) {
|
|
|
182
253
|
displayRules(config);
|
|
183
254
|
}
|
|
184
255
|
function displayRules(config) {
|
|
185
|
-
if (!config.boundaries || config.boundaries.length === 0) {
|
|
256
|
+
if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
|
|
186
257
|
console.log(chalk.yellow("No boundary rules configured."));
|
|
187
258
|
console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
188
259
|
return;
|
|
189
260
|
}
|
|
190
|
-
const
|
|
191
|
-
const
|
|
261
|
+
const { deny } = config.boundaries;
|
|
262
|
+
const sources = Object.keys(deny).filter((k) => deny[k].length > 0);
|
|
263
|
+
const totalRules = sources.reduce((sum, k) => sum + deny[k].length, 0);
|
|
192
264
|
console.log(`
|
|
193
|
-
${chalk.bold(`Boundary rules (${
|
|
265
|
+
${chalk.bold(`Boundary rules (${totalRules} deny rules):`)}
|
|
194
266
|
`);
|
|
195
|
-
for (const
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
200
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
267
|
+
for (const source of sources) {
|
|
268
|
+
for (const target of deny[source]) {
|
|
269
|
+
console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
|
|
270
|
+
}
|
|
201
271
|
}
|
|
202
272
|
console.log(
|
|
203
273
|
`
|
|
@@ -214,24 +284,22 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
214
284
|
});
|
|
215
285
|
console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
216
286
|
const inferred = inferBoundaries(graph);
|
|
217
|
-
|
|
287
|
+
const sources = Object.keys(inferred.deny).filter((k) => inferred.deny[k].length > 0);
|
|
288
|
+
const totalRules = sources.reduce((sum, k) => sum + inferred.deny[k].length, 0);
|
|
289
|
+
if (totalRules === 0) {
|
|
218
290
|
console.log(chalk.yellow("No boundary rules could be inferred."));
|
|
219
291
|
return;
|
|
220
292
|
}
|
|
221
|
-
const allow = inferred.filter((r) => r.allow);
|
|
222
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
223
293
|
console.log(`
|
|
224
294
|
${chalk.bold("Inferred boundary rules:")}
|
|
225
295
|
`);
|
|
226
|
-
for (const
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
231
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
296
|
+
for (const source of sources) {
|
|
297
|
+
for (const target of inferred.deny[source]) {
|
|
298
|
+
console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
|
|
299
|
+
}
|
|
232
300
|
}
|
|
233
301
|
console.log(`
|
|
234
|
-
${
|
|
302
|
+
${totalRules} denied`);
|
|
235
303
|
console.log("");
|
|
236
304
|
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
237
305
|
if (shouldSave) {
|
|
@@ -239,7 +307,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
239
307
|
config.rules.enforceBoundaries = true;
|
|
240
308
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
241
309
|
`);
|
|
242
|
-
console.log(`${chalk.green("\u2713")} Saved ${
|
|
310
|
+
console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
|
|
243
311
|
}
|
|
244
312
|
}
|
|
245
313
|
async function showGraph(projectRoot, config) {
|
|
@@ -557,7 +625,13 @@ async function checkCommand(options, cwd) {
|
|
|
557
625
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
558
626
|
}
|
|
559
627
|
if (filesToCheck.length === 0) {
|
|
560
|
-
|
|
628
|
+
if (options.format === "json") {
|
|
629
|
+
console.log(
|
|
630
|
+
JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
|
|
631
|
+
);
|
|
632
|
+
} else {
|
|
633
|
+
console.log(`${chalk2.green("\u2713")} No files to check.`);
|
|
634
|
+
}
|
|
561
635
|
return 0;
|
|
562
636
|
}
|
|
563
637
|
const violations = [];
|
|
@@ -598,7 +672,7 @@ async function checkCommand(options, cwd) {
|
|
|
598
672
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
599
673
|
violations.push(...testViolations);
|
|
600
674
|
}
|
|
601
|
-
if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
|
|
675
|
+
if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
|
|
602
676
|
const startTime = Date.now();
|
|
603
677
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
604
678
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -614,12 +688,14 @@ async function checkCommand(options, cwd) {
|
|
|
614
688
|
violations.push({
|
|
615
689
|
file: relFile,
|
|
616
690
|
rule: "boundary-violation",
|
|
617
|
-
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}
|
|
691
|
+
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}`,
|
|
618
692
|
severity
|
|
619
693
|
});
|
|
620
694
|
}
|
|
621
695
|
const elapsed = Date.now() - startTime;
|
|
622
|
-
|
|
696
|
+
if (options.format !== "json") {
|
|
697
|
+
console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
698
|
+
}
|
|
623
699
|
}
|
|
624
700
|
if (options.format === "json") {
|
|
625
701
|
console.log(
|
|
@@ -646,8 +722,54 @@ async function checkCommand(options, cwd) {
|
|
|
646
722
|
return 0;
|
|
647
723
|
}
|
|
648
724
|
|
|
725
|
+
// src/commands/check-hook.ts
|
|
726
|
+
import * as fs7 from "fs";
|
|
727
|
+
function parseHookFilePath(input) {
|
|
728
|
+
try {
|
|
729
|
+
if (!input.trim()) return void 0;
|
|
730
|
+
const parsed = JSON.parse(input);
|
|
731
|
+
return parsed?.tool_input?.file_path ?? void 0;
|
|
732
|
+
} catch {
|
|
733
|
+
return void 0;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function readStdin() {
|
|
737
|
+
try {
|
|
738
|
+
return fs7.readFileSync(0, "utf-8");
|
|
739
|
+
} catch {
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function hookCheckCommand(cwd) {
|
|
744
|
+
try {
|
|
745
|
+
const filePath = parseHookFilePath(readStdin());
|
|
746
|
+
if (!filePath) return 0;
|
|
747
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
748
|
+
let captured = "";
|
|
749
|
+
process.stdout.write = (chunk) => {
|
|
750
|
+
captured += typeof chunk === "string" ? chunk : chunk.toString();
|
|
751
|
+
return true;
|
|
752
|
+
};
|
|
753
|
+
try {
|
|
754
|
+
await checkCommand({ files: [filePath], format: "json" }, cwd);
|
|
755
|
+
} finally {
|
|
756
|
+
process.stdout.write = originalWrite;
|
|
757
|
+
}
|
|
758
|
+
if (!captured.trim()) return 0;
|
|
759
|
+
const result = JSON.parse(captured);
|
|
760
|
+
if (result.violations?.length > 0) {
|
|
761
|
+
process.stderr.write(`${captured.trim()}
|
|
762
|
+
`);
|
|
763
|
+
return 2;
|
|
764
|
+
}
|
|
765
|
+
return 0;
|
|
766
|
+
} catch {
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
649
771
|
// src/commands/fix.ts
|
|
650
|
-
import * as
|
|
772
|
+
import * as fs10 from "fs";
|
|
651
773
|
import * as path10 from "path";
|
|
652
774
|
import { loadConfig as loadConfig3 } from "@viberails/config";
|
|
653
775
|
import chalk4 from "chalk";
|
|
@@ -790,7 +912,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
790
912
|
}
|
|
791
913
|
|
|
792
914
|
// src/commands/fix-naming.ts
|
|
793
|
-
import * as
|
|
915
|
+
import * as fs8 from "fs";
|
|
794
916
|
import * as path8 from "path";
|
|
795
917
|
|
|
796
918
|
// src/commands/convert-name.ts
|
|
@@ -852,12 +974,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
852
974
|
const newRelPath = path8.join(dir, newFilename);
|
|
853
975
|
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
854
976
|
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
855
|
-
if (
|
|
977
|
+
if (fs8.existsSync(newAbsPath)) return null;
|
|
856
978
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
857
979
|
}
|
|
858
980
|
function executeRename(rename) {
|
|
859
|
-
if (
|
|
860
|
-
|
|
981
|
+
if (fs8.existsSync(rename.newAbsPath)) return false;
|
|
982
|
+
fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
861
983
|
return true;
|
|
862
984
|
}
|
|
863
985
|
function deduplicateRenames(renames) {
|
|
@@ -872,7 +994,7 @@ function deduplicateRenames(renames) {
|
|
|
872
994
|
}
|
|
873
995
|
|
|
874
996
|
// src/commands/fix-tests.ts
|
|
875
|
-
import * as
|
|
997
|
+
import * as fs9 from "fs";
|
|
876
998
|
import * as path9 from "path";
|
|
877
999
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
878
1000
|
const { testPattern } = config.structure;
|
|
@@ -883,7 +1005,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
883
1005
|
const testFilename = `${stem}${testSuffix}`;
|
|
884
1006
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
885
1007
|
const testAbsPath = path9.join(dir, testFilename);
|
|
886
|
-
if (
|
|
1008
|
+
if (fs9.existsSync(testAbsPath)) return null;
|
|
887
1009
|
return {
|
|
888
1010
|
path: path9.relative(projectRoot, testAbsPath),
|
|
889
1011
|
absPath: testAbsPath,
|
|
@@ -897,8 +1019,8 @@ function writeTestStub(stub, config) {
|
|
|
897
1019
|
it.todo('add tests');
|
|
898
1020
|
});
|
|
899
1021
|
`;
|
|
900
|
-
|
|
901
|
-
|
|
1022
|
+
fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
1023
|
+
fs9.writeFileSync(stub.absPath, content);
|
|
902
1024
|
}
|
|
903
1025
|
|
|
904
1026
|
// src/commands/fix.ts
|
|
@@ -911,7 +1033,7 @@ async function fixCommand(options, cwd) {
|
|
|
911
1033
|
return 1;
|
|
912
1034
|
}
|
|
913
1035
|
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
914
|
-
if (!
|
|
1036
|
+
if (!fs10.existsSync(configPath)) {
|
|
915
1037
|
console.error(
|
|
916
1038
|
`${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
917
1039
|
);
|
|
@@ -975,13 +1097,13 @@ async function fixCommand(options, cwd) {
|
|
|
975
1097
|
}
|
|
976
1098
|
let importUpdateCount = 0;
|
|
977
1099
|
if (renameCount > 0) {
|
|
978
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1100
|
+
const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
|
|
979
1101
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
980
1102
|
importUpdateCount = updates.length;
|
|
981
1103
|
}
|
|
982
1104
|
let stubCount = 0;
|
|
983
1105
|
for (const stub of testStubs) {
|
|
984
|
-
if (!
|
|
1106
|
+
if (!fs10.existsSync(stub.absPath)) {
|
|
985
1107
|
writeTestStub(stub, config);
|
|
986
1108
|
stubCount++;
|
|
987
1109
|
}
|
|
@@ -1002,16 +1124,21 @@ async function fixCommand(options, cwd) {
|
|
|
1002
1124
|
}
|
|
1003
1125
|
|
|
1004
1126
|
// src/commands/init.ts
|
|
1005
|
-
import * as
|
|
1127
|
+
import * as fs13 from "fs";
|
|
1006
1128
|
import * as path13 from "path";
|
|
1007
1129
|
import * as clack2 from "@clack/prompts";
|
|
1008
1130
|
import { generateConfig } from "@viberails/config";
|
|
1009
1131
|
import { scan } from "@viberails/scanner";
|
|
1010
1132
|
import chalk8 from "chalk";
|
|
1011
1133
|
|
|
1012
|
-
// src/display.ts
|
|
1013
|
-
import {
|
|
1014
|
-
|
|
1134
|
+
// src/display-text.ts
|
|
1135
|
+
import {
|
|
1136
|
+
CONVENTION_LABELS as CONVENTION_LABELS2,
|
|
1137
|
+
FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
|
|
1138
|
+
LIBRARY_NAMES as LIBRARY_NAMES2,
|
|
1139
|
+
ORM_NAMES as ORM_NAMES2,
|
|
1140
|
+
STYLING_NAMES as STYLING_NAMES3
|
|
1141
|
+
} from "@viberails/types";
|
|
1015
1142
|
|
|
1016
1143
|
// src/display-helpers.ts
|
|
1017
1144
|
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
@@ -1062,6 +1189,16 @@ function formatRoleGroup(group) {
|
|
|
1062
1189
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1063
1190
|
}
|
|
1064
1191
|
|
|
1192
|
+
// src/display.ts
|
|
1193
|
+
import {
|
|
1194
|
+
CONVENTION_LABELS,
|
|
1195
|
+
FRAMEWORK_NAMES as FRAMEWORK_NAMES2,
|
|
1196
|
+
LIBRARY_NAMES,
|
|
1197
|
+
ORM_NAMES,
|
|
1198
|
+
STYLING_NAMES as STYLING_NAMES2
|
|
1199
|
+
} from "@viberails/types";
|
|
1200
|
+
import chalk6 from "chalk";
|
|
1201
|
+
|
|
1065
1202
|
// src/display-monorepo.ts
|
|
1066
1203
|
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1067
1204
|
import chalk5 from "chalk";
|
|
@@ -1171,12 +1308,6 @@ function formatMonorepoResultsText(scanResult, config) {
|
|
|
1171
1308
|
}
|
|
1172
1309
|
|
|
1173
1310
|
// src/display.ts
|
|
1174
|
-
var CONVENTION_LABELS = {
|
|
1175
|
-
fileNaming: "File naming",
|
|
1176
|
-
componentNaming: "Component naming",
|
|
1177
|
-
hookNaming: "Hook naming",
|
|
1178
|
-
importAlias: "Import alias"
|
|
1179
|
-
};
|
|
1180
1311
|
function formatItem(item, nameMap) {
|
|
1181
1312
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1182
1313
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1312,6 +1443,11 @@ function displayRulesPreview(config) {
|
|
|
1312
1443
|
}
|
|
1313
1444
|
console.log("");
|
|
1314
1445
|
}
|
|
1446
|
+
|
|
1447
|
+
// src/display-text.ts
|
|
1448
|
+
function getConventionStr2(cv) {
|
|
1449
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1450
|
+
}
|
|
1315
1451
|
function plainConfidenceLabel(convention) {
|
|
1316
1452
|
const pct = Math.round(convention.consistency);
|
|
1317
1453
|
if (convention.confidence === "high") {
|
|
@@ -1327,7 +1463,7 @@ function formatConventionsText(scanResult) {
|
|
|
1327
1463
|
lines.push("Conventions:");
|
|
1328
1464
|
for (const [key, convention] of conventionEntries) {
|
|
1329
1465
|
if (convention.confidence === "low") continue;
|
|
1330
|
-
const label =
|
|
1466
|
+
const label = CONVENTION_LABELS2[key] ?? key;
|
|
1331
1467
|
if (scanResult.packages.length > 1) {
|
|
1332
1468
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1333
1469
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1361,7 +1497,7 @@ function formatRulesText(config) {
|
|
|
1361
1497
|
lines.push(" \u2022 Require test files: no");
|
|
1362
1498
|
}
|
|
1363
1499
|
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1364
|
-
lines.push(` \u2022 Enforce file naming: ${
|
|
1500
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
|
|
1365
1501
|
} else {
|
|
1366
1502
|
lines.push(" \u2022 Enforce file naming: no");
|
|
1367
1503
|
}
|
|
@@ -1376,17 +1512,17 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1376
1512
|
const { stack } = scanResult;
|
|
1377
1513
|
lines.push("Detected:");
|
|
1378
1514
|
if (stack.framework) {
|
|
1379
|
-
lines.push(` \u2713 ${formatItem(stack.framework,
|
|
1515
|
+
lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
|
|
1380
1516
|
}
|
|
1381
1517
|
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1382
1518
|
if (stack.styling) {
|
|
1383
|
-
lines.push(` \u2713 ${formatItem(stack.styling,
|
|
1519
|
+
lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
|
|
1384
1520
|
}
|
|
1385
1521
|
if (stack.backend) {
|
|
1386
|
-
lines.push(` \u2713 ${formatItem(stack.backend,
|
|
1522
|
+
lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
|
|
1387
1523
|
}
|
|
1388
1524
|
if (stack.orm) {
|
|
1389
|
-
lines.push(` \u2713 ${formatItem(stack.orm,
|
|
1525
|
+
lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
|
|
1390
1526
|
}
|
|
1391
1527
|
const secondaryParts = [];
|
|
1392
1528
|
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
@@ -1398,7 +1534,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1398
1534
|
}
|
|
1399
1535
|
if (stack.libraries.length > 0) {
|
|
1400
1536
|
for (const lib of stack.libraries) {
|
|
1401
|
-
lines.push(` \u2713 ${formatItem(lib,
|
|
1537
|
+
lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
|
|
1402
1538
|
}
|
|
1403
1539
|
}
|
|
1404
1540
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1422,7 +1558,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1422
1558
|
}
|
|
1423
1559
|
|
|
1424
1560
|
// src/utils/write-generated-files.ts
|
|
1425
|
-
import * as
|
|
1561
|
+
import * as fs11 from "fs";
|
|
1426
1562
|
import * as path11 from "path";
|
|
1427
1563
|
import { generateContext } from "@viberails/context";
|
|
1428
1564
|
var CONTEXT_DIR = ".viberails";
|
|
@@ -1431,12 +1567,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
|
|
|
1431
1567
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1432
1568
|
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1433
1569
|
try {
|
|
1434
|
-
if (!
|
|
1435
|
-
|
|
1570
|
+
if (!fs11.existsSync(contextDir)) {
|
|
1571
|
+
fs11.mkdirSync(contextDir, { recursive: true });
|
|
1436
1572
|
}
|
|
1437
1573
|
const context = generateContext(config);
|
|
1438
|
-
|
|
1439
|
-
|
|
1574
|
+
fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1575
|
+
fs11.writeFileSync(
|
|
1440
1576
|
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1441
1577
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1442
1578
|
`
|
|
@@ -1448,27 +1584,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1448
1584
|
}
|
|
1449
1585
|
|
|
1450
1586
|
// src/commands/init-hooks.ts
|
|
1451
|
-
import * as
|
|
1587
|
+
import * as fs12 from "fs";
|
|
1452
1588
|
import * as path12 from "path";
|
|
1453
1589
|
import chalk7 from "chalk";
|
|
1590
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1454
1591
|
function setupPreCommitHook(projectRoot) {
|
|
1455
1592
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1456
|
-
if (
|
|
1593
|
+
if (fs12.existsSync(lefthookPath)) {
|
|
1457
1594
|
addLefthookPreCommit(lefthookPath);
|
|
1458
1595
|
console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1459
1596
|
return;
|
|
1460
1597
|
}
|
|
1461
1598
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1462
|
-
if (
|
|
1599
|
+
if (fs12.existsSync(huskyDir)) {
|
|
1463
1600
|
writeHuskyPreCommit(huskyDir);
|
|
1464
1601
|
console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1465
1602
|
return;
|
|
1466
1603
|
}
|
|
1467
1604
|
const gitDir = path12.join(projectRoot, ".git");
|
|
1468
|
-
if (
|
|
1605
|
+
if (fs12.existsSync(gitDir)) {
|
|
1469
1606
|
const hooksDir = path12.join(gitDir, "hooks");
|
|
1470
|
-
if (!
|
|
1471
|
-
|
|
1607
|
+
if (!fs12.existsSync(hooksDir)) {
|
|
1608
|
+
fs12.mkdirSync(hooksDir, { recursive: true });
|
|
1472
1609
|
}
|
|
1473
1610
|
writeGitHookPreCommit(hooksDir);
|
|
1474
1611
|
console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -1476,10 +1613,10 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1476
1613
|
}
|
|
1477
1614
|
function writeGitHookPreCommit(hooksDir) {
|
|
1478
1615
|
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1479
|
-
if (
|
|
1480
|
-
const existing =
|
|
1616
|
+
if (fs12.existsSync(hookPath)) {
|
|
1617
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1481
1618
|
if (existing.includes("viberails")) return;
|
|
1482
|
-
|
|
1619
|
+
fs12.writeFileSync(
|
|
1483
1620
|
hookPath,
|
|
1484
1621
|
`${existing.trimEnd()}
|
|
1485
1622
|
|
|
@@ -1496,61 +1633,51 @@ npx viberails check --staged
|
|
|
1496
1633
|
"npx viberails check --staged",
|
|
1497
1634
|
""
|
|
1498
1635
|
].join("\n");
|
|
1499
|
-
|
|
1636
|
+
fs12.writeFileSync(hookPath, script, { mode: 493 });
|
|
1500
1637
|
}
|
|
1501
1638
|
function addLefthookPreCommit(lefthookPath) {
|
|
1502
|
-
const content =
|
|
1639
|
+
const content = fs12.readFileSync(lefthookPath, "utf-8");
|
|
1503
1640
|
if (content.includes("viberails")) return;
|
|
1504
|
-
const
|
|
1505
|
-
if (
|
|
1506
|
-
|
|
1507
|
-
"\n"
|
|
1508
|
-
);
|
|
1509
|
-
const updated = `${content.trimEnd()}
|
|
1510
|
-
${commandBlock}
|
|
1511
|
-
`;
|
|
1512
|
-
fs11.writeFileSync(lefthookPath, updated);
|
|
1513
|
-
} else {
|
|
1514
|
-
const section = [
|
|
1515
|
-
"",
|
|
1516
|
-
"pre-commit:",
|
|
1517
|
-
" commands:",
|
|
1518
|
-
" viberails:",
|
|
1519
|
-
" run: npx viberails check --staged"
|
|
1520
|
-
].join("\n");
|
|
1521
|
-
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1522
|
-
${section}
|
|
1523
|
-
`);
|
|
1641
|
+
const doc = parseYaml(content) ?? {};
|
|
1642
|
+
if (!doc["pre-commit"]) {
|
|
1643
|
+
doc["pre-commit"] = { commands: {} };
|
|
1524
1644
|
}
|
|
1645
|
+
if (!doc["pre-commit"].commands) {
|
|
1646
|
+
doc["pre-commit"].commands = {};
|
|
1647
|
+
}
|
|
1648
|
+
doc["pre-commit"].commands.viberails = {
|
|
1649
|
+
run: "npx viberails check --staged"
|
|
1650
|
+
};
|
|
1651
|
+
fs12.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
1525
1652
|
}
|
|
1526
1653
|
function detectHookManager(projectRoot) {
|
|
1527
|
-
if (
|
|
1528
|
-
if (
|
|
1529
|
-
if (
|
|
1654
|
+
if (fs12.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
1655
|
+
if (fs12.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
|
|
1656
|
+
if (fs12.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
|
|
1530
1657
|
return void 0;
|
|
1531
1658
|
}
|
|
1532
1659
|
function setupClaudeCodeHook(projectRoot) {
|
|
1533
1660
|
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1534
|
-
if (!
|
|
1535
|
-
|
|
1661
|
+
if (!fs12.existsSync(claudeDir)) {
|
|
1662
|
+
fs12.mkdirSync(claudeDir, { recursive: true });
|
|
1536
1663
|
}
|
|
1537
1664
|
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1538
1665
|
let settings = {};
|
|
1539
|
-
if (
|
|
1666
|
+
if (fs12.existsSync(settingsPath)) {
|
|
1540
1667
|
try {
|
|
1541
|
-
settings = JSON.parse(
|
|
1668
|
+
settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
1542
1669
|
} catch {
|
|
1543
1670
|
console.warn(
|
|
1544
|
-
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014
|
|
1671
|
+
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
1545
1672
|
);
|
|
1546
|
-
|
|
1673
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk7.cyan("viberails init --force")}`);
|
|
1674
|
+
return;
|
|
1547
1675
|
}
|
|
1548
1676
|
}
|
|
1549
1677
|
const hooks = settings.hooks ?? {};
|
|
1550
1678
|
const existing = hooks.PostToolUse ?? [];
|
|
1551
1679
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1552
|
-
const
|
|
1553
|
-
const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
|
|
1680
|
+
const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
|
|
1554
1681
|
hooks.PostToolUse = [
|
|
1555
1682
|
...existing,
|
|
1556
1683
|
{
|
|
@@ -1564,22 +1691,34 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1564
1691
|
}
|
|
1565
1692
|
];
|
|
1566
1693
|
settings.hooks = hooks;
|
|
1567
|
-
|
|
1694
|
+
fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1568
1695
|
`);
|
|
1569
1696
|
console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1570
1697
|
}
|
|
1698
|
+
function setupClaudeMdReference(projectRoot) {
|
|
1699
|
+
const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
|
|
1700
|
+
let content = "";
|
|
1701
|
+
if (fs12.existsSync(claudeMdPath)) {
|
|
1702
|
+
content = fs12.readFileSync(claudeMdPath, "utf-8");
|
|
1703
|
+
}
|
|
1704
|
+
if (content.includes("@.viberails/context.md")) return;
|
|
1705
|
+
const ref = "\n@.viberails/context.md\n";
|
|
1706
|
+
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1707
|
+
fs12.writeFileSync(claudeMdPath, prefix + ref);
|
|
1708
|
+
console.log(` ${chalk7.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
1709
|
+
}
|
|
1571
1710
|
function writeHuskyPreCommit(huskyDir) {
|
|
1572
1711
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1573
|
-
if (
|
|
1574
|
-
const existing =
|
|
1712
|
+
if (fs12.existsSync(hookPath)) {
|
|
1713
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1575
1714
|
if (!existing.includes("viberails")) {
|
|
1576
|
-
|
|
1715
|
+
fs12.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1577
1716
|
npx viberails check --staged
|
|
1578
1717
|
`);
|
|
1579
1718
|
}
|
|
1580
1719
|
return;
|
|
1581
1720
|
}
|
|
1582
|
-
|
|
1721
|
+
fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1583
1722
|
}
|
|
1584
1723
|
|
|
1585
1724
|
// src/commands/init.ts
|
|
@@ -1596,14 +1735,10 @@ function filterHighConfidence(conventions) {
|
|
|
1596
1735
|
}
|
|
1597
1736
|
return filtered;
|
|
1598
1737
|
}
|
|
1599
|
-
function
|
|
1738
|
+
function getConventionStr3(cv) {
|
|
1600
1739
|
if (!cv) return void 0;
|
|
1601
1740
|
return typeof cv === "string" ? cv : cv.value;
|
|
1602
1741
|
}
|
|
1603
|
-
function hasConventionOverrides(config) {
|
|
1604
|
-
if (!config.packages || config.packages.length === 0) return false;
|
|
1605
|
-
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1606
|
-
}
|
|
1607
1742
|
async function initCommand(options, cwd) {
|
|
1608
1743
|
const startDir = cwd ?? process.cwd();
|
|
1609
1744
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1613,10 +1748,10 @@ async function initCommand(options, cwd) {
|
|
|
1613
1748
|
);
|
|
1614
1749
|
}
|
|
1615
1750
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1616
|
-
if (
|
|
1751
|
+
if (fs13.existsSync(configPath) && !options.force) {
|
|
1617
1752
|
console.log(
|
|
1618
1753
|
`${chalk8.yellow("!")} viberails is already initialized.
|
|
1619
|
-
Run ${chalk8.cyan("viberails sync")} to update, or
|
|
1754
|
+
Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
|
|
1620
1755
|
);
|
|
1621
1756
|
return;
|
|
1622
1757
|
}
|
|
@@ -1636,16 +1771,18 @@ async function initCommand(options, cwd) {
|
|
|
1636
1771
|
ignore: config2.ignore
|
|
1637
1772
|
});
|
|
1638
1773
|
const inferred = inferBoundaries(graph);
|
|
1639
|
-
|
|
1774
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1775
|
+
if (denyCount > 0) {
|
|
1640
1776
|
config2.boundaries = inferred;
|
|
1641
1777
|
config2.rules.enforceBoundaries = true;
|
|
1642
|
-
console.log(` Inferred ${
|
|
1778
|
+
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1643
1779
|
}
|
|
1644
1780
|
}
|
|
1645
|
-
|
|
1781
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1646
1782
|
`);
|
|
1647
1783
|
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1648
1784
|
updateGitignore(projectRoot);
|
|
1785
|
+
setupClaudeMdReference(projectRoot);
|
|
1649
1786
|
console.log(`
|
|
1650
1787
|
Created:`);
|
|
1651
1788
|
console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
|
|
@@ -1668,27 +1805,18 @@ Created:`);
|
|
|
1668
1805
|
clack2.note(resultsText, "Scan results");
|
|
1669
1806
|
const decision = await promptInitDecision();
|
|
1670
1807
|
if (decision === "customize") {
|
|
1671
|
-
|
|
1672
|
-
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1673
|
-
"Rules"
|
|
1674
|
-
);
|
|
1675
|
-
const overrides = await promptRuleCustomization({
|
|
1808
|
+
const overrides = await promptRuleMenu({
|
|
1676
1809
|
maxFileLines: config.rules.maxFileLines,
|
|
1677
1810
|
requireTests: config.rules.requireTests,
|
|
1678
1811
|
enforceNaming: config.rules.enforceNaming,
|
|
1679
1812
|
enforcement: config.enforcement,
|
|
1680
|
-
fileNamingValue:
|
|
1813
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming),
|
|
1814
|
+
packageOverrides: config.packages
|
|
1681
1815
|
});
|
|
1682
1816
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1683
1817
|
config.rules.requireTests = overrides.requireTests;
|
|
1684
1818
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1685
1819
|
config.enforcement = overrides.enforcement;
|
|
1686
|
-
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1687
|
-
clack2.note(
|
|
1688
|
-
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1689
|
-
"Per-package overrides"
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
1820
|
}
|
|
1693
1821
|
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1694
1822
|
clack2.note(
|
|
@@ -1706,10 +1834,11 @@ Created:`);
|
|
|
1706
1834
|
ignore: config.ignore
|
|
1707
1835
|
});
|
|
1708
1836
|
const inferred = inferBoundaries(graph);
|
|
1709
|
-
|
|
1837
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1838
|
+
if (denyCount > 0) {
|
|
1710
1839
|
config.boundaries = inferred;
|
|
1711
1840
|
config.rules.enforceBoundaries = true;
|
|
1712
|
-
bs.stop(`Inferred ${
|
|
1841
|
+
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
1713
1842
|
} else {
|
|
1714
1843
|
bs.stop("No boundary rules inferred");
|
|
1715
1844
|
}
|
|
@@ -1717,13 +1846,7 @@ Created:`);
|
|
|
1717
1846
|
}
|
|
1718
1847
|
const hookManager = detectHookManager(projectRoot);
|
|
1719
1848
|
const integrations = await promptIntegrations(hookManager);
|
|
1720
|
-
|
|
1721
|
-
clack2.note(
|
|
1722
|
-
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1723
|
-
"Per-package conventions"
|
|
1724
|
-
);
|
|
1725
|
-
}
|
|
1726
|
-
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1849
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1727
1850
|
`);
|
|
1728
1851
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1729
1852
|
updateGitignore(projectRoot);
|
|
@@ -1734,8 +1857,7 @@ Created:`);
|
|
|
1734
1857
|
];
|
|
1735
1858
|
if (integrations.preCommitHook) {
|
|
1736
1859
|
setupPreCommitHook(projectRoot);
|
|
1737
|
-
|
|
1738
|
-
if (hookMgr) {
|
|
1860
|
+
if (hookManager === "Lefthook") {
|
|
1739
1861
|
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1740
1862
|
}
|
|
1741
1863
|
}
|
|
@@ -1743,6 +1865,10 @@ Created:`);
|
|
|
1743
1865
|
setupClaudeCodeHook(projectRoot);
|
|
1744
1866
|
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1745
1867
|
}
|
|
1868
|
+
if (integrations.claudeMdRef) {
|
|
1869
|
+
setupClaudeMdReference(projectRoot);
|
|
1870
|
+
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1871
|
+
}
|
|
1746
1872
|
clack2.log.success(`Created:
|
|
1747
1873
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1748
1874
|
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
@@ -1750,24 +1876,162 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
|
1750
1876
|
function updateGitignore(projectRoot) {
|
|
1751
1877
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
1752
1878
|
let content = "";
|
|
1753
|
-
if (
|
|
1754
|
-
content =
|
|
1879
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
1880
|
+
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1755
1881
|
}
|
|
1756
1882
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1757
1883
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1758
1884
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1759
1885
|
`;
|
|
1760
|
-
|
|
1886
|
+
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1761
1887
|
}
|
|
1762
1888
|
}
|
|
1763
1889
|
|
|
1764
1890
|
// src/commands/sync.ts
|
|
1765
|
-
import * as
|
|
1891
|
+
import * as fs14 from "fs";
|
|
1766
1892
|
import * as path14 from "path";
|
|
1767
1893
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1768
1894
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1769
1895
|
import chalk9 from "chalk";
|
|
1896
|
+
|
|
1897
|
+
// src/utils/diff-configs.ts
|
|
1898
|
+
import { CONVENTION_LABELS as CONVENTION_LABELS3, FRAMEWORK_NAMES as FRAMEWORK_NAMES4, ORM_NAMES as ORM_NAMES3, STYLING_NAMES as STYLING_NAMES4 } from "@viberails/types";
|
|
1899
|
+
function parseStackString(s) {
|
|
1900
|
+
const atIdx = s.indexOf("@");
|
|
1901
|
+
if (atIdx > 0) {
|
|
1902
|
+
return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
|
|
1903
|
+
}
|
|
1904
|
+
return { name: s };
|
|
1905
|
+
}
|
|
1906
|
+
function displayStackName(s) {
|
|
1907
|
+
const { name, version } = parseStackString(s);
|
|
1908
|
+
const allMaps = {
|
|
1909
|
+
...FRAMEWORK_NAMES4,
|
|
1910
|
+
...STYLING_NAMES4,
|
|
1911
|
+
...ORM_NAMES3
|
|
1912
|
+
};
|
|
1913
|
+
const display = allMaps[name] ?? name;
|
|
1914
|
+
return version ? `${display} ${version}` : display;
|
|
1915
|
+
}
|
|
1916
|
+
function conventionStr(cv) {
|
|
1917
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1918
|
+
}
|
|
1919
|
+
function isDetected(cv) {
|
|
1920
|
+
return typeof cv !== "string" && cv._detected === true;
|
|
1921
|
+
}
|
|
1922
|
+
var STACK_FIELDS = [
|
|
1923
|
+
"framework",
|
|
1924
|
+
"styling",
|
|
1925
|
+
"backend",
|
|
1926
|
+
"orm",
|
|
1927
|
+
"linter",
|
|
1928
|
+
"formatter",
|
|
1929
|
+
"testRunner"
|
|
1930
|
+
];
|
|
1931
|
+
var CONVENTION_KEYS = [
|
|
1932
|
+
"fileNaming",
|
|
1933
|
+
"componentNaming",
|
|
1934
|
+
"hookNaming",
|
|
1935
|
+
"importAlias"
|
|
1936
|
+
];
|
|
1937
|
+
var STRUCTURE_FIELDS = [
|
|
1938
|
+
{ key: "srcDir", label: "source directory" },
|
|
1939
|
+
{ key: "pages", label: "pages directory" },
|
|
1940
|
+
{ key: "components", label: "components directory" },
|
|
1941
|
+
{ key: "hooks", label: "hooks directory" },
|
|
1942
|
+
{ key: "utils", label: "utilities directory" },
|
|
1943
|
+
{ key: "types", label: "types directory" },
|
|
1944
|
+
{ key: "tests", label: "tests directory" },
|
|
1945
|
+
{ key: "testPattern", label: "test pattern" }
|
|
1946
|
+
];
|
|
1947
|
+
function diffConfigs(existing, merged) {
|
|
1948
|
+
const changes = [];
|
|
1949
|
+
for (const field of STACK_FIELDS) {
|
|
1950
|
+
const oldVal = existing.stack[field];
|
|
1951
|
+
const newVal = merged.stack[field];
|
|
1952
|
+
if (!oldVal && newVal) {
|
|
1953
|
+
changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
|
|
1954
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1955
|
+
changes.push({
|
|
1956
|
+
type: "changed",
|
|
1957
|
+
description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
for (const key of CONVENTION_KEYS) {
|
|
1962
|
+
const oldVal = existing.conventions[key];
|
|
1963
|
+
const newVal = merged.conventions[key];
|
|
1964
|
+
const label = CONVENTION_LABELS3[key] ?? key;
|
|
1965
|
+
if (!oldVal && newVal) {
|
|
1966
|
+
changes.push({
|
|
1967
|
+
type: "added",
|
|
1968
|
+
description: `New convention: ${label} (${conventionStr(newVal)})`
|
|
1969
|
+
});
|
|
1970
|
+
} else if (oldVal && newVal && isDetected(newVal)) {
|
|
1971
|
+
changes.push({
|
|
1972
|
+
type: "changed",
|
|
1973
|
+
description: `Convention updated: ${label} (${conventionStr(newVal)})`
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1978
|
+
const oldVal = existing.structure[key];
|
|
1979
|
+
const newVal = merged.structure[key];
|
|
1980
|
+
if (!oldVal && newVal) {
|
|
1981
|
+
changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
|
|
1985
|
+
for (const pkg of merged.packages ?? []) {
|
|
1986
|
+
if (!existingPaths.has(pkg.path)) {
|
|
1987
|
+
changes.push({ type: "added", description: `New package: ${pkg.path}` });
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
|
|
1991
|
+
const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
|
|
1992
|
+
for (const pkg of mergedWsPkgs) {
|
|
1993
|
+
if (!existingWsPkgs.has(pkg)) {
|
|
1994
|
+
changes.push({ type: "added", description: `Workspace: added ${pkg}` });
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
for (const pkg of existingWsPkgs) {
|
|
1998
|
+
if (!mergedWsPkgs.has(pkg)) {
|
|
1999
|
+
changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return changes;
|
|
2003
|
+
}
|
|
2004
|
+
function formatStatsDelta(oldStats, newStats) {
|
|
2005
|
+
const fileDelta = newStats.totalFiles - oldStats.totalFiles;
|
|
2006
|
+
const lineDelta = newStats.totalLines - oldStats.totalLines;
|
|
2007
|
+
if (fileDelta === 0 && lineDelta === 0) return void 0;
|
|
2008
|
+
const parts = [];
|
|
2009
|
+
if (fileDelta !== 0) {
|
|
2010
|
+
const sign = fileDelta > 0 ? "+" : "";
|
|
2011
|
+
parts.push(`${sign}${fileDelta.toLocaleString()} files`);
|
|
2012
|
+
}
|
|
2013
|
+
if (lineDelta !== 0) {
|
|
2014
|
+
const sign = lineDelta > 0 ? "+" : "";
|
|
2015
|
+
parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
|
|
2016
|
+
}
|
|
2017
|
+
return `${parts.join(", ")} since last sync`;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/commands/sync.ts
|
|
1770
2021
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
2022
|
+
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
2023
|
+
function loadPreviousStats(projectRoot) {
|
|
2024
|
+
const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
|
|
2025
|
+
try {
|
|
2026
|
+
const raw = fs14.readFileSync(scanResultPath, "utf-8");
|
|
2027
|
+
const parsed = JSON.parse(raw);
|
|
2028
|
+
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
2029
|
+
return parsed.statistics;
|
|
2030
|
+
}
|
|
2031
|
+
} catch {
|
|
2032
|
+
}
|
|
2033
|
+
return void 0;
|
|
2034
|
+
}
|
|
1771
2035
|
async function syncCommand(cwd) {
|
|
1772
2036
|
const startDir = cwd ?? process.cwd();
|
|
1773
2037
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1778,18 +2042,27 @@ async function syncCommand(cwd) {
|
|
|
1778
2042
|
}
|
|
1779
2043
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1780
2044
|
const existing = await loadConfig4(configPath);
|
|
2045
|
+
const previousStats = loadPreviousStats(projectRoot);
|
|
1781
2046
|
console.log(chalk9.dim("Scanning project..."));
|
|
1782
2047
|
const scanResult = await scan2(projectRoot);
|
|
1783
2048
|
const merged = mergeConfig(existing, scanResult);
|
|
1784
2049
|
const existingJson = JSON.stringify(existing, null, 2);
|
|
1785
2050
|
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1786
2051
|
const configChanged = existingJson !== mergedJson;
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2052
|
+
const changes = configChanged ? diffConfigs(existing, merged) : [];
|
|
2053
|
+
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
2054
|
+
if (changes.length > 0 || statsDelta) {
|
|
2055
|
+
console.log(`
|
|
2056
|
+
${chalk9.bold("Changes:")}`);
|
|
2057
|
+
for (const change of changes) {
|
|
2058
|
+
const icon = change.type === "removed" ? chalk9.red("-") : chalk9.green("+");
|
|
2059
|
+
console.log(` ${icon} ${change.description}`);
|
|
2060
|
+
}
|
|
2061
|
+
if (statsDelta) {
|
|
2062
|
+
console.log(` ${chalk9.dim(statsDelta)}`);
|
|
2063
|
+
}
|
|
1791
2064
|
}
|
|
1792
|
-
|
|
2065
|
+
fs14.writeFileSync(configPath, `${mergedJson}
|
|
1793
2066
|
`);
|
|
1794
2067
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1795
2068
|
console.log(`
|
|
@@ -1804,10 +2077,10 @@ ${chalk9.bold("Synced:")}`);
|
|
|
1804
2077
|
}
|
|
1805
2078
|
|
|
1806
2079
|
// src/index.ts
|
|
1807
|
-
var VERSION = "0.
|
|
2080
|
+
var VERSION = "0.4.0";
|
|
1808
2081
|
var program = new Command();
|
|
1809
2082
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1810
|
-
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
|
|
2083
|
+
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
|
|
1811
2084
|
try {
|
|
1812
2085
|
await initCommand(options);
|
|
1813
2086
|
} catch (err) {
|
|
@@ -1825,9 +2098,13 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1825
2098
|
process.exit(1);
|
|
1826
2099
|
}
|
|
1827
2100
|
});
|
|
1828
|
-
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").action(
|
|
2101
|
+
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").action(
|
|
1829
2102
|
async (options) => {
|
|
1830
2103
|
try {
|
|
2104
|
+
if (options.hook) {
|
|
2105
|
+
const exitCode2 = await hookCheckCommand();
|
|
2106
|
+
process.exit(exitCode2);
|
|
2107
|
+
}
|
|
1831
2108
|
const exitCode = await checkCommand({
|
|
1832
2109
|
...options,
|
|
1833
2110
|
noBoundaries: options.boundaries === false,
|