viberails 0.3.3 → 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 +240 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +240 -156
- 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) {
|
|
@@ -593,7 +658,13 @@ async function checkCommand(options, cwd) {
|
|
|
593
658
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
594
659
|
}
|
|
595
660
|
if (filesToCheck.length === 0) {
|
|
596
|
-
|
|
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
|
+
}
|
|
597
668
|
return 0;
|
|
598
669
|
}
|
|
599
670
|
const violations = [];
|
|
@@ -655,7 +726,9 @@ async function checkCommand(options, cwd) {
|
|
|
655
726
|
});
|
|
656
727
|
}
|
|
657
728
|
const elapsed = Date.now() - startTime;
|
|
658
|
-
|
|
729
|
+
if (options.format !== "json") {
|
|
730
|
+
console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
731
|
+
}
|
|
659
732
|
}
|
|
660
733
|
if (options.format === "json") {
|
|
661
734
|
console.log(
|
|
@@ -682,8 +755,54 @@ async function checkCommand(options, cwd) {
|
|
|
682
755
|
return 0;
|
|
683
756
|
}
|
|
684
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
|
+
|
|
685
804
|
// src/commands/fix.ts
|
|
686
|
-
var
|
|
805
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
687
806
|
var path10 = __toESM(require("path"), 1);
|
|
688
807
|
var import_config3 = require("@viberails/config");
|
|
689
808
|
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
@@ -826,7 +945,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
826
945
|
}
|
|
827
946
|
|
|
828
947
|
// src/commands/fix-naming.ts
|
|
829
|
-
var
|
|
948
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
830
949
|
var path8 = __toESM(require("path"), 1);
|
|
831
950
|
|
|
832
951
|
// src/commands/convert-name.ts
|
|
@@ -888,12 +1007,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
888
1007
|
const newRelPath = path8.join(dir, newFilename);
|
|
889
1008
|
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
890
1009
|
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
891
|
-
if (
|
|
1010
|
+
if (fs8.existsSync(newAbsPath)) return null;
|
|
892
1011
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
893
1012
|
}
|
|
894
1013
|
function executeRename(rename) {
|
|
895
|
-
if (
|
|
896
|
-
|
|
1014
|
+
if (fs8.existsSync(rename.newAbsPath)) return false;
|
|
1015
|
+
fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
897
1016
|
return true;
|
|
898
1017
|
}
|
|
899
1018
|
function deduplicateRenames(renames) {
|
|
@@ -908,7 +1027,7 @@ function deduplicateRenames(renames) {
|
|
|
908
1027
|
}
|
|
909
1028
|
|
|
910
1029
|
// src/commands/fix-tests.ts
|
|
911
|
-
var
|
|
1030
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
912
1031
|
var path9 = __toESM(require("path"), 1);
|
|
913
1032
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
914
1033
|
const { testPattern } = config.structure;
|
|
@@ -919,7 +1038,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
919
1038
|
const testFilename = `${stem}${testSuffix}`;
|
|
920
1039
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
921
1040
|
const testAbsPath = path9.join(dir, testFilename);
|
|
922
|
-
if (
|
|
1041
|
+
if (fs9.existsSync(testAbsPath)) return null;
|
|
923
1042
|
return {
|
|
924
1043
|
path: path9.relative(projectRoot, testAbsPath),
|
|
925
1044
|
absPath: testAbsPath,
|
|
@@ -933,8 +1052,8 @@ function writeTestStub(stub, config) {
|
|
|
933
1052
|
it.todo('add tests');
|
|
934
1053
|
});
|
|
935
1054
|
`;
|
|
936
|
-
|
|
937
|
-
|
|
1055
|
+
fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
1056
|
+
fs9.writeFileSync(stub.absPath, content);
|
|
938
1057
|
}
|
|
939
1058
|
|
|
940
1059
|
// src/commands/fix.ts
|
|
@@ -947,7 +1066,7 @@ async function fixCommand(options, cwd) {
|
|
|
947
1066
|
return 1;
|
|
948
1067
|
}
|
|
949
1068
|
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
950
|
-
if (!
|
|
1069
|
+
if (!fs10.existsSync(configPath)) {
|
|
951
1070
|
console.error(
|
|
952
1071
|
`${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
953
1072
|
);
|
|
@@ -1011,13 +1130,13 @@ async function fixCommand(options, cwd) {
|
|
|
1011
1130
|
}
|
|
1012
1131
|
let importUpdateCount = 0;
|
|
1013
1132
|
if (renameCount > 0) {
|
|
1014
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1133
|
+
const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
|
|
1015
1134
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
1016
1135
|
importUpdateCount = updates.length;
|
|
1017
1136
|
}
|
|
1018
1137
|
let stubCount = 0;
|
|
1019
1138
|
for (const stub of testStubs) {
|
|
1020
|
-
if (!
|
|
1139
|
+
if (!fs10.existsSync(stub.absPath)) {
|
|
1021
1140
|
writeTestStub(stub, config);
|
|
1022
1141
|
stubCount++;
|
|
1023
1142
|
}
|
|
@@ -1038,7 +1157,7 @@ async function fixCommand(options, cwd) {
|
|
|
1038
1157
|
}
|
|
1039
1158
|
|
|
1040
1159
|
// src/commands/init.ts
|
|
1041
|
-
var
|
|
1160
|
+
var fs13 = __toESM(require("fs"), 1);
|
|
1042
1161
|
var path13 = __toESM(require("path"), 1);
|
|
1043
1162
|
var clack2 = __toESM(require("@clack/prompts"), 1);
|
|
1044
1163
|
var import_config4 = require("@viberails/config");
|
|
@@ -1460,7 +1579,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1460
1579
|
}
|
|
1461
1580
|
|
|
1462
1581
|
// src/utils/write-generated-files.ts
|
|
1463
|
-
var
|
|
1582
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
1464
1583
|
var path11 = __toESM(require("path"), 1);
|
|
1465
1584
|
var import_context = require("@viberails/context");
|
|
1466
1585
|
var CONTEXT_DIR = ".viberails";
|
|
@@ -1469,12 +1588,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
|
|
|
1469
1588
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1470
1589
|
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1471
1590
|
try {
|
|
1472
|
-
if (!
|
|
1473
|
-
|
|
1591
|
+
if (!fs11.existsSync(contextDir)) {
|
|
1592
|
+
fs11.mkdirSync(contextDir, { recursive: true });
|
|
1474
1593
|
}
|
|
1475
1594
|
const context = (0, import_context.generateContext)(config);
|
|
1476
|
-
|
|
1477
|
-
|
|
1595
|
+
fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1596
|
+
fs11.writeFileSync(
|
|
1478
1597
|
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1479
1598
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1480
1599
|
`
|
|
@@ -1486,27 +1605,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1486
1605
|
}
|
|
1487
1606
|
|
|
1488
1607
|
// src/commands/init-hooks.ts
|
|
1489
|
-
var
|
|
1608
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
1490
1609
|
var path12 = __toESM(require("path"), 1);
|
|
1491
1610
|
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
1611
|
+
var import_yaml = require("yaml");
|
|
1492
1612
|
function setupPreCommitHook(projectRoot) {
|
|
1493
1613
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1494
|
-
if (
|
|
1614
|
+
if (fs12.existsSync(lefthookPath)) {
|
|
1495
1615
|
addLefthookPreCommit(lefthookPath);
|
|
1496
1616
|
console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1497
1617
|
return;
|
|
1498
1618
|
}
|
|
1499
1619
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1500
|
-
if (
|
|
1620
|
+
if (fs12.existsSync(huskyDir)) {
|
|
1501
1621
|
writeHuskyPreCommit(huskyDir);
|
|
1502
1622
|
console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1503
1623
|
return;
|
|
1504
1624
|
}
|
|
1505
1625
|
const gitDir = path12.join(projectRoot, ".git");
|
|
1506
|
-
if (
|
|
1626
|
+
if (fs12.existsSync(gitDir)) {
|
|
1507
1627
|
const hooksDir = path12.join(gitDir, "hooks");
|
|
1508
|
-
if (!
|
|
1509
|
-
|
|
1628
|
+
if (!fs12.existsSync(hooksDir)) {
|
|
1629
|
+
fs12.mkdirSync(hooksDir, { recursive: true });
|
|
1510
1630
|
}
|
|
1511
1631
|
writeGitHookPreCommit(hooksDir);
|
|
1512
1632
|
console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -1514,10 +1634,10 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1514
1634
|
}
|
|
1515
1635
|
function writeGitHookPreCommit(hooksDir) {
|
|
1516
1636
|
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1517
|
-
if (
|
|
1518
|
-
const existing =
|
|
1637
|
+
if (fs12.existsSync(hookPath)) {
|
|
1638
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1519
1639
|
if (existing.includes("viberails")) return;
|
|
1520
|
-
|
|
1640
|
+
fs12.writeFileSync(
|
|
1521
1641
|
hookPath,
|
|
1522
1642
|
`${existing.trimEnd()}
|
|
1523
1643
|
|
|
@@ -1534,71 +1654,51 @@ npx viberails check --staged
|
|
|
1534
1654
|
"npx viberails check --staged",
|
|
1535
1655
|
""
|
|
1536
1656
|
].join("\n");
|
|
1537
|
-
|
|
1657
|
+
fs12.writeFileSync(hookPath, script, { mode: 493 });
|
|
1538
1658
|
}
|
|
1539
1659
|
function addLefthookPreCommit(lefthookPath) {
|
|
1540
|
-
const content =
|
|
1660
|
+
const content = fs12.readFileSync(lefthookPath, "utf-8");
|
|
1541
1661
|
if (content.includes("viberails")) return;
|
|
1542
|
-
const
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
${commandBlock}
|
|
1549
|
-
`;
|
|
1550
|
-
fs11.writeFileSync(lefthookPath, updated);
|
|
1551
|
-
} else {
|
|
1552
|
-
const section = [
|
|
1553
|
-
"",
|
|
1554
|
-
"pre-commit:",
|
|
1555
|
-
" commands:",
|
|
1556
|
-
" viberails:",
|
|
1557
|
-
" run: npx viberails check --staged"
|
|
1558
|
-
].join("\n");
|
|
1559
|
-
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1560
|
-
${section}
|
|
1561
|
-
`);
|
|
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 = {};
|
|
1562
1668
|
}
|
|
1669
|
+
doc["pre-commit"].commands.viberails = {
|
|
1670
|
+
run: "npx viberails check --staged"
|
|
1671
|
+
};
|
|
1672
|
+
fs12.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
1563
1673
|
}
|
|
1564
1674
|
function detectHookManager(projectRoot) {
|
|
1565
|
-
if (
|
|
1566
|
-
if (
|
|
1567
|
-
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";
|
|
1568
1678
|
return void 0;
|
|
1569
1679
|
}
|
|
1570
1680
|
function setupClaudeCodeHook(projectRoot) {
|
|
1571
1681
|
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1572
|
-
if (!
|
|
1573
|
-
|
|
1682
|
+
if (!fs12.existsSync(claudeDir)) {
|
|
1683
|
+
fs12.mkdirSync(claudeDir, { recursive: true });
|
|
1574
1684
|
}
|
|
1575
1685
|
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1576
1686
|
let settings = {};
|
|
1577
|
-
if (
|
|
1687
|
+
if (fs12.existsSync(settingsPath)) {
|
|
1578
1688
|
try {
|
|
1579
|
-
settings = JSON.parse(
|
|
1689
|
+
settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
1580
1690
|
} catch {
|
|
1581
1691
|
console.warn(
|
|
1582
|
-
` ${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`
|
|
1583
1693
|
);
|
|
1584
|
-
|
|
1694
|
+
console.warn(` Fix the JSON manually, then re-run ${import_chalk7.default.cyan("viberails init --force")}`);
|
|
1695
|
+
return;
|
|
1585
1696
|
}
|
|
1586
1697
|
}
|
|
1587
1698
|
const hooks = settings.hooks ?? {};
|
|
1588
1699
|
const existing = hooks.PostToolUse ?? [];
|
|
1589
1700
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1590
|
-
const
|
|
1591
|
-
const checkAndReport = [
|
|
1592
|
-
`FILE=$(${extractFile})`,
|
|
1593
|
-
'if [ -z "$FILE" ]; then exit 0; fi',
|
|
1594
|
-
'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
|
|
1595
|
-
`if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
|
|
1596
|
-
' echo "$OUTPUT" >&2',
|
|
1597
|
-
" exit 2",
|
|
1598
|
-
"fi",
|
|
1599
|
-
"exit 0"
|
|
1600
|
-
].join("\n");
|
|
1601
|
-
const hookCommand = checkAndReport;
|
|
1701
|
+
const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
|
|
1602
1702
|
hooks.PostToolUse = [
|
|
1603
1703
|
...existing,
|
|
1604
1704
|
{
|
|
@@ -1612,34 +1712,34 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1612
1712
|
}
|
|
1613
1713
|
];
|
|
1614
1714
|
settings.hooks = hooks;
|
|
1615
|
-
|
|
1715
|
+
fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1616
1716
|
`);
|
|
1617
1717
|
console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1618
1718
|
}
|
|
1619
1719
|
function setupClaudeMdReference(projectRoot) {
|
|
1620
1720
|
const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
|
|
1621
1721
|
let content = "";
|
|
1622
|
-
if (
|
|
1623
|
-
content =
|
|
1722
|
+
if (fs12.existsSync(claudeMdPath)) {
|
|
1723
|
+
content = fs12.readFileSync(claudeMdPath, "utf-8");
|
|
1624
1724
|
}
|
|
1625
1725
|
if (content.includes("@.viberails/context.md")) return;
|
|
1626
1726
|
const ref = "\n@.viberails/context.md\n";
|
|
1627
1727
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1628
|
-
|
|
1728
|
+
fs12.writeFileSync(claudeMdPath, prefix + ref);
|
|
1629
1729
|
console.log(` ${import_chalk7.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
1630
1730
|
}
|
|
1631
1731
|
function writeHuskyPreCommit(huskyDir) {
|
|
1632
1732
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1633
|
-
if (
|
|
1634
|
-
const existing =
|
|
1733
|
+
if (fs12.existsSync(hookPath)) {
|
|
1734
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1635
1735
|
if (!existing.includes("viberails")) {
|
|
1636
|
-
|
|
1736
|
+
fs12.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1637
1737
|
npx viberails check --staged
|
|
1638
1738
|
`);
|
|
1639
1739
|
}
|
|
1640
1740
|
return;
|
|
1641
1741
|
}
|
|
1642
|
-
|
|
1742
|
+
fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1643
1743
|
}
|
|
1644
1744
|
|
|
1645
1745
|
// src/commands/init.ts
|
|
@@ -1660,10 +1760,6 @@ function getConventionStr3(cv) {
|
|
|
1660
1760
|
if (!cv) return void 0;
|
|
1661
1761
|
return typeof cv === "string" ? cv : cv.value;
|
|
1662
1762
|
}
|
|
1663
|
-
function hasConventionOverrides(config) {
|
|
1664
|
-
if (!config.packages || config.packages.length === 0) return false;
|
|
1665
|
-
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1666
|
-
}
|
|
1667
1763
|
async function initCommand(options, cwd) {
|
|
1668
1764
|
const startDir = cwd ?? process.cwd();
|
|
1669
1765
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1673,7 +1769,7 @@ async function initCommand(options, cwd) {
|
|
|
1673
1769
|
);
|
|
1674
1770
|
}
|
|
1675
1771
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1676
|
-
if (
|
|
1772
|
+
if (fs13.existsSync(configPath) && !options.force) {
|
|
1677
1773
|
console.log(
|
|
1678
1774
|
`${import_chalk8.default.yellow("!")} viberails is already initialized.
|
|
1679
1775
|
Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
|
|
@@ -1703,7 +1799,7 @@ async function initCommand(options, cwd) {
|
|
|
1703
1799
|
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1704
1800
|
}
|
|
1705
1801
|
}
|
|
1706
|
-
|
|
1802
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1707
1803
|
`);
|
|
1708
1804
|
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1709
1805
|
updateGitignore(projectRoot);
|
|
@@ -1730,27 +1826,18 @@ Created:`);
|
|
|
1730
1826
|
clack2.note(resultsText, "Scan results");
|
|
1731
1827
|
const decision = await promptInitDecision();
|
|
1732
1828
|
if (decision === "customize") {
|
|
1733
|
-
|
|
1734
|
-
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1735
|
-
"Rules"
|
|
1736
|
-
);
|
|
1737
|
-
const overrides = await promptRuleCustomization({
|
|
1829
|
+
const overrides = await promptRuleMenu({
|
|
1738
1830
|
maxFileLines: config.rules.maxFileLines,
|
|
1739
1831
|
requireTests: config.rules.requireTests,
|
|
1740
1832
|
enforceNaming: config.rules.enforceNaming,
|
|
1741
1833
|
enforcement: config.enforcement,
|
|
1742
|
-
fileNamingValue: getConventionStr3(config.conventions.fileNaming)
|
|
1834
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming),
|
|
1835
|
+
packageOverrides: config.packages
|
|
1743
1836
|
});
|
|
1744
1837
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1745
1838
|
config.rules.requireTests = overrides.requireTests;
|
|
1746
1839
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1747
1840
|
config.enforcement = overrides.enforcement;
|
|
1748
|
-
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1749
|
-
clack2.note(
|
|
1750
|
-
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1751
|
-
"Per-package overrides"
|
|
1752
|
-
);
|
|
1753
|
-
}
|
|
1754
1841
|
}
|
|
1755
1842
|
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1756
1843
|
clack2.note(
|
|
@@ -1780,13 +1867,7 @@ Created:`);
|
|
|
1780
1867
|
}
|
|
1781
1868
|
const hookManager = detectHookManager(projectRoot);
|
|
1782
1869
|
const integrations = await promptIntegrations(hookManager);
|
|
1783
|
-
|
|
1784
|
-
clack2.note(
|
|
1785
|
-
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1786
|
-
"Per-package conventions"
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1870
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1790
1871
|
`);
|
|
1791
1872
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1792
1873
|
updateGitignore(projectRoot);
|
|
@@ -1797,8 +1878,7 @@ Created:`);
|
|
|
1797
1878
|
];
|
|
1798
1879
|
if (integrations.preCommitHook) {
|
|
1799
1880
|
setupPreCommitHook(projectRoot);
|
|
1800
|
-
|
|
1801
|
-
if (hookMgr) {
|
|
1881
|
+
if (hookManager === "Lefthook") {
|
|
1802
1882
|
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1803
1883
|
}
|
|
1804
1884
|
}
|
|
@@ -1817,19 +1897,19 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
|
1817
1897
|
function updateGitignore(projectRoot) {
|
|
1818
1898
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
1819
1899
|
let content = "";
|
|
1820
|
-
if (
|
|
1821
|
-
content =
|
|
1900
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
1901
|
+
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1822
1902
|
}
|
|
1823
1903
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1824
1904
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1825
1905
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1826
1906
|
`;
|
|
1827
|
-
|
|
1907
|
+
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1828
1908
|
}
|
|
1829
1909
|
}
|
|
1830
1910
|
|
|
1831
1911
|
// src/commands/sync.ts
|
|
1832
|
-
var
|
|
1912
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
1833
1913
|
var path14 = __toESM(require("path"), 1);
|
|
1834
1914
|
var import_config5 = require("@viberails/config");
|
|
1835
1915
|
var import_scanner2 = require("@viberails/scanner");
|
|
@@ -1964,7 +2044,7 @@ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
|
1964
2044
|
function loadPreviousStats(projectRoot) {
|
|
1965
2045
|
const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
|
|
1966
2046
|
try {
|
|
1967
|
-
const raw =
|
|
2047
|
+
const raw = fs14.readFileSync(scanResultPath, "utf-8");
|
|
1968
2048
|
const parsed = JSON.parse(raw);
|
|
1969
2049
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
1970
2050
|
return parsed.statistics;
|
|
@@ -2003,7 +2083,7 @@ ${import_chalk9.default.bold("Changes:")}`);
|
|
|
2003
2083
|
console.log(` ${import_chalk9.default.dim(statsDelta)}`);
|
|
2004
2084
|
}
|
|
2005
2085
|
}
|
|
2006
|
-
|
|
2086
|
+
fs14.writeFileSync(configPath, `${mergedJson}
|
|
2007
2087
|
`);
|
|
2008
2088
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
2009
2089
|
console.log(`
|
|
@@ -2018,7 +2098,7 @@ ${import_chalk9.default.bold("Synced:")}`);
|
|
|
2018
2098
|
}
|
|
2019
2099
|
|
|
2020
2100
|
// src/index.ts
|
|
2021
|
-
var VERSION = "0.
|
|
2101
|
+
var VERSION = "0.4.0";
|
|
2022
2102
|
var program = new import_commander.Command();
|
|
2023
2103
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
2024
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) => {
|
|
@@ -2039,9 +2119,13 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
2039
2119
|
process.exit(1);
|
|
2040
2120
|
}
|
|
2041
2121
|
});
|
|
2042
|
-
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(
|
|
2043
2123
|
async (options) => {
|
|
2044
2124
|
try {
|
|
2125
|
+
if (options.hook) {
|
|
2126
|
+
const exitCode2 = await hookCheckCommand();
|
|
2127
|
+
process.exit(exitCode2);
|
|
2128
|
+
}
|
|
2045
2129
|
const exitCode = await checkCommand({
|
|
2046
2130
|
...options,
|
|
2047
2131
|
noBoundaries: options.boundaries === false,
|