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.cjs
CHANGED
|
@@ -89,50 +89,115 @@ async function promptInitDecision() {
|
|
|
89
89
|
assertNotCancelled(result);
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
|
-
async function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
assertNotCancelled(maxFileLinesResult);
|
|
103
|
-
const requireTestsResult = await clack.confirm({
|
|
104
|
-
message: "Require matching test files for source files?",
|
|
105
|
-
initialValue: defaults.requireTests
|
|
106
|
-
});
|
|
107
|
-
assertNotCancelled(requireTestsResult);
|
|
108
|
-
const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
|
|
109
|
-
const enforceNamingResult = await clack.confirm({
|
|
110
|
-
message: namingLabel,
|
|
111
|
-
initialValue: defaults.enforceNaming
|
|
112
|
-
});
|
|
113
|
-
assertNotCancelled(enforceNamingResult);
|
|
114
|
-
const enforcementResult = await clack.select({
|
|
115
|
-
message: "Enforcement mode",
|
|
116
|
-
options: [
|
|
92
|
+
async function promptRuleMenu(defaults) {
|
|
93
|
+
const state = { ...defaults };
|
|
94
|
+
while (true) {
|
|
95
|
+
const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
|
|
96
|
+
const enforcementHint = state.enforcement === "warn" ? "warn \u2014 violations shown but commits allowed" : "enforce \u2014 commits blocked on violation";
|
|
97
|
+
const options = [
|
|
98
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
117
99
|
{
|
|
118
|
-
value: "
|
|
119
|
-
label: "
|
|
120
|
-
hint:
|
|
100
|
+
value: "requireTests",
|
|
101
|
+
label: "Require test files",
|
|
102
|
+
hint: state.requireTests ? "yes" : "no"
|
|
121
103
|
},
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
104
|
+
{ value: "enforceNaming", label: "Enforce file naming", hint: namingHint },
|
|
105
|
+
{ value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
|
|
106
|
+
];
|
|
107
|
+
if (state.packageOverrides && state.packageOverrides.length > 0) {
|
|
108
|
+
const count = state.packageOverrides.length;
|
|
109
|
+
options.push({
|
|
110
|
+
value: "packageOverrides",
|
|
111
|
+
label: "Per-package overrides",
|
|
112
|
+
hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
options.push({ value: "done", label: "Done" });
|
|
116
|
+
const choice = await clack.select({
|
|
117
|
+
message: "Customize rules",
|
|
118
|
+
options
|
|
119
|
+
});
|
|
120
|
+
assertNotCancelled(choice);
|
|
121
|
+
if (choice === "done") break;
|
|
122
|
+
if (choice === "packageOverrides" && state.packageOverrides) {
|
|
123
|
+
const lines = state.packageOverrides.map((pkg) => {
|
|
124
|
+
const diffs = [];
|
|
125
|
+
if (pkg.conventions) {
|
|
126
|
+
for (const [key, val] of Object.entries(pkg.conventions)) {
|
|
127
|
+
const v = typeof val === "string" ? val : val?.value;
|
|
128
|
+
if (v) diffs.push(`${key}: ${v}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (pkg.stack) {
|
|
132
|
+
for (const [key, val] of Object.entries(pkg.stack)) {
|
|
133
|
+
if (val) diffs.push(`${key}: ${val}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return `${pkg.path}
|
|
137
|
+
${diffs.join(", ") || "minor differences"}`;
|
|
138
|
+
});
|
|
139
|
+
clack.note(
|
|
140
|
+
`${lines.join("\n\n")}
|
|
141
|
+
|
|
142
|
+
Edit the "packages" section in viberails.config.json to adjust.`,
|
|
143
|
+
"Per-package overrides"
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (choice === "maxFileLines") {
|
|
148
|
+
const result = await clack.text({
|
|
149
|
+
message: "Maximum lines per source file?",
|
|
150
|
+
initialValue: String(state.maxFileLines),
|
|
151
|
+
validate: (v) => {
|
|
152
|
+
const n = Number.parseInt(v, 10);
|
|
153
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
assertNotCancelled(result);
|
|
157
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
158
|
+
}
|
|
159
|
+
if (choice === "requireTests") {
|
|
160
|
+
const result = await clack.confirm({
|
|
161
|
+
message: "Require matching test files for source files?",
|
|
162
|
+
initialValue: state.requireTests
|
|
163
|
+
});
|
|
164
|
+
assertNotCancelled(result);
|
|
165
|
+
state.requireTests = result;
|
|
166
|
+
}
|
|
167
|
+
if (choice === "enforceNaming") {
|
|
168
|
+
const result = await clack.confirm({
|
|
169
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
170
|
+
initialValue: state.enforceNaming
|
|
171
|
+
});
|
|
172
|
+
assertNotCancelled(result);
|
|
173
|
+
state.enforceNaming = result;
|
|
174
|
+
}
|
|
175
|
+
if (choice === "enforcement") {
|
|
176
|
+
const result = await clack.select({
|
|
177
|
+
message: "Enforcement mode",
|
|
178
|
+
options: [
|
|
179
|
+
{
|
|
180
|
+
value: "warn",
|
|
181
|
+
label: "warn",
|
|
182
|
+
hint: "show violations but don't block commits (recommended)"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
value: "enforce",
|
|
186
|
+
label: "enforce",
|
|
187
|
+
hint: "block commits with violations"
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
initialValue: state.enforcement
|
|
191
|
+
});
|
|
192
|
+
assertNotCancelled(result);
|
|
193
|
+
state.enforcement = result;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
131
196
|
return {
|
|
132
|
-
maxFileLines:
|
|
133
|
-
requireTests:
|
|
134
|
-
enforceNaming:
|
|
135
|
-
enforcement:
|
|
197
|
+
maxFileLines: state.maxFileLines,
|
|
198
|
+
requireTests: state.requireTests,
|
|
199
|
+
enforceNaming: state.enforceNaming,
|
|
200
|
+
enforcement: state.enforcement
|
|
136
201
|
};
|
|
137
202
|
}
|
|
138
203
|
async function promptIntegrations(hookManager) {
|
|
@@ -149,15 +214,21 @@ async function promptIntegrations(hookManager) {
|
|
|
149
214
|
value: "claude",
|
|
150
215
|
label: "Claude Code hook",
|
|
151
216
|
hint: "checks files when Claude edits them"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
value: "claudeMd",
|
|
220
|
+
label: "CLAUDE.md reference",
|
|
221
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
152
222
|
}
|
|
153
223
|
],
|
|
154
|
-
initialValues: ["preCommit", "claude"],
|
|
224
|
+
initialValues: ["preCommit", "claude", "claudeMd"],
|
|
155
225
|
required: false
|
|
156
226
|
});
|
|
157
227
|
assertNotCancelled(result);
|
|
158
228
|
return {
|
|
159
229
|
preCommitHook: result.includes("preCommit"),
|
|
160
|
-
claudeCodeHook: result.includes("claude")
|
|
230
|
+
claudeCodeHook: result.includes("claude"),
|
|
231
|
+
claudeMdRef: result.includes("claudeMd")
|
|
161
232
|
};
|
|
162
233
|
}
|
|
163
234
|
|
|
@@ -215,22 +286,21 @@ async function boundariesCommand(options, cwd) {
|
|
|
215
286
|
displayRules(config);
|
|
216
287
|
}
|
|
217
288
|
function displayRules(config) {
|
|
218
|
-
if (!config.boundaries || config.boundaries.length === 0) {
|
|
289
|
+
if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
|
|
219
290
|
console.log(import_chalk.default.yellow("No boundary rules configured."));
|
|
220
291
|
console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
221
292
|
return;
|
|
222
293
|
}
|
|
223
|
-
const
|
|
224
|
-
const
|
|
294
|
+
const { deny } = config.boundaries;
|
|
295
|
+
const sources = Object.keys(deny).filter((k) => deny[k].length > 0);
|
|
296
|
+
const totalRules = sources.reduce((sum, k) => sum + deny[k].length, 0);
|
|
225
297
|
console.log(`
|
|
226
|
-
${import_chalk.default.bold(`Boundary rules (${
|
|
298
|
+
${import_chalk.default.bold(`Boundary rules (${totalRules} deny rules):`)}
|
|
227
299
|
`);
|
|
228
|
-
for (const
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
233
|
-
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
300
|
+
for (const source of sources) {
|
|
301
|
+
for (const target of deny[source]) {
|
|
302
|
+
console.log(` ${import_chalk.default.red("\u2717")} ${source} \u2192 ${target}`);
|
|
303
|
+
}
|
|
234
304
|
}
|
|
235
305
|
console.log(
|
|
236
306
|
`
|
|
@@ -247,24 +317,22 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
247
317
|
});
|
|
248
318
|
console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
249
319
|
const inferred = inferBoundaries(graph);
|
|
250
|
-
|
|
320
|
+
const sources = Object.keys(inferred.deny).filter((k) => inferred.deny[k].length > 0);
|
|
321
|
+
const totalRules = sources.reduce((sum, k) => sum + inferred.deny[k].length, 0);
|
|
322
|
+
if (totalRules === 0) {
|
|
251
323
|
console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
|
|
252
324
|
return;
|
|
253
325
|
}
|
|
254
|
-
const allow = inferred.filter((r) => r.allow);
|
|
255
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
256
326
|
console.log(`
|
|
257
327
|
${import_chalk.default.bold("Inferred boundary rules:")}
|
|
258
328
|
`);
|
|
259
|
-
for (const
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
|
|
264
|
-
console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
329
|
+
for (const source of sources) {
|
|
330
|
+
for (const target of inferred.deny[source]) {
|
|
331
|
+
console.log(` ${import_chalk.default.red("\u2717")} ${source} \u2192 ${target}`);
|
|
332
|
+
}
|
|
265
333
|
}
|
|
266
334
|
console.log(`
|
|
267
|
-
${
|
|
335
|
+
${totalRules} denied`);
|
|
268
336
|
console.log("");
|
|
269
337
|
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
270
338
|
if (shouldSave) {
|
|
@@ -272,7 +340,7 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
|
|
|
272
340
|
config.rules.enforceBoundaries = true;
|
|
273
341
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
274
342
|
`);
|
|
275
|
-
console.log(`${import_chalk.default.green("\u2713")} Saved ${
|
|
343
|
+
console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
|
|
276
344
|
}
|
|
277
345
|
}
|
|
278
346
|
async function showGraph(projectRoot, config) {
|
|
@@ -590,7 +658,13 @@ async function checkCommand(options, cwd) {
|
|
|
590
658
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
591
659
|
}
|
|
592
660
|
if (filesToCheck.length === 0) {
|
|
593
|
-
|
|
661
|
+
if (options.format === "json") {
|
|
662
|
+
console.log(
|
|
663
|
+
JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
|
|
664
|
+
);
|
|
665
|
+
} else {
|
|
666
|
+
console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
|
|
667
|
+
}
|
|
594
668
|
return 0;
|
|
595
669
|
}
|
|
596
670
|
const violations = [];
|
|
@@ -631,7 +705,7 @@ async function checkCommand(options, cwd) {
|
|
|
631
705
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
632
706
|
violations.push(...testViolations);
|
|
633
707
|
}
|
|
634
|
-
if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
|
|
708
|
+
if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
|
|
635
709
|
const startTime = Date.now();
|
|
636
710
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
637
711
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -647,12 +721,14 @@ async function checkCommand(options, cwd) {
|
|
|
647
721
|
violations.push({
|
|
648
722
|
file: relFile,
|
|
649
723
|
rule: "boundary-violation",
|
|
650
|
-
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}
|
|
724
|
+
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}`,
|
|
651
725
|
severity
|
|
652
726
|
});
|
|
653
727
|
}
|
|
654
728
|
const elapsed = Date.now() - startTime;
|
|
655
|
-
|
|
729
|
+
if (options.format !== "json") {
|
|
730
|
+
console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
731
|
+
}
|
|
656
732
|
}
|
|
657
733
|
if (options.format === "json") {
|
|
658
734
|
console.log(
|
|
@@ -679,8 +755,54 @@ async function checkCommand(options, cwd) {
|
|
|
679
755
|
return 0;
|
|
680
756
|
}
|
|
681
757
|
|
|
758
|
+
// src/commands/check-hook.ts
|
|
759
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
760
|
+
function parseHookFilePath(input) {
|
|
761
|
+
try {
|
|
762
|
+
if (!input.trim()) return void 0;
|
|
763
|
+
const parsed = JSON.parse(input);
|
|
764
|
+
return parsed?.tool_input?.file_path ?? void 0;
|
|
765
|
+
} catch {
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function readStdin() {
|
|
770
|
+
try {
|
|
771
|
+
return fs7.readFileSync(0, "utf-8");
|
|
772
|
+
} catch {
|
|
773
|
+
return "";
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function hookCheckCommand(cwd) {
|
|
777
|
+
try {
|
|
778
|
+
const filePath = parseHookFilePath(readStdin());
|
|
779
|
+
if (!filePath) return 0;
|
|
780
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
781
|
+
let captured = "";
|
|
782
|
+
process.stdout.write = (chunk) => {
|
|
783
|
+
captured += typeof chunk === "string" ? chunk : chunk.toString();
|
|
784
|
+
return true;
|
|
785
|
+
};
|
|
786
|
+
try {
|
|
787
|
+
await checkCommand({ files: [filePath], format: "json" }, cwd);
|
|
788
|
+
} finally {
|
|
789
|
+
process.stdout.write = originalWrite;
|
|
790
|
+
}
|
|
791
|
+
if (!captured.trim()) return 0;
|
|
792
|
+
const result = JSON.parse(captured);
|
|
793
|
+
if (result.violations?.length > 0) {
|
|
794
|
+
process.stderr.write(`${captured.trim()}
|
|
795
|
+
`);
|
|
796
|
+
return 2;
|
|
797
|
+
}
|
|
798
|
+
return 0;
|
|
799
|
+
} catch {
|
|
800
|
+
return 0;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
682
804
|
// src/commands/fix.ts
|
|
683
|
-
var
|
|
805
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
684
806
|
var path10 = __toESM(require("path"), 1);
|
|
685
807
|
var import_config3 = require("@viberails/config");
|
|
686
808
|
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
@@ -823,7 +945,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
823
945
|
}
|
|
824
946
|
|
|
825
947
|
// src/commands/fix-naming.ts
|
|
826
|
-
var
|
|
948
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
827
949
|
var path8 = __toESM(require("path"), 1);
|
|
828
950
|
|
|
829
951
|
// src/commands/convert-name.ts
|
|
@@ -885,12 +1007,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
885
1007
|
const newRelPath = path8.join(dir, newFilename);
|
|
886
1008
|
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
887
1009
|
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
888
|
-
if (
|
|
1010
|
+
if (fs8.existsSync(newAbsPath)) return null;
|
|
889
1011
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
890
1012
|
}
|
|
891
1013
|
function executeRename(rename) {
|
|
892
|
-
if (
|
|
893
|
-
|
|
1014
|
+
if (fs8.existsSync(rename.newAbsPath)) return false;
|
|
1015
|
+
fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
894
1016
|
return true;
|
|
895
1017
|
}
|
|
896
1018
|
function deduplicateRenames(renames) {
|
|
@@ -905,7 +1027,7 @@ function deduplicateRenames(renames) {
|
|
|
905
1027
|
}
|
|
906
1028
|
|
|
907
1029
|
// src/commands/fix-tests.ts
|
|
908
|
-
var
|
|
1030
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
909
1031
|
var path9 = __toESM(require("path"), 1);
|
|
910
1032
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
911
1033
|
const { testPattern } = config.structure;
|
|
@@ -916,7 +1038,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
916
1038
|
const testFilename = `${stem}${testSuffix}`;
|
|
917
1039
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
918
1040
|
const testAbsPath = path9.join(dir, testFilename);
|
|
919
|
-
if (
|
|
1041
|
+
if (fs9.existsSync(testAbsPath)) return null;
|
|
920
1042
|
return {
|
|
921
1043
|
path: path9.relative(projectRoot, testAbsPath),
|
|
922
1044
|
absPath: testAbsPath,
|
|
@@ -930,8 +1052,8 @@ function writeTestStub(stub, config) {
|
|
|
930
1052
|
it.todo('add tests');
|
|
931
1053
|
});
|
|
932
1054
|
`;
|
|
933
|
-
|
|
934
|
-
|
|
1055
|
+
fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
1056
|
+
fs9.writeFileSync(stub.absPath, content);
|
|
935
1057
|
}
|
|
936
1058
|
|
|
937
1059
|
// src/commands/fix.ts
|
|
@@ -944,7 +1066,7 @@ async function fixCommand(options, cwd) {
|
|
|
944
1066
|
return 1;
|
|
945
1067
|
}
|
|
946
1068
|
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
947
|
-
if (!
|
|
1069
|
+
if (!fs10.existsSync(configPath)) {
|
|
948
1070
|
console.error(
|
|
949
1071
|
`${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
950
1072
|
);
|
|
@@ -1008,13 +1130,13 @@ async function fixCommand(options, cwd) {
|
|
|
1008
1130
|
}
|
|
1009
1131
|
let importUpdateCount = 0;
|
|
1010
1132
|
if (renameCount > 0) {
|
|
1011
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1133
|
+
const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
|
|
1012
1134
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
1013
1135
|
importUpdateCount = updates.length;
|
|
1014
1136
|
}
|
|
1015
1137
|
let stubCount = 0;
|
|
1016
1138
|
for (const stub of testStubs) {
|
|
1017
|
-
if (!
|
|
1139
|
+
if (!fs10.existsSync(stub.absPath)) {
|
|
1018
1140
|
writeTestStub(stub, config);
|
|
1019
1141
|
stubCount++;
|
|
1020
1142
|
}
|
|
@@ -1035,16 +1157,15 @@ async function fixCommand(options, cwd) {
|
|
|
1035
1157
|
}
|
|
1036
1158
|
|
|
1037
1159
|
// src/commands/init.ts
|
|
1038
|
-
var
|
|
1160
|
+
var fs13 = __toESM(require("fs"), 1);
|
|
1039
1161
|
var path13 = __toESM(require("path"), 1);
|
|
1040
1162
|
var clack2 = __toESM(require("@clack/prompts"), 1);
|
|
1041
1163
|
var import_config4 = require("@viberails/config");
|
|
1042
1164
|
var import_scanner = require("@viberails/scanner");
|
|
1043
1165
|
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
1044
1166
|
|
|
1045
|
-
// src/display.ts
|
|
1046
|
-
var
|
|
1047
|
-
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1167
|
+
// src/display-text.ts
|
|
1168
|
+
var import_types4 = require("@viberails/types");
|
|
1048
1169
|
|
|
1049
1170
|
// src/display-helpers.ts
|
|
1050
1171
|
var import_types = require("@viberails/types");
|
|
@@ -1095,6 +1216,10 @@ function formatRoleGroup(group) {
|
|
|
1095
1216
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1096
1217
|
}
|
|
1097
1218
|
|
|
1219
|
+
// src/display.ts
|
|
1220
|
+
var import_types3 = require("@viberails/types");
|
|
1221
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1222
|
+
|
|
1098
1223
|
// src/display-monorepo.ts
|
|
1099
1224
|
var import_types2 = require("@viberails/types");
|
|
1100
1225
|
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
@@ -1204,12 +1329,6 @@ function formatMonorepoResultsText(scanResult, config) {
|
|
|
1204
1329
|
}
|
|
1205
1330
|
|
|
1206
1331
|
// src/display.ts
|
|
1207
|
-
var CONVENTION_LABELS = {
|
|
1208
|
-
fileNaming: "File naming",
|
|
1209
|
-
componentNaming: "Component naming",
|
|
1210
|
-
hookNaming: "Hook naming",
|
|
1211
|
-
importAlias: "Import alias"
|
|
1212
|
-
};
|
|
1213
1332
|
function formatItem(item, nameMap) {
|
|
1214
1333
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1215
1334
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1228,7 +1347,7 @@ function displayConventions(scanResult) {
|
|
|
1228
1347
|
${import_chalk6.default.bold("Conventions:")}`);
|
|
1229
1348
|
for (const [key, convention] of conventionEntries) {
|
|
1230
1349
|
if (convention.confidence === "low") continue;
|
|
1231
|
-
const label = CONVENTION_LABELS[key] ?? key;
|
|
1350
|
+
const label = import_types3.CONVENTION_LABELS[key] ?? key;
|
|
1232
1351
|
if (scanResult.packages.length > 1) {
|
|
1233
1352
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1234
1353
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1345,6 +1464,11 @@ function displayRulesPreview(config) {
|
|
|
1345
1464
|
}
|
|
1346
1465
|
console.log("");
|
|
1347
1466
|
}
|
|
1467
|
+
|
|
1468
|
+
// src/display-text.ts
|
|
1469
|
+
function getConventionStr2(cv) {
|
|
1470
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1471
|
+
}
|
|
1348
1472
|
function plainConfidenceLabel(convention) {
|
|
1349
1473
|
const pct = Math.round(convention.consistency);
|
|
1350
1474
|
if (convention.confidence === "high") {
|
|
@@ -1360,7 +1484,7 @@ function formatConventionsText(scanResult) {
|
|
|
1360
1484
|
lines.push("Conventions:");
|
|
1361
1485
|
for (const [key, convention] of conventionEntries) {
|
|
1362
1486
|
if (convention.confidence === "low") continue;
|
|
1363
|
-
const label = CONVENTION_LABELS[key] ?? key;
|
|
1487
|
+
const label = import_types4.CONVENTION_LABELS[key] ?? key;
|
|
1364
1488
|
if (scanResult.packages.length > 1) {
|
|
1365
1489
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1366
1490
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1394,7 +1518,7 @@ function formatRulesText(config) {
|
|
|
1394
1518
|
lines.push(" \u2022 Require test files: no");
|
|
1395
1519
|
}
|
|
1396
1520
|
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1397
|
-
lines.push(` \u2022 Enforce file naming: ${
|
|
1521
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
|
|
1398
1522
|
} else {
|
|
1399
1523
|
lines.push(" \u2022 Enforce file naming: no");
|
|
1400
1524
|
}
|
|
@@ -1409,17 +1533,17 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1409
1533
|
const { stack } = scanResult;
|
|
1410
1534
|
lines.push("Detected:");
|
|
1411
1535
|
if (stack.framework) {
|
|
1412
|
-
lines.push(` \u2713 ${formatItem(stack.framework,
|
|
1536
|
+
lines.push(` \u2713 ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
|
|
1413
1537
|
}
|
|
1414
1538
|
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1415
1539
|
if (stack.styling) {
|
|
1416
|
-
lines.push(` \u2713 ${formatItem(stack.styling,
|
|
1540
|
+
lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
|
|
1417
1541
|
}
|
|
1418
1542
|
if (stack.backend) {
|
|
1419
|
-
lines.push(` \u2713 ${formatItem(stack.backend,
|
|
1543
|
+
lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
|
|
1420
1544
|
}
|
|
1421
1545
|
if (stack.orm) {
|
|
1422
|
-
lines.push(` \u2713 ${formatItem(stack.orm,
|
|
1546
|
+
lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
|
|
1423
1547
|
}
|
|
1424
1548
|
const secondaryParts = [];
|
|
1425
1549
|
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
@@ -1431,7 +1555,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1431
1555
|
}
|
|
1432
1556
|
if (stack.libraries.length > 0) {
|
|
1433
1557
|
for (const lib of stack.libraries) {
|
|
1434
|
-
lines.push(` \u2713 ${formatItem(lib,
|
|
1558
|
+
lines.push(` \u2713 ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
|
|
1435
1559
|
}
|
|
1436
1560
|
}
|
|
1437
1561
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1455,7 +1579,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1455
1579
|
}
|
|
1456
1580
|
|
|
1457
1581
|
// src/utils/write-generated-files.ts
|
|
1458
|
-
var
|
|
1582
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
1459
1583
|
var path11 = __toESM(require("path"), 1);
|
|
1460
1584
|
var import_context = require("@viberails/context");
|
|
1461
1585
|
var CONTEXT_DIR = ".viberails";
|
|
@@ -1464,12 +1588,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
|
|
|
1464
1588
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1465
1589
|
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1466
1590
|
try {
|
|
1467
|
-
if (!
|
|
1468
|
-
|
|
1591
|
+
if (!fs11.existsSync(contextDir)) {
|
|
1592
|
+
fs11.mkdirSync(contextDir, { recursive: true });
|
|
1469
1593
|
}
|
|
1470
1594
|
const context = (0, import_context.generateContext)(config);
|
|
1471
|
-
|
|
1472
|
-
|
|
1595
|
+
fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1596
|
+
fs11.writeFileSync(
|
|
1473
1597
|
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1474
1598
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1475
1599
|
`
|
|
@@ -1481,27 +1605,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1481
1605
|
}
|
|
1482
1606
|
|
|
1483
1607
|
// src/commands/init-hooks.ts
|
|
1484
|
-
var
|
|
1608
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
1485
1609
|
var path12 = __toESM(require("path"), 1);
|
|
1486
1610
|
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
1611
|
+
var import_yaml = require("yaml");
|
|
1487
1612
|
function setupPreCommitHook(projectRoot) {
|
|
1488
1613
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1489
|
-
if (
|
|
1614
|
+
if (fs12.existsSync(lefthookPath)) {
|
|
1490
1615
|
addLefthookPreCommit(lefthookPath);
|
|
1491
1616
|
console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1492
1617
|
return;
|
|
1493
1618
|
}
|
|
1494
1619
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1495
|
-
if (
|
|
1620
|
+
if (fs12.existsSync(huskyDir)) {
|
|
1496
1621
|
writeHuskyPreCommit(huskyDir);
|
|
1497
1622
|
console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1498
1623
|
return;
|
|
1499
1624
|
}
|
|
1500
1625
|
const gitDir = path12.join(projectRoot, ".git");
|
|
1501
|
-
if (
|
|
1626
|
+
if (fs12.existsSync(gitDir)) {
|
|
1502
1627
|
const hooksDir = path12.join(gitDir, "hooks");
|
|
1503
|
-
if (!
|
|
1504
|
-
|
|
1628
|
+
if (!fs12.existsSync(hooksDir)) {
|
|
1629
|
+
fs12.mkdirSync(hooksDir, { recursive: true });
|
|
1505
1630
|
}
|
|
1506
1631
|
writeGitHookPreCommit(hooksDir);
|
|
1507
1632
|
console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -1509,10 +1634,10 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1509
1634
|
}
|
|
1510
1635
|
function writeGitHookPreCommit(hooksDir) {
|
|
1511
1636
|
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1512
|
-
if (
|
|
1513
|
-
const existing =
|
|
1637
|
+
if (fs12.existsSync(hookPath)) {
|
|
1638
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1514
1639
|
if (existing.includes("viberails")) return;
|
|
1515
|
-
|
|
1640
|
+
fs12.writeFileSync(
|
|
1516
1641
|
hookPath,
|
|
1517
1642
|
`${existing.trimEnd()}
|
|
1518
1643
|
|
|
@@ -1529,61 +1654,51 @@ npx viberails check --staged
|
|
|
1529
1654
|
"npx viberails check --staged",
|
|
1530
1655
|
""
|
|
1531
1656
|
].join("\n");
|
|
1532
|
-
|
|
1657
|
+
fs12.writeFileSync(hookPath, script, { mode: 493 });
|
|
1533
1658
|
}
|
|
1534
1659
|
function addLefthookPreCommit(lefthookPath) {
|
|
1535
|
-
const content =
|
|
1660
|
+
const content = fs12.readFileSync(lefthookPath, "utf-8");
|
|
1536
1661
|
if (content.includes("viberails")) return;
|
|
1537
|
-
const
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
${commandBlock}
|
|
1544
|
-
`;
|
|
1545
|
-
fs11.writeFileSync(lefthookPath, updated);
|
|
1546
|
-
} else {
|
|
1547
|
-
const section = [
|
|
1548
|
-
"",
|
|
1549
|
-
"pre-commit:",
|
|
1550
|
-
" commands:",
|
|
1551
|
-
" viberails:",
|
|
1552
|
-
" run: npx viberails check --staged"
|
|
1553
|
-
].join("\n");
|
|
1554
|
-
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1555
|
-
${section}
|
|
1556
|
-
`);
|
|
1662
|
+
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
1663
|
+
if (!doc["pre-commit"]) {
|
|
1664
|
+
doc["pre-commit"] = { commands: {} };
|
|
1665
|
+
}
|
|
1666
|
+
if (!doc["pre-commit"].commands) {
|
|
1667
|
+
doc["pre-commit"].commands = {};
|
|
1557
1668
|
}
|
|
1669
|
+
doc["pre-commit"].commands.viberails = {
|
|
1670
|
+
run: "npx viberails check --staged"
|
|
1671
|
+
};
|
|
1672
|
+
fs12.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
1558
1673
|
}
|
|
1559
1674
|
function detectHookManager(projectRoot) {
|
|
1560
|
-
if (
|
|
1561
|
-
if (
|
|
1562
|
-
if (
|
|
1675
|
+
if (fs12.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
1676
|
+
if (fs12.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
|
|
1677
|
+
if (fs12.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
|
|
1563
1678
|
return void 0;
|
|
1564
1679
|
}
|
|
1565
1680
|
function setupClaudeCodeHook(projectRoot) {
|
|
1566
1681
|
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1567
|
-
if (!
|
|
1568
|
-
|
|
1682
|
+
if (!fs12.existsSync(claudeDir)) {
|
|
1683
|
+
fs12.mkdirSync(claudeDir, { recursive: true });
|
|
1569
1684
|
}
|
|
1570
1685
|
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1571
1686
|
let settings = {};
|
|
1572
|
-
if (
|
|
1687
|
+
if (fs12.existsSync(settingsPath)) {
|
|
1573
1688
|
try {
|
|
1574
|
-
settings = JSON.parse(
|
|
1689
|
+
settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
1575
1690
|
} catch {
|
|
1576
1691
|
console.warn(
|
|
1577
|
-
` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014
|
|
1692
|
+
` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
1578
1693
|
);
|
|
1579
|
-
|
|
1694
|
+
console.warn(` Fix the JSON manually, then re-run ${import_chalk7.default.cyan("viberails init --force")}`);
|
|
1695
|
+
return;
|
|
1580
1696
|
}
|
|
1581
1697
|
}
|
|
1582
1698
|
const hooks = settings.hooks ?? {};
|
|
1583
1699
|
const existing = hooks.PostToolUse ?? [];
|
|
1584
1700
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1585
|
-
const
|
|
1586
|
-
const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
|
|
1701
|
+
const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
|
|
1587
1702
|
hooks.PostToolUse = [
|
|
1588
1703
|
...existing,
|
|
1589
1704
|
{
|
|
@@ -1597,22 +1712,34 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1597
1712
|
}
|
|
1598
1713
|
];
|
|
1599
1714
|
settings.hooks = hooks;
|
|
1600
|
-
|
|
1715
|
+
fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1601
1716
|
`);
|
|
1602
1717
|
console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1603
1718
|
}
|
|
1719
|
+
function setupClaudeMdReference(projectRoot) {
|
|
1720
|
+
const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
|
|
1721
|
+
let content = "";
|
|
1722
|
+
if (fs12.existsSync(claudeMdPath)) {
|
|
1723
|
+
content = fs12.readFileSync(claudeMdPath, "utf-8");
|
|
1724
|
+
}
|
|
1725
|
+
if (content.includes("@.viberails/context.md")) return;
|
|
1726
|
+
const ref = "\n@.viberails/context.md\n";
|
|
1727
|
+
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1728
|
+
fs12.writeFileSync(claudeMdPath, prefix + ref);
|
|
1729
|
+
console.log(` ${import_chalk7.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
1730
|
+
}
|
|
1604
1731
|
function writeHuskyPreCommit(huskyDir) {
|
|
1605
1732
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1606
|
-
if (
|
|
1607
|
-
const existing =
|
|
1733
|
+
if (fs12.existsSync(hookPath)) {
|
|
1734
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1608
1735
|
if (!existing.includes("viberails")) {
|
|
1609
|
-
|
|
1736
|
+
fs12.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1610
1737
|
npx viberails check --staged
|
|
1611
1738
|
`);
|
|
1612
1739
|
}
|
|
1613
1740
|
return;
|
|
1614
1741
|
}
|
|
1615
|
-
|
|
1742
|
+
fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1616
1743
|
}
|
|
1617
1744
|
|
|
1618
1745
|
// src/commands/init.ts
|
|
@@ -1629,14 +1756,10 @@ function filterHighConfidence(conventions) {
|
|
|
1629
1756
|
}
|
|
1630
1757
|
return filtered;
|
|
1631
1758
|
}
|
|
1632
|
-
function
|
|
1759
|
+
function getConventionStr3(cv) {
|
|
1633
1760
|
if (!cv) return void 0;
|
|
1634
1761
|
return typeof cv === "string" ? cv : cv.value;
|
|
1635
1762
|
}
|
|
1636
|
-
function hasConventionOverrides(config) {
|
|
1637
|
-
if (!config.packages || config.packages.length === 0) return false;
|
|
1638
|
-
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1639
|
-
}
|
|
1640
1763
|
async function initCommand(options, cwd) {
|
|
1641
1764
|
const startDir = cwd ?? process.cwd();
|
|
1642
1765
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1646,10 +1769,10 @@ async function initCommand(options, cwd) {
|
|
|
1646
1769
|
);
|
|
1647
1770
|
}
|
|
1648
1771
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1649
|
-
if (
|
|
1772
|
+
if (fs13.existsSync(configPath) && !options.force) {
|
|
1650
1773
|
console.log(
|
|
1651
1774
|
`${import_chalk8.default.yellow("!")} viberails is already initialized.
|
|
1652
|
-
Run ${import_chalk8.default.cyan("viberails sync")} to update, or
|
|
1775
|
+
Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
|
|
1653
1776
|
);
|
|
1654
1777
|
return;
|
|
1655
1778
|
}
|
|
@@ -1669,16 +1792,18 @@ async function initCommand(options, cwd) {
|
|
|
1669
1792
|
ignore: config2.ignore
|
|
1670
1793
|
});
|
|
1671
1794
|
const inferred = inferBoundaries(graph);
|
|
1672
|
-
|
|
1795
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1796
|
+
if (denyCount > 0) {
|
|
1673
1797
|
config2.boundaries = inferred;
|
|
1674
1798
|
config2.rules.enforceBoundaries = true;
|
|
1675
|
-
console.log(` Inferred ${
|
|
1799
|
+
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1676
1800
|
}
|
|
1677
1801
|
}
|
|
1678
|
-
|
|
1802
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1679
1803
|
`);
|
|
1680
1804
|
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1681
1805
|
updateGitignore(projectRoot);
|
|
1806
|
+
setupClaudeMdReference(projectRoot);
|
|
1682
1807
|
console.log(`
|
|
1683
1808
|
Created:`);
|
|
1684
1809
|
console.log(` ${import_chalk8.default.green("\u2713")} ${CONFIG_FILE4}`);
|
|
@@ -1701,27 +1826,18 @@ Created:`);
|
|
|
1701
1826
|
clack2.note(resultsText, "Scan results");
|
|
1702
1827
|
const decision = await promptInitDecision();
|
|
1703
1828
|
if (decision === "customize") {
|
|
1704
|
-
|
|
1705
|
-
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1706
|
-
"Rules"
|
|
1707
|
-
);
|
|
1708
|
-
const overrides = await promptRuleCustomization({
|
|
1829
|
+
const overrides = await promptRuleMenu({
|
|
1709
1830
|
maxFileLines: config.rules.maxFileLines,
|
|
1710
1831
|
requireTests: config.rules.requireTests,
|
|
1711
1832
|
enforceNaming: config.rules.enforceNaming,
|
|
1712
1833
|
enforcement: config.enforcement,
|
|
1713
|
-
fileNamingValue:
|
|
1834
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming),
|
|
1835
|
+
packageOverrides: config.packages
|
|
1714
1836
|
});
|
|
1715
1837
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1716
1838
|
config.rules.requireTests = overrides.requireTests;
|
|
1717
1839
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1718
1840
|
config.enforcement = overrides.enforcement;
|
|
1719
|
-
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1720
|
-
clack2.note(
|
|
1721
|
-
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1722
|
-
"Per-package overrides"
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
1841
|
}
|
|
1726
1842
|
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1727
1843
|
clack2.note(
|
|
@@ -1739,10 +1855,11 @@ Created:`);
|
|
|
1739
1855
|
ignore: config.ignore
|
|
1740
1856
|
});
|
|
1741
1857
|
const inferred = inferBoundaries(graph);
|
|
1742
|
-
|
|
1858
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1859
|
+
if (denyCount > 0) {
|
|
1743
1860
|
config.boundaries = inferred;
|
|
1744
1861
|
config.rules.enforceBoundaries = true;
|
|
1745
|
-
bs.stop(`Inferred ${
|
|
1862
|
+
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
1746
1863
|
} else {
|
|
1747
1864
|
bs.stop("No boundary rules inferred");
|
|
1748
1865
|
}
|
|
@@ -1750,13 +1867,7 @@ Created:`);
|
|
|
1750
1867
|
}
|
|
1751
1868
|
const hookManager = detectHookManager(projectRoot);
|
|
1752
1869
|
const integrations = await promptIntegrations(hookManager);
|
|
1753
|
-
|
|
1754
|
-
clack2.note(
|
|
1755
|
-
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1756
|
-
"Per-package conventions"
|
|
1757
|
-
);
|
|
1758
|
-
}
|
|
1759
|
-
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1870
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1760
1871
|
`);
|
|
1761
1872
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1762
1873
|
updateGitignore(projectRoot);
|
|
@@ -1767,8 +1878,7 @@ Created:`);
|
|
|
1767
1878
|
];
|
|
1768
1879
|
if (integrations.preCommitHook) {
|
|
1769
1880
|
setupPreCommitHook(projectRoot);
|
|
1770
|
-
|
|
1771
|
-
if (hookMgr) {
|
|
1881
|
+
if (hookManager === "Lefthook") {
|
|
1772
1882
|
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1773
1883
|
}
|
|
1774
1884
|
}
|
|
@@ -1776,6 +1886,10 @@ Created:`);
|
|
|
1776
1886
|
setupClaudeCodeHook(projectRoot);
|
|
1777
1887
|
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1778
1888
|
}
|
|
1889
|
+
if (integrations.claudeMdRef) {
|
|
1890
|
+
setupClaudeMdReference(projectRoot);
|
|
1891
|
+
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1892
|
+
}
|
|
1779
1893
|
clack2.log.success(`Created:
|
|
1780
1894
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1781
1895
|
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
@@ -1783,24 +1897,162 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
|
1783
1897
|
function updateGitignore(projectRoot) {
|
|
1784
1898
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
1785
1899
|
let content = "";
|
|
1786
|
-
if (
|
|
1787
|
-
content =
|
|
1900
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
1901
|
+
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1788
1902
|
}
|
|
1789
1903
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1790
1904
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1791
1905
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1792
1906
|
`;
|
|
1793
|
-
|
|
1907
|
+
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1794
1908
|
}
|
|
1795
1909
|
}
|
|
1796
1910
|
|
|
1797
1911
|
// src/commands/sync.ts
|
|
1798
|
-
var
|
|
1912
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
1799
1913
|
var path14 = __toESM(require("path"), 1);
|
|
1800
1914
|
var import_config5 = require("@viberails/config");
|
|
1801
1915
|
var import_scanner2 = require("@viberails/scanner");
|
|
1802
1916
|
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
1917
|
+
|
|
1918
|
+
// src/utils/diff-configs.ts
|
|
1919
|
+
var import_types5 = require("@viberails/types");
|
|
1920
|
+
function parseStackString(s) {
|
|
1921
|
+
const atIdx = s.indexOf("@");
|
|
1922
|
+
if (atIdx > 0) {
|
|
1923
|
+
return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
|
|
1924
|
+
}
|
|
1925
|
+
return { name: s };
|
|
1926
|
+
}
|
|
1927
|
+
function displayStackName(s) {
|
|
1928
|
+
const { name, version } = parseStackString(s);
|
|
1929
|
+
const allMaps = {
|
|
1930
|
+
...import_types5.FRAMEWORK_NAMES,
|
|
1931
|
+
...import_types5.STYLING_NAMES,
|
|
1932
|
+
...import_types5.ORM_NAMES
|
|
1933
|
+
};
|
|
1934
|
+
const display = allMaps[name] ?? name;
|
|
1935
|
+
return version ? `${display} ${version}` : display;
|
|
1936
|
+
}
|
|
1937
|
+
function conventionStr(cv) {
|
|
1938
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1939
|
+
}
|
|
1940
|
+
function isDetected(cv) {
|
|
1941
|
+
return typeof cv !== "string" && cv._detected === true;
|
|
1942
|
+
}
|
|
1943
|
+
var STACK_FIELDS = [
|
|
1944
|
+
"framework",
|
|
1945
|
+
"styling",
|
|
1946
|
+
"backend",
|
|
1947
|
+
"orm",
|
|
1948
|
+
"linter",
|
|
1949
|
+
"formatter",
|
|
1950
|
+
"testRunner"
|
|
1951
|
+
];
|
|
1952
|
+
var CONVENTION_KEYS = [
|
|
1953
|
+
"fileNaming",
|
|
1954
|
+
"componentNaming",
|
|
1955
|
+
"hookNaming",
|
|
1956
|
+
"importAlias"
|
|
1957
|
+
];
|
|
1958
|
+
var STRUCTURE_FIELDS = [
|
|
1959
|
+
{ key: "srcDir", label: "source directory" },
|
|
1960
|
+
{ key: "pages", label: "pages directory" },
|
|
1961
|
+
{ key: "components", label: "components directory" },
|
|
1962
|
+
{ key: "hooks", label: "hooks directory" },
|
|
1963
|
+
{ key: "utils", label: "utilities directory" },
|
|
1964
|
+
{ key: "types", label: "types directory" },
|
|
1965
|
+
{ key: "tests", label: "tests directory" },
|
|
1966
|
+
{ key: "testPattern", label: "test pattern" }
|
|
1967
|
+
];
|
|
1968
|
+
function diffConfigs(existing, merged) {
|
|
1969
|
+
const changes = [];
|
|
1970
|
+
for (const field of STACK_FIELDS) {
|
|
1971
|
+
const oldVal = existing.stack[field];
|
|
1972
|
+
const newVal = merged.stack[field];
|
|
1973
|
+
if (!oldVal && newVal) {
|
|
1974
|
+
changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
|
|
1975
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1976
|
+
changes.push({
|
|
1977
|
+
type: "changed",
|
|
1978
|
+
description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
for (const key of CONVENTION_KEYS) {
|
|
1983
|
+
const oldVal = existing.conventions[key];
|
|
1984
|
+
const newVal = merged.conventions[key];
|
|
1985
|
+
const label = import_types5.CONVENTION_LABELS[key] ?? key;
|
|
1986
|
+
if (!oldVal && newVal) {
|
|
1987
|
+
changes.push({
|
|
1988
|
+
type: "added",
|
|
1989
|
+
description: `New convention: ${label} (${conventionStr(newVal)})`
|
|
1990
|
+
});
|
|
1991
|
+
} else if (oldVal && newVal && isDetected(newVal)) {
|
|
1992
|
+
changes.push({
|
|
1993
|
+
type: "changed",
|
|
1994
|
+
description: `Convention updated: ${label} (${conventionStr(newVal)})`
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1999
|
+
const oldVal = existing.structure[key];
|
|
2000
|
+
const newVal = merged.structure[key];
|
|
2001
|
+
if (!oldVal && newVal) {
|
|
2002
|
+
changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
|
|
2006
|
+
for (const pkg of merged.packages ?? []) {
|
|
2007
|
+
if (!existingPaths.has(pkg.path)) {
|
|
2008
|
+
changes.push({ type: "added", description: `New package: ${pkg.path}` });
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
|
|
2012
|
+
const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
|
|
2013
|
+
for (const pkg of mergedWsPkgs) {
|
|
2014
|
+
if (!existingWsPkgs.has(pkg)) {
|
|
2015
|
+
changes.push({ type: "added", description: `Workspace: added ${pkg}` });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
for (const pkg of existingWsPkgs) {
|
|
2019
|
+
if (!mergedWsPkgs.has(pkg)) {
|
|
2020
|
+
changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return changes;
|
|
2024
|
+
}
|
|
2025
|
+
function formatStatsDelta(oldStats, newStats) {
|
|
2026
|
+
const fileDelta = newStats.totalFiles - oldStats.totalFiles;
|
|
2027
|
+
const lineDelta = newStats.totalLines - oldStats.totalLines;
|
|
2028
|
+
if (fileDelta === 0 && lineDelta === 0) return void 0;
|
|
2029
|
+
const parts = [];
|
|
2030
|
+
if (fileDelta !== 0) {
|
|
2031
|
+
const sign = fileDelta > 0 ? "+" : "";
|
|
2032
|
+
parts.push(`${sign}${fileDelta.toLocaleString()} files`);
|
|
2033
|
+
}
|
|
2034
|
+
if (lineDelta !== 0) {
|
|
2035
|
+
const sign = lineDelta > 0 ? "+" : "";
|
|
2036
|
+
parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
|
|
2037
|
+
}
|
|
2038
|
+
return `${parts.join(", ")} since last sync`;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/commands/sync.ts
|
|
1803
2042
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
2043
|
+
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
2044
|
+
function loadPreviousStats(projectRoot) {
|
|
2045
|
+
const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
|
|
2046
|
+
try {
|
|
2047
|
+
const raw = fs14.readFileSync(scanResultPath, "utf-8");
|
|
2048
|
+
const parsed = JSON.parse(raw);
|
|
2049
|
+
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
2050
|
+
return parsed.statistics;
|
|
2051
|
+
}
|
|
2052
|
+
} catch {
|
|
2053
|
+
}
|
|
2054
|
+
return void 0;
|
|
2055
|
+
}
|
|
1804
2056
|
async function syncCommand(cwd) {
|
|
1805
2057
|
const startDir = cwd ?? process.cwd();
|
|
1806
2058
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1811,18 +2063,27 @@ async function syncCommand(cwd) {
|
|
|
1811
2063
|
}
|
|
1812
2064
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1813
2065
|
const existing = await (0, import_config5.loadConfig)(configPath);
|
|
2066
|
+
const previousStats = loadPreviousStats(projectRoot);
|
|
1814
2067
|
console.log(import_chalk9.default.dim("Scanning project..."));
|
|
1815
2068
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
1816
2069
|
const merged = (0, import_config5.mergeConfig)(existing, scanResult);
|
|
1817
2070
|
const existingJson = JSON.stringify(existing, null, 2);
|
|
1818
2071
|
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1819
2072
|
const configChanged = existingJson !== mergedJson;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2073
|
+
const changes = configChanged ? diffConfigs(existing, merged) : [];
|
|
2074
|
+
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
2075
|
+
if (changes.length > 0 || statsDelta) {
|
|
2076
|
+
console.log(`
|
|
2077
|
+
${import_chalk9.default.bold("Changes:")}`);
|
|
2078
|
+
for (const change of changes) {
|
|
2079
|
+
const icon = change.type === "removed" ? import_chalk9.default.red("-") : import_chalk9.default.green("+");
|
|
2080
|
+
console.log(` ${icon} ${change.description}`);
|
|
2081
|
+
}
|
|
2082
|
+
if (statsDelta) {
|
|
2083
|
+
console.log(` ${import_chalk9.default.dim(statsDelta)}`);
|
|
2084
|
+
}
|
|
1824
2085
|
}
|
|
1825
|
-
|
|
2086
|
+
fs14.writeFileSync(configPath, `${mergedJson}
|
|
1826
2087
|
`);
|
|
1827
2088
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1828
2089
|
console.log(`
|
|
@@ -1837,10 +2098,10 @@ ${import_chalk9.default.bold("Synced:")}`);
|
|
|
1837
2098
|
}
|
|
1838
2099
|
|
|
1839
2100
|
// src/index.ts
|
|
1840
|
-
var VERSION = "0.
|
|
2101
|
+
var VERSION = "0.4.0";
|
|
1841
2102
|
var program = new import_commander.Command();
|
|
1842
2103
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1843
|
-
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
|
|
2104
|
+
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) => {
|
|
1844
2105
|
try {
|
|
1845
2106
|
await initCommand(options);
|
|
1846
2107
|
} catch (err) {
|
|
@@ -1858,9 +2119,13 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1858
2119
|
process.exit(1);
|
|
1859
2120
|
}
|
|
1860
2121
|
});
|
|
1861
|
-
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").action(
|
|
2122
|
+
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(
|
|
1862
2123
|
async (options) => {
|
|
1863
2124
|
try {
|
|
2125
|
+
if (options.hook) {
|
|
2126
|
+
const exitCode2 = await hookCheckCommand();
|
|
2127
|
+
process.exit(exitCode2);
|
|
2128
|
+
}
|
|
1864
2129
|
const exitCode = await checkCommand({
|
|
1865
2130
|
...options,
|
|
1866
2131
|
noBoundaries: options.boundaries === false,
|