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.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) {
|
|
@@ -560,7 +625,13 @@ async function checkCommand(options, cwd) {
|
|
|
560
625
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
561
626
|
}
|
|
562
627
|
if (filesToCheck.length === 0) {
|
|
563
|
-
|
|
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
|
+
}
|
|
564
635
|
return 0;
|
|
565
636
|
}
|
|
566
637
|
const violations = [];
|
|
@@ -622,7 +693,9 @@ async function checkCommand(options, cwd) {
|
|
|
622
693
|
});
|
|
623
694
|
}
|
|
624
695
|
const elapsed = Date.now() - startTime;
|
|
625
|
-
|
|
696
|
+
if (options.format !== "json") {
|
|
697
|
+
console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
698
|
+
}
|
|
626
699
|
}
|
|
627
700
|
if (options.format === "json") {
|
|
628
701
|
console.log(
|
|
@@ -649,8 +722,54 @@ async function checkCommand(options, cwd) {
|
|
|
649
722
|
return 0;
|
|
650
723
|
}
|
|
651
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
|
+
|
|
652
771
|
// src/commands/fix.ts
|
|
653
|
-
import * as
|
|
772
|
+
import * as fs10 from "fs";
|
|
654
773
|
import * as path10 from "path";
|
|
655
774
|
import { loadConfig as loadConfig3 } from "@viberails/config";
|
|
656
775
|
import chalk4 from "chalk";
|
|
@@ -793,7 +912,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
793
912
|
}
|
|
794
913
|
|
|
795
914
|
// src/commands/fix-naming.ts
|
|
796
|
-
import * as
|
|
915
|
+
import * as fs8 from "fs";
|
|
797
916
|
import * as path8 from "path";
|
|
798
917
|
|
|
799
918
|
// src/commands/convert-name.ts
|
|
@@ -855,12 +974,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
855
974
|
const newRelPath = path8.join(dir, newFilename);
|
|
856
975
|
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
857
976
|
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
858
|
-
if (
|
|
977
|
+
if (fs8.existsSync(newAbsPath)) return null;
|
|
859
978
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
860
979
|
}
|
|
861
980
|
function executeRename(rename) {
|
|
862
|
-
if (
|
|
863
|
-
|
|
981
|
+
if (fs8.existsSync(rename.newAbsPath)) return false;
|
|
982
|
+
fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
864
983
|
return true;
|
|
865
984
|
}
|
|
866
985
|
function deduplicateRenames(renames) {
|
|
@@ -875,7 +994,7 @@ function deduplicateRenames(renames) {
|
|
|
875
994
|
}
|
|
876
995
|
|
|
877
996
|
// src/commands/fix-tests.ts
|
|
878
|
-
import * as
|
|
997
|
+
import * as fs9 from "fs";
|
|
879
998
|
import * as path9 from "path";
|
|
880
999
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
881
1000
|
const { testPattern } = config.structure;
|
|
@@ -886,7 +1005,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
886
1005
|
const testFilename = `${stem}${testSuffix}`;
|
|
887
1006
|
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
888
1007
|
const testAbsPath = path9.join(dir, testFilename);
|
|
889
|
-
if (
|
|
1008
|
+
if (fs9.existsSync(testAbsPath)) return null;
|
|
890
1009
|
return {
|
|
891
1010
|
path: path9.relative(projectRoot, testAbsPath),
|
|
892
1011
|
absPath: testAbsPath,
|
|
@@ -900,8 +1019,8 @@ function writeTestStub(stub, config) {
|
|
|
900
1019
|
it.todo('add tests');
|
|
901
1020
|
});
|
|
902
1021
|
`;
|
|
903
|
-
|
|
904
|
-
|
|
1022
|
+
fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
1023
|
+
fs9.writeFileSync(stub.absPath, content);
|
|
905
1024
|
}
|
|
906
1025
|
|
|
907
1026
|
// src/commands/fix.ts
|
|
@@ -914,7 +1033,7 @@ async function fixCommand(options, cwd) {
|
|
|
914
1033
|
return 1;
|
|
915
1034
|
}
|
|
916
1035
|
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
917
|
-
if (!
|
|
1036
|
+
if (!fs10.existsSync(configPath)) {
|
|
918
1037
|
console.error(
|
|
919
1038
|
`${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
920
1039
|
);
|
|
@@ -978,13 +1097,13 @@ async function fixCommand(options, cwd) {
|
|
|
978
1097
|
}
|
|
979
1098
|
let importUpdateCount = 0;
|
|
980
1099
|
if (renameCount > 0) {
|
|
981
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1100
|
+
const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
|
|
982
1101
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
983
1102
|
importUpdateCount = updates.length;
|
|
984
1103
|
}
|
|
985
1104
|
let stubCount = 0;
|
|
986
1105
|
for (const stub of testStubs) {
|
|
987
|
-
if (!
|
|
1106
|
+
if (!fs10.existsSync(stub.absPath)) {
|
|
988
1107
|
writeTestStub(stub, config);
|
|
989
1108
|
stubCount++;
|
|
990
1109
|
}
|
|
@@ -1005,7 +1124,7 @@ async function fixCommand(options, cwd) {
|
|
|
1005
1124
|
}
|
|
1006
1125
|
|
|
1007
1126
|
// src/commands/init.ts
|
|
1008
|
-
import * as
|
|
1127
|
+
import * as fs13 from "fs";
|
|
1009
1128
|
import * as path13 from "path";
|
|
1010
1129
|
import * as clack2 from "@clack/prompts";
|
|
1011
1130
|
import { generateConfig } from "@viberails/config";
|
|
@@ -1439,7 +1558,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1439
1558
|
}
|
|
1440
1559
|
|
|
1441
1560
|
// src/utils/write-generated-files.ts
|
|
1442
|
-
import * as
|
|
1561
|
+
import * as fs11 from "fs";
|
|
1443
1562
|
import * as path11 from "path";
|
|
1444
1563
|
import { generateContext } from "@viberails/context";
|
|
1445
1564
|
var CONTEXT_DIR = ".viberails";
|
|
@@ -1448,12 +1567,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
|
|
|
1448
1567
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1449
1568
|
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1450
1569
|
try {
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
1570
|
+
if (!fs11.existsSync(contextDir)) {
|
|
1571
|
+
fs11.mkdirSync(contextDir, { recursive: true });
|
|
1453
1572
|
}
|
|
1454
1573
|
const context = generateContext(config);
|
|
1455
|
-
|
|
1456
|
-
|
|
1574
|
+
fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1575
|
+
fs11.writeFileSync(
|
|
1457
1576
|
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1458
1577
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1459
1578
|
`
|
|
@@ -1465,27 +1584,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1465
1584
|
}
|
|
1466
1585
|
|
|
1467
1586
|
// src/commands/init-hooks.ts
|
|
1468
|
-
import * as
|
|
1587
|
+
import * as fs12 from "fs";
|
|
1469
1588
|
import * as path12 from "path";
|
|
1470
1589
|
import chalk7 from "chalk";
|
|
1590
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1471
1591
|
function setupPreCommitHook(projectRoot) {
|
|
1472
1592
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1473
|
-
if (
|
|
1593
|
+
if (fs12.existsSync(lefthookPath)) {
|
|
1474
1594
|
addLefthookPreCommit(lefthookPath);
|
|
1475
1595
|
console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1476
1596
|
return;
|
|
1477
1597
|
}
|
|
1478
1598
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1479
|
-
if (
|
|
1599
|
+
if (fs12.existsSync(huskyDir)) {
|
|
1480
1600
|
writeHuskyPreCommit(huskyDir);
|
|
1481
1601
|
console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1482
1602
|
return;
|
|
1483
1603
|
}
|
|
1484
1604
|
const gitDir = path12.join(projectRoot, ".git");
|
|
1485
|
-
if (
|
|
1605
|
+
if (fs12.existsSync(gitDir)) {
|
|
1486
1606
|
const hooksDir = path12.join(gitDir, "hooks");
|
|
1487
|
-
if (!
|
|
1488
|
-
|
|
1607
|
+
if (!fs12.existsSync(hooksDir)) {
|
|
1608
|
+
fs12.mkdirSync(hooksDir, { recursive: true });
|
|
1489
1609
|
}
|
|
1490
1610
|
writeGitHookPreCommit(hooksDir);
|
|
1491
1611
|
console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -1493,10 +1613,10 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1493
1613
|
}
|
|
1494
1614
|
function writeGitHookPreCommit(hooksDir) {
|
|
1495
1615
|
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1496
|
-
if (
|
|
1497
|
-
const existing =
|
|
1616
|
+
if (fs12.existsSync(hookPath)) {
|
|
1617
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1498
1618
|
if (existing.includes("viberails")) return;
|
|
1499
|
-
|
|
1619
|
+
fs12.writeFileSync(
|
|
1500
1620
|
hookPath,
|
|
1501
1621
|
`${existing.trimEnd()}
|
|
1502
1622
|
|
|
@@ -1513,71 +1633,51 @@ npx viberails check --staged
|
|
|
1513
1633
|
"npx viberails check --staged",
|
|
1514
1634
|
""
|
|
1515
1635
|
].join("\n");
|
|
1516
|
-
|
|
1636
|
+
fs12.writeFileSync(hookPath, script, { mode: 493 });
|
|
1517
1637
|
}
|
|
1518
1638
|
function addLefthookPreCommit(lefthookPath) {
|
|
1519
|
-
const content =
|
|
1639
|
+
const content = fs12.readFileSync(lefthookPath, "utf-8");
|
|
1520
1640
|
if (content.includes("viberails")) return;
|
|
1521
|
-
const
|
|
1522
|
-
if (
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
${commandBlock}
|
|
1528
|
-
`;
|
|
1529
|
-
fs11.writeFileSync(lefthookPath, updated);
|
|
1530
|
-
} else {
|
|
1531
|
-
const section = [
|
|
1532
|
-
"",
|
|
1533
|
-
"pre-commit:",
|
|
1534
|
-
" commands:",
|
|
1535
|
-
" viberails:",
|
|
1536
|
-
" run: npx viberails check --staged"
|
|
1537
|
-
].join("\n");
|
|
1538
|
-
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1539
|
-
${section}
|
|
1540
|
-
`);
|
|
1641
|
+
const doc = parseYaml(content) ?? {};
|
|
1642
|
+
if (!doc["pre-commit"]) {
|
|
1643
|
+
doc["pre-commit"] = { commands: {} };
|
|
1644
|
+
}
|
|
1645
|
+
if (!doc["pre-commit"].commands) {
|
|
1646
|
+
doc["pre-commit"].commands = {};
|
|
1541
1647
|
}
|
|
1648
|
+
doc["pre-commit"].commands.viberails = {
|
|
1649
|
+
run: "npx viberails check --staged"
|
|
1650
|
+
};
|
|
1651
|
+
fs12.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
1542
1652
|
}
|
|
1543
1653
|
function detectHookManager(projectRoot) {
|
|
1544
|
-
if (
|
|
1545
|
-
if (
|
|
1546
|
-
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";
|
|
1547
1657
|
return void 0;
|
|
1548
1658
|
}
|
|
1549
1659
|
function setupClaudeCodeHook(projectRoot) {
|
|
1550
1660
|
const claudeDir = path12.join(projectRoot, ".claude");
|
|
1551
|
-
if (!
|
|
1552
|
-
|
|
1661
|
+
if (!fs12.existsSync(claudeDir)) {
|
|
1662
|
+
fs12.mkdirSync(claudeDir, { recursive: true });
|
|
1553
1663
|
}
|
|
1554
1664
|
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1555
1665
|
let settings = {};
|
|
1556
|
-
if (
|
|
1666
|
+
if (fs12.existsSync(settingsPath)) {
|
|
1557
1667
|
try {
|
|
1558
|
-
settings = JSON.parse(
|
|
1668
|
+
settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
|
|
1559
1669
|
} catch {
|
|
1560
1670
|
console.warn(
|
|
1561
|
-
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014
|
|
1671
|
+
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
1562
1672
|
);
|
|
1563
|
-
|
|
1673
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk7.cyan("viberails init --force")}`);
|
|
1674
|
+
return;
|
|
1564
1675
|
}
|
|
1565
1676
|
}
|
|
1566
1677
|
const hooks = settings.hooks ?? {};
|
|
1567
1678
|
const existing = hooks.PostToolUse ?? [];
|
|
1568
1679
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1569
|
-
const
|
|
1570
|
-
const checkAndReport = [
|
|
1571
|
-
`FILE=$(${extractFile})`,
|
|
1572
|
-
'if [ -z "$FILE" ]; then exit 0; fi',
|
|
1573
|
-
'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
|
|
1574
|
-
`if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
|
|
1575
|
-
' echo "$OUTPUT" >&2',
|
|
1576
|
-
" exit 2",
|
|
1577
|
-
"fi",
|
|
1578
|
-
"exit 0"
|
|
1579
|
-
].join("\n");
|
|
1580
|
-
const hookCommand = checkAndReport;
|
|
1680
|
+
const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
|
|
1581
1681
|
hooks.PostToolUse = [
|
|
1582
1682
|
...existing,
|
|
1583
1683
|
{
|
|
@@ -1591,34 +1691,34 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1591
1691
|
}
|
|
1592
1692
|
];
|
|
1593
1693
|
settings.hooks = hooks;
|
|
1594
|
-
|
|
1694
|
+
fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1595
1695
|
`);
|
|
1596
1696
|
console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1597
1697
|
}
|
|
1598
1698
|
function setupClaudeMdReference(projectRoot) {
|
|
1599
1699
|
const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
|
|
1600
1700
|
let content = "";
|
|
1601
|
-
if (
|
|
1602
|
-
content =
|
|
1701
|
+
if (fs12.existsSync(claudeMdPath)) {
|
|
1702
|
+
content = fs12.readFileSync(claudeMdPath, "utf-8");
|
|
1603
1703
|
}
|
|
1604
1704
|
if (content.includes("@.viberails/context.md")) return;
|
|
1605
1705
|
const ref = "\n@.viberails/context.md\n";
|
|
1606
1706
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1607
|
-
|
|
1707
|
+
fs12.writeFileSync(claudeMdPath, prefix + ref);
|
|
1608
1708
|
console.log(` ${chalk7.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
1609
1709
|
}
|
|
1610
1710
|
function writeHuskyPreCommit(huskyDir) {
|
|
1611
1711
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1612
|
-
if (
|
|
1613
|
-
const existing =
|
|
1712
|
+
if (fs12.existsSync(hookPath)) {
|
|
1713
|
+
const existing = fs12.readFileSync(hookPath, "utf-8");
|
|
1614
1714
|
if (!existing.includes("viberails")) {
|
|
1615
|
-
|
|
1715
|
+
fs12.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1616
1716
|
npx viberails check --staged
|
|
1617
1717
|
`);
|
|
1618
1718
|
}
|
|
1619
1719
|
return;
|
|
1620
1720
|
}
|
|
1621
|
-
|
|
1721
|
+
fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1622
1722
|
}
|
|
1623
1723
|
|
|
1624
1724
|
// src/commands/init.ts
|
|
@@ -1639,10 +1739,6 @@ function getConventionStr3(cv) {
|
|
|
1639
1739
|
if (!cv) return void 0;
|
|
1640
1740
|
return typeof cv === "string" ? cv : cv.value;
|
|
1641
1741
|
}
|
|
1642
|
-
function hasConventionOverrides(config) {
|
|
1643
|
-
if (!config.packages || config.packages.length === 0) return false;
|
|
1644
|
-
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1645
|
-
}
|
|
1646
1742
|
async function initCommand(options, cwd) {
|
|
1647
1743
|
const startDir = cwd ?? process.cwd();
|
|
1648
1744
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1652,7 +1748,7 @@ async function initCommand(options, cwd) {
|
|
|
1652
1748
|
);
|
|
1653
1749
|
}
|
|
1654
1750
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1655
|
-
if (
|
|
1751
|
+
if (fs13.existsSync(configPath) && !options.force) {
|
|
1656
1752
|
console.log(
|
|
1657
1753
|
`${chalk8.yellow("!")} viberails is already initialized.
|
|
1658
1754
|
Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
|
|
@@ -1682,7 +1778,7 @@ async function initCommand(options, cwd) {
|
|
|
1682
1778
|
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1683
1779
|
}
|
|
1684
1780
|
}
|
|
1685
|
-
|
|
1781
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1686
1782
|
`);
|
|
1687
1783
|
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1688
1784
|
updateGitignore(projectRoot);
|
|
@@ -1709,27 +1805,18 @@ Created:`);
|
|
|
1709
1805
|
clack2.note(resultsText, "Scan results");
|
|
1710
1806
|
const decision = await promptInitDecision();
|
|
1711
1807
|
if (decision === "customize") {
|
|
1712
|
-
|
|
1713
|
-
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1714
|
-
"Rules"
|
|
1715
|
-
);
|
|
1716
|
-
const overrides = await promptRuleCustomization({
|
|
1808
|
+
const overrides = await promptRuleMenu({
|
|
1717
1809
|
maxFileLines: config.rules.maxFileLines,
|
|
1718
1810
|
requireTests: config.rules.requireTests,
|
|
1719
1811
|
enforceNaming: config.rules.enforceNaming,
|
|
1720
1812
|
enforcement: config.enforcement,
|
|
1721
|
-
fileNamingValue: getConventionStr3(config.conventions.fileNaming)
|
|
1813
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming),
|
|
1814
|
+
packageOverrides: config.packages
|
|
1722
1815
|
});
|
|
1723
1816
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1724
1817
|
config.rules.requireTests = overrides.requireTests;
|
|
1725
1818
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1726
1819
|
config.enforcement = overrides.enforcement;
|
|
1727
|
-
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1728
|
-
clack2.note(
|
|
1729
|
-
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1730
|
-
"Per-package overrides"
|
|
1731
|
-
);
|
|
1732
|
-
}
|
|
1733
1820
|
}
|
|
1734
1821
|
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1735
1822
|
clack2.note(
|
|
@@ -1759,13 +1846,7 @@ Created:`);
|
|
|
1759
1846
|
}
|
|
1760
1847
|
const hookManager = detectHookManager(projectRoot);
|
|
1761
1848
|
const integrations = await promptIntegrations(hookManager);
|
|
1762
|
-
|
|
1763
|
-
clack2.note(
|
|
1764
|
-
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1765
|
-
"Per-package conventions"
|
|
1766
|
-
);
|
|
1767
|
-
}
|
|
1768
|
-
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1849
|
+
fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1769
1850
|
`);
|
|
1770
1851
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1771
1852
|
updateGitignore(projectRoot);
|
|
@@ -1776,8 +1857,7 @@ Created:`);
|
|
|
1776
1857
|
];
|
|
1777
1858
|
if (integrations.preCommitHook) {
|
|
1778
1859
|
setupPreCommitHook(projectRoot);
|
|
1779
|
-
|
|
1780
|
-
if (hookMgr) {
|
|
1860
|
+
if (hookManager === "Lefthook") {
|
|
1781
1861
|
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1782
1862
|
}
|
|
1783
1863
|
}
|
|
@@ -1796,19 +1876,19 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
|
1796
1876
|
function updateGitignore(projectRoot) {
|
|
1797
1877
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
1798
1878
|
let content = "";
|
|
1799
|
-
if (
|
|
1800
|
-
content =
|
|
1879
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
1880
|
+
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1801
1881
|
}
|
|
1802
1882
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
1803
1883
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1804
1884
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1805
1885
|
`;
|
|
1806
|
-
|
|
1886
|
+
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1807
1887
|
}
|
|
1808
1888
|
}
|
|
1809
1889
|
|
|
1810
1890
|
// src/commands/sync.ts
|
|
1811
|
-
import * as
|
|
1891
|
+
import * as fs14 from "fs";
|
|
1812
1892
|
import * as path14 from "path";
|
|
1813
1893
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1814
1894
|
import { scan as scan2 } from "@viberails/scanner";
|
|
@@ -1943,7 +2023,7 @@ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
|
1943
2023
|
function loadPreviousStats(projectRoot) {
|
|
1944
2024
|
const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
|
|
1945
2025
|
try {
|
|
1946
|
-
const raw =
|
|
2026
|
+
const raw = fs14.readFileSync(scanResultPath, "utf-8");
|
|
1947
2027
|
const parsed = JSON.parse(raw);
|
|
1948
2028
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
1949
2029
|
return parsed.statistics;
|
|
@@ -1982,7 +2062,7 @@ ${chalk9.bold("Changes:")}`);
|
|
|
1982
2062
|
console.log(` ${chalk9.dim(statsDelta)}`);
|
|
1983
2063
|
}
|
|
1984
2064
|
}
|
|
1985
|
-
|
|
2065
|
+
fs14.writeFileSync(configPath, `${mergedJson}
|
|
1986
2066
|
`);
|
|
1987
2067
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1988
2068
|
console.log(`
|
|
@@ -1997,7 +2077,7 @@ ${chalk9.bold("Synced:")}`);
|
|
|
1997
2077
|
}
|
|
1998
2078
|
|
|
1999
2079
|
// src/index.ts
|
|
2000
|
-
var VERSION = "0.
|
|
2080
|
+
var VERSION = "0.4.0";
|
|
2001
2081
|
var program = new Command();
|
|
2002
2082
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
2003
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) => {
|
|
@@ -2018,9 +2098,13 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
2018
2098
|
process.exit(1);
|
|
2019
2099
|
}
|
|
2020
2100
|
});
|
|
2021
|
-
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(
|
|
2022
2102
|
async (options) => {
|
|
2023
2103
|
try {
|
|
2104
|
+
if (options.hook) {
|
|
2105
|
+
const exitCode2 = await hookCheckCommand();
|
|
2106
|
+
process.exit(exitCode2);
|
|
2107
|
+
}
|
|
2024
2108
|
const exitCode = await checkCommand({
|
|
2025
2109
|
...options,
|
|
2026
2110
|
noBoundaries: options.boundaries === false,
|