viberails 0.5.4 → 0.6.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 +573 -318
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +573 -318
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk14 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -62,11 +62,11 @@ async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace
|
|
|
62
62
|
stdio: "pipe"
|
|
63
63
|
});
|
|
64
64
|
if (result.status === 0) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const lefthookPath =
|
|
68
|
-
if (!
|
|
69
|
-
|
|
65
|
+
const fs21 = await import("fs");
|
|
66
|
+
const path21 = await import("path");
|
|
67
|
+
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
68
|
+
if (!fs21.existsSync(lefthookPath)) {
|
|
69
|
+
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
70
70
|
}
|
|
71
71
|
s.stop("Installed Lefthook");
|
|
72
72
|
return "Lefthook";
|
|
@@ -98,7 +98,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
|
98
98
|
options.push({
|
|
99
99
|
value: "typecheck",
|
|
100
100
|
label: "Typecheck (tsc --noEmit)",
|
|
101
|
-
hint: "
|
|
101
|
+
hint: "pre-commit hook + CI check"
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
if (tools?.linter) {
|
|
@@ -106,7 +106,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
|
106
106
|
options.push({
|
|
107
107
|
value: "lint",
|
|
108
108
|
label: `Lint check (${linterName})`,
|
|
109
|
-
hint: "
|
|
109
|
+
hint: "pre-commit hook + CI check"
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
options.push(
|
|
@@ -698,7 +698,7 @@ ${chalk.yellow("Cycles detected:")}`);
|
|
|
698
698
|
import * as fs7 from "fs";
|
|
699
699
|
import * as path7 from "path";
|
|
700
700
|
import { loadConfig as loadConfig2 } from "@viberails/config";
|
|
701
|
-
import
|
|
701
|
+
import chalk3 from "chalk";
|
|
702
702
|
|
|
703
703
|
// src/commands/check-config.ts
|
|
704
704
|
import { BUILTIN_IGNORE } from "@viberails/config";
|
|
@@ -789,7 +789,8 @@ function runCoverageCommand(pkgRoot, command) {
|
|
|
789
789
|
cwd: pkgRoot,
|
|
790
790
|
shell: true,
|
|
791
791
|
encoding: "utf-8",
|
|
792
|
-
stdio: "pipe"
|
|
792
|
+
stdio: "pipe",
|
|
793
|
+
timeout: 3e5
|
|
793
794
|
});
|
|
794
795
|
if (result.status === 0) return { ok: true };
|
|
795
796
|
const stderr = result.stderr?.trim() ?? "";
|
|
@@ -952,7 +953,7 @@ function checkNaming(relPath, conventions) {
|
|
|
952
953
|
}
|
|
953
954
|
function getStagedFiles(projectRoot) {
|
|
954
955
|
try {
|
|
955
|
-
const output = execSync("git diff --cached --name-only --diff-filter=
|
|
956
|
+
const output = execSync("git diff --cached --name-only --diff-filter=ACMR", {
|
|
956
957
|
cwd: projectRoot,
|
|
957
958
|
encoding: "utf-8",
|
|
958
959
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -979,7 +980,62 @@ function getDiffFiles(projectRoot, base) {
|
|
|
979
980
|
added: addedOutput.trim().split("\n").filter(Boolean)
|
|
980
981
|
};
|
|
981
982
|
} catch {
|
|
982
|
-
|
|
983
|
+
const msg = `git diff failed for base '${base}' \u2014 no files will be checked`;
|
|
984
|
+
process.stderr.write(`Warning: ${msg}
|
|
985
|
+
`);
|
|
986
|
+
return { all: [], added: [], error: msg };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function testFileToSourceFile(testFile) {
|
|
990
|
+
const match = testFile.match(/^(.+)\.(test|spec)(\.[^.]+)$/);
|
|
991
|
+
if (!match) return null;
|
|
992
|
+
return `${match[1]}${match[3]}`;
|
|
993
|
+
}
|
|
994
|
+
function deletedTestFileToSourceFile(deletedTestFile, config) {
|
|
995
|
+
const normalized = deletedTestFile.replaceAll("\\", "/");
|
|
996
|
+
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
997
|
+
for (const pkg of sortedPackages) {
|
|
998
|
+
const relInPkg = pkg.path === "." ? normalized : normalized.startsWith(`${pkg.path}/`) ? normalized.slice(pkg.path.length + 1) : null;
|
|
999
|
+
if (relInPkg === null) continue;
|
|
1000
|
+
const srcDir = pkg.structure?.srcDir;
|
|
1001
|
+
if (!srcDir) continue;
|
|
1002
|
+
const testsDir = pkg.structure?.tests;
|
|
1003
|
+
if (testsDir && relInPkg.startsWith(`${testsDir}/`)) {
|
|
1004
|
+
const relWithinTests = relInPkg.slice(testsDir.length + 1);
|
|
1005
|
+
const relWithinSrc = testFileToSourceFile(relWithinTests);
|
|
1006
|
+
if (relWithinSrc) {
|
|
1007
|
+
return pkg.path === "." ? path5.posix.join(srcDir, relWithinSrc) : path5.posix.join(pkg.path, srcDir, relWithinSrc);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const colocated = testFileToSourceFile(relInPkg);
|
|
1011
|
+
if (colocated) {
|
|
1012
|
+
return pkg.path === "." ? colocated : path5.posix.join(pkg.path, colocated);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
1018
|
+
try {
|
|
1019
|
+
const output = execSync("git diff --cached --name-only --diff-filter=D", {
|
|
1020
|
+
cwd: projectRoot,
|
|
1021
|
+
encoding: "utf-8",
|
|
1022
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1023
|
+
});
|
|
1024
|
+
return output.trim().split("\n").filter(Boolean).map((file) => deletedTestFileToSourceFile(file, config)).filter((f) => f !== null);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
|
|
1030
|
+
try {
|
|
1031
|
+
const output = execSync(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
|
|
1032
|
+
cwd: projectRoot,
|
|
1033
|
+
encoding: "utf-8",
|
|
1034
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1035
|
+
});
|
|
1036
|
+
return output.trim().split("\n").filter(Boolean).map((file) => deletedTestFileToSourceFile(file, config)).filter((f) => f !== null);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return [];
|
|
983
1039
|
}
|
|
984
1040
|
}
|
|
985
1041
|
function getAllSourceFiles(projectRoot, config) {
|
|
@@ -1022,7 +1078,7 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
1022
1078
|
}
|
|
1023
1079
|
for (const entry of entries) {
|
|
1024
1080
|
if (entry.isDirectory()) {
|
|
1025
|
-
if (entry.name
|
|
1081
|
+
if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;
|
|
1026
1082
|
walk(path5.join(d, entry.name));
|
|
1027
1083
|
} else if (entry.isFile()) {
|
|
1028
1084
|
files.push(path5.relative(projectRoot, path5.join(d, entry.name)));
|
|
@@ -1033,6 +1089,55 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
1033
1089
|
return files;
|
|
1034
1090
|
}
|
|
1035
1091
|
|
|
1092
|
+
// src/commands/check-print.ts
|
|
1093
|
+
import chalk2 from "chalk";
|
|
1094
|
+
function printGroupedViolations(violations, limit) {
|
|
1095
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1096
|
+
for (const v of violations) {
|
|
1097
|
+
const existing = groups.get(v.rule) ?? [];
|
|
1098
|
+
existing.push(v);
|
|
1099
|
+
groups.set(v.rule, existing);
|
|
1100
|
+
}
|
|
1101
|
+
const ruleOrder = [
|
|
1102
|
+
"file-size",
|
|
1103
|
+
"file-naming",
|
|
1104
|
+
"missing-test",
|
|
1105
|
+
"test-coverage",
|
|
1106
|
+
"boundary-violation"
|
|
1107
|
+
];
|
|
1108
|
+
const sortedKeys = [...groups.keys()].sort(
|
|
1109
|
+
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
1110
|
+
);
|
|
1111
|
+
let totalShown = 0;
|
|
1112
|
+
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
1113
|
+
for (const rule of sortedKeys) {
|
|
1114
|
+
const group = groups.get(rule);
|
|
1115
|
+
if (!group) continue;
|
|
1116
|
+
const remaining = totalLimit - totalShown;
|
|
1117
|
+
if (remaining <= 0) break;
|
|
1118
|
+
const toShow = group.slice(0, remaining);
|
|
1119
|
+
const hidden = group.length - toShow.length;
|
|
1120
|
+
for (const v of toShow) {
|
|
1121
|
+
const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
1122
|
+
console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
1123
|
+
}
|
|
1124
|
+
totalShown += toShow.length;
|
|
1125
|
+
if (hidden > 0) {
|
|
1126
|
+
console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function printSummary(violations) {
|
|
1131
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1132
|
+
for (const v of violations) {
|
|
1133
|
+
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
1134
|
+
}
|
|
1135
|
+
const word = violations.length === 1 ? "violation" : "violations";
|
|
1136
|
+
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
1137
|
+
console.log(`
|
|
1138
|
+
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1036
1141
|
// src/commands/check-tests.ts
|
|
1037
1142
|
import * as fs6 from "fs";
|
|
1038
1143
|
import * as path6 from "path";
|
|
@@ -1062,18 +1167,19 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1062
1167
|
const testSuffix = testPattern.replace("*", "");
|
|
1063
1168
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1064
1169
|
for (const relFile of sourceFiles) {
|
|
1065
|
-
const
|
|
1066
|
-
if (
|
|
1170
|
+
const basename9 = path6.basename(relFile);
|
|
1171
|
+
if (basename9.includes(".test.") || basename9.includes(".spec.") || basename9.startsWith("index.") || basename9.endsWith(".d.ts")) {
|
|
1067
1172
|
continue;
|
|
1068
1173
|
}
|
|
1069
|
-
const ext = path6.extname(
|
|
1174
|
+
const ext = path6.extname(basename9);
|
|
1070
1175
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1071
|
-
const stem =
|
|
1176
|
+
const stem = basename9.slice(0, -ext.length);
|
|
1072
1177
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1073
1178
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1074
1179
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
1075
1180
|
const testsDir = pkg.structure?.tests;
|
|
1076
|
-
const
|
|
1181
|
+
const relToSrc = path6.relative(srcPath, path6.join(projectRoot, path6.dirname(relFile)));
|
|
1182
|
+
const dedicatedTest = testsDir ? path6.join(packageRoot2, testsDir, relToSrc, expectedTestFile) : null;
|
|
1077
1183
|
const hasTest = fs6.existsSync(colocatedTest) || dedicatedTest !== null && fs6.existsSync(dedicatedTest);
|
|
1078
1184
|
if (!hasTest) {
|
|
1079
1185
|
violations.push({
|
|
@@ -1103,91 +1209,52 @@ function isTestFile(relPath) {
|
|
|
1103
1209
|
const filename = path7.basename(relPath);
|
|
1104
1210
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
1105
1211
|
}
|
|
1106
|
-
function printGroupedViolations(violations, limit) {
|
|
1107
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1108
|
-
for (const v of violations) {
|
|
1109
|
-
const existing = groups.get(v.rule) ?? [];
|
|
1110
|
-
existing.push(v);
|
|
1111
|
-
groups.set(v.rule, existing);
|
|
1112
|
-
}
|
|
1113
|
-
const ruleOrder = [
|
|
1114
|
-
"file-size",
|
|
1115
|
-
"file-naming",
|
|
1116
|
-
"missing-test",
|
|
1117
|
-
"test-coverage",
|
|
1118
|
-
"boundary-violation"
|
|
1119
|
-
];
|
|
1120
|
-
const sortedKeys = [...groups.keys()].sort(
|
|
1121
|
-
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
1122
|
-
);
|
|
1123
|
-
let totalShown = 0;
|
|
1124
|
-
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
1125
|
-
for (const rule of sortedKeys) {
|
|
1126
|
-
const group = groups.get(rule);
|
|
1127
|
-
if (!group) continue;
|
|
1128
|
-
const remaining = totalLimit - totalShown;
|
|
1129
|
-
if (remaining <= 0) break;
|
|
1130
|
-
const toShow = group.slice(0, remaining);
|
|
1131
|
-
const hidden = group.length - toShow.length;
|
|
1132
|
-
for (const v of toShow) {
|
|
1133
|
-
const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
1134
|
-
console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
1135
|
-
}
|
|
1136
|
-
totalShown += toShow.length;
|
|
1137
|
-
if (hidden > 0) {
|
|
1138
|
-
console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
function printSummary(violations) {
|
|
1143
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1144
|
-
for (const v of violations) {
|
|
1145
|
-
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
1146
|
-
}
|
|
1147
|
-
const word = violations.length === 1 ? "violation" : "violations";
|
|
1148
|
-
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
1149
|
-
console.log(`
|
|
1150
|
-
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
1151
|
-
}
|
|
1152
1212
|
async function checkCommand(options, cwd) {
|
|
1153
1213
|
const startDir = cwd ?? process.cwd();
|
|
1154
1214
|
const projectRoot = findProjectRoot(startDir);
|
|
1155
1215
|
if (!projectRoot) {
|
|
1156
|
-
console.error(`${
|
|
1216
|
+
console.error(`${chalk3.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
1157
1217
|
return 1;
|
|
1158
1218
|
}
|
|
1159
1219
|
const configPath = path7.join(projectRoot, CONFIG_FILE2);
|
|
1160
1220
|
if (!fs7.existsSync(configPath)) {
|
|
1161
1221
|
console.error(
|
|
1162
|
-
`${
|
|
1222
|
+
`${chalk3.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
1163
1223
|
);
|
|
1164
1224
|
return 1;
|
|
1165
1225
|
}
|
|
1166
1226
|
const config = await loadConfig2(configPath);
|
|
1167
1227
|
let filesToCheck;
|
|
1168
1228
|
let diffAddedFiles = null;
|
|
1229
|
+
let deletedTestSourceFiles = [];
|
|
1169
1230
|
if (options.staged) {
|
|
1170
|
-
filesToCheck = getStagedFiles(projectRoot);
|
|
1231
|
+
filesToCheck = getStagedFiles(projectRoot).filter((f) => SOURCE_EXTS.has(path7.extname(f)));
|
|
1232
|
+
deletedTestSourceFiles = getStagedDeletedTestSourceFiles(projectRoot, config);
|
|
1171
1233
|
} else if (options.diffBase) {
|
|
1172
1234
|
const diff = getDiffFiles(projectRoot, options.diffBase);
|
|
1235
|
+
if (diff.error && options.enforce) {
|
|
1236
|
+
console.error(`${chalk3.red("Error:")} ${diff.error}`);
|
|
1237
|
+
return 1;
|
|
1238
|
+
}
|
|
1173
1239
|
filesToCheck = diff.all.filter((f) => SOURCE_EXTS.has(path7.extname(f)));
|
|
1174
1240
|
diffAddedFiles = new Set(diff.added);
|
|
1241
|
+
deletedTestSourceFiles = getDiffDeletedTestSourceFiles(projectRoot, options.diffBase, config);
|
|
1175
1242
|
} else if (options.files && options.files.length > 0) {
|
|
1176
1243
|
filesToCheck = options.files;
|
|
1177
1244
|
} else {
|
|
1178
1245
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
1179
1246
|
}
|
|
1180
|
-
if (filesToCheck.length === 0) {
|
|
1247
|
+
if (filesToCheck.length === 0 && deletedTestSourceFiles.length === 0) {
|
|
1181
1248
|
if (options.format === "json") {
|
|
1182
1249
|
console.log(JSON.stringify({ violations: [], checkedFiles: 0 }));
|
|
1183
1250
|
} else {
|
|
1184
|
-
console.log(`${
|
|
1251
|
+
console.log(`${chalk3.green("\u2713")} No files to check.`);
|
|
1185
1252
|
}
|
|
1186
1253
|
return 0;
|
|
1187
1254
|
}
|
|
1188
1255
|
const violations = [];
|
|
1189
1256
|
const severity = options.enforce ? "error" : "warn";
|
|
1190
|
-
const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(
|
|
1257
|
+
const log7 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
|
|
1191
1258
|
};
|
|
1192
1259
|
log7(" Checking files...");
|
|
1193
1260
|
for (const file of filesToCheck) {
|
|
@@ -1223,12 +1290,20 @@ async function checkCommand(options, cwd) {
|
|
|
1223
1290
|
}
|
|
1224
1291
|
}
|
|
1225
1292
|
log7(" done\n");
|
|
1226
|
-
if (!options.
|
|
1293
|
+
if (!options.files) {
|
|
1227
1294
|
log7(" Checking missing tests...");
|
|
1228
1295
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1296
|
+
if (options.staged) {
|
|
1297
|
+
const stagedSet = new Set(filesToCheck);
|
|
1298
|
+
for (const f of deletedTestSourceFiles) stagedSet.add(f);
|
|
1299
|
+
violations.push(...testViolations.filter((v) => stagedSet.has(v.file)));
|
|
1300
|
+
} else if (diffAddedFiles) {
|
|
1301
|
+
const checkSet = new Set(diffAddedFiles);
|
|
1302
|
+
for (const f of deletedTestSourceFiles) checkSet.add(f);
|
|
1303
|
+
violations.push(...testViolations.filter((v) => checkSet.has(v.file)));
|
|
1304
|
+
} else {
|
|
1305
|
+
violations.push(...testViolations);
|
|
1306
|
+
}
|
|
1232
1307
|
log7(" done\n");
|
|
1233
1308
|
}
|
|
1234
1309
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
@@ -1274,7 +1349,7 @@ async function checkCommand(options, cwd) {
|
|
|
1274
1349
|
return options.enforce && violations.length > 0 ? 1 : 0;
|
|
1275
1350
|
}
|
|
1276
1351
|
if (violations.length === 0) {
|
|
1277
|
-
console.log(`${
|
|
1352
|
+
console.log(`${chalk3.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
1278
1353
|
return 0;
|
|
1279
1354
|
}
|
|
1280
1355
|
if (!options.quiet) {
|
|
@@ -1282,7 +1357,7 @@ async function checkCommand(options, cwd) {
|
|
|
1282
1357
|
}
|
|
1283
1358
|
printSummary(violations);
|
|
1284
1359
|
if (options.enforce) {
|
|
1285
|
-
console.log(
|
|
1360
|
+
console.log(chalk3.red("Fix violations before committing."));
|
|
1286
1361
|
return 1;
|
|
1287
1362
|
}
|
|
1288
1363
|
return 0;
|
|
@@ -1340,7 +1415,7 @@ import * as path9 from "path";
|
|
|
1340
1415
|
import * as clack6 from "@clack/prompts";
|
|
1341
1416
|
import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
|
|
1342
1417
|
import { scan } from "@viberails/scanner";
|
|
1343
|
-
import
|
|
1418
|
+
import chalk6 from "chalk";
|
|
1344
1419
|
|
|
1345
1420
|
// src/display-text.ts
|
|
1346
1421
|
import {
|
|
@@ -1359,7 +1434,7 @@ import {
|
|
|
1359
1434
|
ORM_NAMES,
|
|
1360
1435
|
STYLING_NAMES as STYLING_NAMES2
|
|
1361
1436
|
} from "@viberails/types";
|
|
1362
|
-
import
|
|
1437
|
+
import chalk5 from "chalk";
|
|
1363
1438
|
|
|
1364
1439
|
// src/display-helpers.ts
|
|
1365
1440
|
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
@@ -1412,7 +1487,7 @@ function formatRoleGroup(group) {
|
|
|
1412
1487
|
|
|
1413
1488
|
// src/display-monorepo.ts
|
|
1414
1489
|
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1415
|
-
import
|
|
1490
|
+
import chalk4 from "chalk";
|
|
1416
1491
|
function formatPackageSummary(pkg) {
|
|
1417
1492
|
const parts = [];
|
|
1418
1493
|
if (pkg.stack.framework) {
|
|
@@ -1421,30 +1496,31 @@ function formatPackageSummary(pkg) {
|
|
|
1421
1496
|
if (pkg.stack.styling) {
|
|
1422
1497
|
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
1423
1498
|
}
|
|
1424
|
-
const
|
|
1499
|
+
const n = pkg.statistics.totalFiles;
|
|
1500
|
+
const files = `${n} ${n === 1 ? "file" : "files"}`;
|
|
1425
1501
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1426
1502
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1427
1503
|
}
|
|
1428
1504
|
function displayMonorepoResults(scanResult) {
|
|
1429
1505
|
const { stack, packages } = scanResult;
|
|
1430
1506
|
console.log(`
|
|
1431
|
-
${
|
|
1432
|
-
console.log(` ${
|
|
1507
|
+
${chalk4.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1508
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1433
1509
|
if (stack.packageManager) {
|
|
1434
|
-
console.log(` ${
|
|
1510
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1435
1511
|
}
|
|
1436
1512
|
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1437
|
-
console.log(` ${
|
|
1513
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1438
1514
|
} else {
|
|
1439
1515
|
if (stack.linter) {
|
|
1440
|
-
console.log(` ${
|
|
1516
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1441
1517
|
}
|
|
1442
1518
|
if (stack.formatter) {
|
|
1443
|
-
console.log(` ${
|
|
1519
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1444
1520
|
}
|
|
1445
1521
|
}
|
|
1446
1522
|
if (stack.testRunner) {
|
|
1447
|
-
console.log(` ${
|
|
1523
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1448
1524
|
}
|
|
1449
1525
|
console.log("");
|
|
1450
1526
|
for (const pkg of packages) {
|
|
@@ -1455,13 +1531,13 @@ ${chalk3.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
|
1455
1531
|
);
|
|
1456
1532
|
if (packagesWithDirs.length > 0) {
|
|
1457
1533
|
console.log(`
|
|
1458
|
-
${
|
|
1534
|
+
${chalk4.bold("Structure:")}`);
|
|
1459
1535
|
for (const pkg of packagesWithDirs) {
|
|
1460
1536
|
const groups = groupByRole(pkg.structure.directories);
|
|
1461
1537
|
if (groups.length === 0) continue;
|
|
1462
1538
|
console.log(` ${pkg.relativePath}:`);
|
|
1463
1539
|
for (const group of groups) {
|
|
1464
|
-
console.log(` ${
|
|
1540
|
+
console.log(` ${chalk4.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1465
1541
|
}
|
|
1466
1542
|
}
|
|
1467
1543
|
}
|
|
@@ -1477,7 +1553,8 @@ function formatPackageSummaryPlain(pkg) {
|
|
|
1477
1553
|
if (pkg.stack.styling) {
|
|
1478
1554
|
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
1479
1555
|
}
|
|
1480
|
-
const
|
|
1556
|
+
const n = pkg.statistics.totalFiles;
|
|
1557
|
+
const files = `${n} ${n === 1 ? "file" : "files"}`;
|
|
1481
1558
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1482
1559
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1483
1560
|
}
|
|
@@ -1541,7 +1618,7 @@ function displayConventions(scanResult) {
|
|
|
1541
1618
|
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1542
1619
|
if (conventionEntries.length === 0) return;
|
|
1543
1620
|
console.log(`
|
|
1544
|
-
${
|
|
1621
|
+
${chalk5.bold("Conventions:")}`);
|
|
1545
1622
|
for (const [key, convention] of conventionEntries) {
|
|
1546
1623
|
if (convention.confidence === "low") continue;
|
|
1547
1624
|
const label = CONVENTION_LABELS[key] ?? key;
|
|
@@ -1549,19 +1626,19 @@ ${chalk4.bold("Conventions:")}`);
|
|
|
1549
1626
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1550
1627
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1551
1628
|
if (allSame || pkgValues.length <= 1) {
|
|
1552
|
-
const ind = convention.confidence === "high" ?
|
|
1553
|
-
const detail =
|
|
1629
|
+
const ind = convention.confidence === "high" ? chalk5.green("\u2713") : chalk5.yellow("~");
|
|
1630
|
+
const detail = chalk5.dim(`(${confidenceLabel(convention)})`);
|
|
1554
1631
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1555
1632
|
} else {
|
|
1556
|
-
console.log(` ${
|
|
1633
|
+
console.log(` ${chalk5.yellow("~")} ${label}: varies by package`);
|
|
1557
1634
|
for (const pv of pkgValues) {
|
|
1558
1635
|
const pct = Math.round(pv.convention.consistency);
|
|
1559
1636
|
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1560
1637
|
}
|
|
1561
1638
|
}
|
|
1562
1639
|
} else {
|
|
1563
|
-
const ind = convention.confidence === "high" ?
|
|
1564
|
-
const detail =
|
|
1640
|
+
const ind = convention.confidence === "high" ? chalk5.green("\u2713") : chalk5.yellow("~");
|
|
1641
|
+
const detail = chalk5.dim(`(${confidenceLabel(convention)})`);
|
|
1565
1642
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1566
1643
|
}
|
|
1567
1644
|
}
|
|
@@ -1569,7 +1646,7 @@ ${chalk4.bold("Conventions:")}`);
|
|
|
1569
1646
|
function displaySummarySection(scanResult) {
|
|
1570
1647
|
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1571
1648
|
console.log(`
|
|
1572
|
-
${
|
|
1649
|
+
${chalk5.bold("Summary:")}`);
|
|
1573
1650
|
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1574
1651
|
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1575
1652
|
if (ext) {
|
|
@@ -1583,47 +1660,47 @@ function displayScanResults(scanResult) {
|
|
|
1583
1660
|
}
|
|
1584
1661
|
const { stack } = scanResult;
|
|
1585
1662
|
console.log(`
|
|
1586
|
-
${
|
|
1663
|
+
${chalk5.bold("Detected:")}`);
|
|
1587
1664
|
if (stack.framework) {
|
|
1588
|
-
console.log(` ${
|
|
1665
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
|
|
1589
1666
|
}
|
|
1590
|
-
console.log(` ${
|
|
1667
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1591
1668
|
if (stack.styling) {
|
|
1592
|
-
console.log(` ${
|
|
1669
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
|
|
1593
1670
|
}
|
|
1594
1671
|
if (stack.backend) {
|
|
1595
|
-
console.log(` ${
|
|
1672
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
|
|
1596
1673
|
}
|
|
1597
1674
|
if (stack.orm) {
|
|
1598
|
-
console.log(` ${
|
|
1675
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
|
|
1599
1676
|
}
|
|
1600
1677
|
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1601
|
-
console.log(` ${
|
|
1678
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1602
1679
|
} else {
|
|
1603
1680
|
if (stack.linter) {
|
|
1604
|
-
console.log(` ${
|
|
1681
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1605
1682
|
}
|
|
1606
1683
|
if (stack.formatter) {
|
|
1607
|
-
console.log(` ${
|
|
1684
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1608
1685
|
}
|
|
1609
1686
|
}
|
|
1610
1687
|
if (stack.testRunner) {
|
|
1611
|
-
console.log(` ${
|
|
1688
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1612
1689
|
}
|
|
1613
1690
|
if (stack.packageManager) {
|
|
1614
|
-
console.log(` ${
|
|
1691
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1615
1692
|
}
|
|
1616
1693
|
if (stack.libraries.length > 0) {
|
|
1617
1694
|
for (const lib of stack.libraries) {
|
|
1618
|
-
console.log(` ${
|
|
1695
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1619
1696
|
}
|
|
1620
1697
|
}
|
|
1621
1698
|
const groups = groupByRole(scanResult.structure.directories);
|
|
1622
1699
|
if (groups.length > 0) {
|
|
1623
1700
|
console.log(`
|
|
1624
|
-
${
|
|
1701
|
+
${chalk5.bold("Structure:")}`);
|
|
1625
1702
|
for (const group of groups) {
|
|
1626
|
-
console.log(` ${
|
|
1703
|
+
console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1627
1704
|
}
|
|
1628
1705
|
}
|
|
1629
1706
|
displayConventions(scanResult);
|
|
@@ -1633,49 +1710,49 @@ ${chalk4.bold("Structure:")}`);
|
|
|
1633
1710
|
function displayRulesPreview(config) {
|
|
1634
1711
|
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1635
1712
|
console.log(
|
|
1636
|
-
`${
|
|
1713
|
+
`${chalk5.bold("Rules:")} ${chalk5.dim("(warns on violation; use --enforce in CI to block)")}`
|
|
1637
1714
|
);
|
|
1638
|
-
console.log(` ${
|
|
1715
|
+
console.log(` ${chalk5.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1639
1716
|
if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
|
|
1640
1717
|
console.log(
|
|
1641
|
-
` ${
|
|
1718
|
+
` ${chalk5.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
|
|
1642
1719
|
);
|
|
1643
1720
|
} else if (config.rules.testCoverage > 0) {
|
|
1644
|
-
console.log(` ${
|
|
1721
|
+
console.log(` ${chalk5.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
|
|
1645
1722
|
} else {
|
|
1646
|
-
console.log(` ${
|
|
1723
|
+
console.log(` ${chalk5.dim("\u2022")} Test coverage target: disabled`);
|
|
1647
1724
|
}
|
|
1648
1725
|
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
1649
|
-
console.log(` ${
|
|
1726
|
+
console.log(` ${chalk5.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1650
1727
|
} else {
|
|
1651
|
-
console.log(` ${
|
|
1728
|
+
console.log(` ${chalk5.dim("\u2022")} Enforce file naming: no`);
|
|
1652
1729
|
}
|
|
1653
1730
|
console.log(
|
|
1654
|
-
` ${
|
|
1731
|
+
` ${chalk5.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1655
1732
|
);
|
|
1656
1733
|
console.log("");
|
|
1657
1734
|
}
|
|
1658
1735
|
function displayInitSummary(config, exemptedPackages) {
|
|
1659
1736
|
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1660
1737
|
const isMonorepo = config.packages.length > 1;
|
|
1661
|
-
const ok =
|
|
1662
|
-
const off =
|
|
1738
|
+
const ok = chalk5.green("\u2713");
|
|
1739
|
+
const off = chalk5.dim("\u25CB");
|
|
1663
1740
|
console.log("");
|
|
1664
|
-
console.log(` ${
|
|
1665
|
-
console.log(` ${ok} Max file size: ${
|
|
1741
|
+
console.log(` ${chalk5.bold("Rules to apply:")}`);
|
|
1742
|
+
console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1666
1743
|
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1667
1744
|
if (config.rules.enforceNaming && fileNaming) {
|
|
1668
|
-
console.log(` ${ok} File naming: ${
|
|
1745
|
+
console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
|
|
1669
1746
|
} else {
|
|
1670
|
-
console.log(` ${off} File naming: ${
|
|
1747
|
+
console.log(` ${off} File naming: ${chalk5.dim("not enforced")}`);
|
|
1671
1748
|
}
|
|
1672
1749
|
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1673
1750
|
if (config.rules.enforceMissingTests && testPattern) {
|
|
1674
|
-
console.log(` ${ok} Missing tests: ${
|
|
1751
|
+
console.log(` ${ok} Missing tests: ${chalk5.cyan(`enforced (${testPattern})`)}`);
|
|
1675
1752
|
} else if (config.rules.enforceMissingTests) {
|
|
1676
|
-
console.log(` ${ok} Missing tests: ${
|
|
1753
|
+
console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
|
|
1677
1754
|
} else {
|
|
1678
|
-
console.log(` ${off} Missing tests: ${
|
|
1755
|
+
console.log(` ${off} Missing tests: ${chalk5.dim("not enforced")}`);
|
|
1679
1756
|
}
|
|
1680
1757
|
if (config.rules.testCoverage > 0) {
|
|
1681
1758
|
if (isMonorepo) {
|
|
@@ -1683,27 +1760,27 @@ function displayInitSummary(config, exemptedPackages) {
|
|
|
1683
1760
|
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1684
1761
|
);
|
|
1685
1762
|
console.log(
|
|
1686
|
-
` ${ok} Coverage: ${
|
|
1763
|
+
` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)} default ${chalk5.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1687
1764
|
);
|
|
1688
1765
|
} else {
|
|
1689
|
-
console.log(` ${ok} Coverage: ${
|
|
1766
|
+
console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1690
1767
|
}
|
|
1691
1768
|
} else {
|
|
1692
|
-
console.log(` ${off} Coverage: ${
|
|
1769
|
+
console.log(` ${off} Coverage: ${chalk5.dim("disabled")}`);
|
|
1693
1770
|
}
|
|
1694
1771
|
if (exemptedPackages.length > 0) {
|
|
1695
1772
|
console.log(
|
|
1696
|
-
` ${
|
|
1773
|
+
` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
|
|
1697
1774
|
);
|
|
1698
1775
|
}
|
|
1699
1776
|
if (isMonorepo) {
|
|
1700
1777
|
console.log(
|
|
1701
1778
|
`
|
|
1702
|
-
${
|
|
1779
|
+
${chalk5.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
|
|
1703
1780
|
);
|
|
1704
1781
|
} else {
|
|
1705
1782
|
console.log(`
|
|
1706
|
-
${
|
|
1783
|
+
${chalk5.dim("warns on violation \xB7 use --enforce in CI to block")}`);
|
|
1707
1784
|
}
|
|
1708
1785
|
console.log("");
|
|
1709
1786
|
}
|
|
@@ -2015,7 +2092,7 @@ async function configCommand(options, cwd) {
|
|
|
2015
2092
|
}
|
|
2016
2093
|
const configPath = path9.join(projectRoot, CONFIG_FILE3);
|
|
2017
2094
|
if (!fs10.existsSync(configPath)) {
|
|
2018
|
-
console.log(`${
|
|
2095
|
+
console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails init")} first.`);
|
|
2019
2096
|
return;
|
|
2020
2097
|
}
|
|
2021
2098
|
clack6.intro("viberails config");
|
|
@@ -2100,22 +2177,22 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
2100
2177
|
import * as fs13 from "fs";
|
|
2101
2178
|
import * as path13 from "path";
|
|
2102
2179
|
import { loadConfig as loadConfig4 } from "@viberails/config";
|
|
2103
|
-
import
|
|
2180
|
+
import chalk8 from "chalk";
|
|
2104
2181
|
|
|
2105
2182
|
// src/commands/fix-helpers.ts
|
|
2106
2183
|
import { execSync as execSync2 } from "child_process";
|
|
2107
|
-
import
|
|
2184
|
+
import chalk7 from "chalk";
|
|
2108
2185
|
function printPlan(renames, stubs) {
|
|
2109
2186
|
if (renames.length > 0) {
|
|
2110
|
-
console.log(
|
|
2187
|
+
console.log(chalk7.bold("\nFile renames:"));
|
|
2111
2188
|
for (const r of renames) {
|
|
2112
|
-
console.log(` ${
|
|
2189
|
+
console.log(` ${chalk7.red(r.oldPath)} \u2192 ${chalk7.green(r.newPath)}`);
|
|
2113
2190
|
}
|
|
2114
2191
|
}
|
|
2115
2192
|
if (stubs.length > 0) {
|
|
2116
|
-
console.log(
|
|
2193
|
+
console.log(chalk7.bold("\nTest stubs to create:"));
|
|
2117
2194
|
for (const s of stubs) {
|
|
2118
|
-
console.log(` ${
|
|
2195
|
+
console.log(` ${chalk7.green("+")} ${s.path}`);
|
|
2119
2196
|
}
|
|
2120
2197
|
}
|
|
2121
2198
|
}
|
|
@@ -2149,8 +2226,62 @@ function computeNewSpecifier(oldSpecifier, newBare) {
|
|
|
2149
2226
|
const newSpec = prefix + newBare;
|
|
2150
2227
|
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
2151
2228
|
}
|
|
2152
|
-
async function
|
|
2229
|
+
async function scanForAliasImports(renames, projectRoot) {
|
|
2153
2230
|
if (renames.length === 0) return [];
|
|
2231
|
+
const { readFile, readdir } = await import("fs/promises");
|
|
2232
|
+
const oldBareNames = /* @__PURE__ */ new Set();
|
|
2233
|
+
for (const r of renames) {
|
|
2234
|
+
const oldFilename = path10.basename(r.oldPath);
|
|
2235
|
+
oldBareNames.add(oldFilename.slice(0, oldFilename.indexOf(".")));
|
|
2236
|
+
}
|
|
2237
|
+
const importPattern = /(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
2238
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
2239
|
+
"node_modules",
|
|
2240
|
+
"dist",
|
|
2241
|
+
"build",
|
|
2242
|
+
".next",
|
|
2243
|
+
".expo",
|
|
2244
|
+
".turbo",
|
|
2245
|
+
"coverage"
|
|
2246
|
+
]);
|
|
2247
|
+
const sourceExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2248
|
+
const allEntries = await readdir(projectRoot, { recursive: true, withFileTypes: true });
|
|
2249
|
+
const files = allEntries.filter((e) => {
|
|
2250
|
+
if (!e.isFile()) return false;
|
|
2251
|
+
const ext = path10.extname(e.name);
|
|
2252
|
+
if (!sourceExts.has(ext)) return false;
|
|
2253
|
+
const rel = path10.join(e.parentPath, e.name);
|
|
2254
|
+
const segments = path10.relative(projectRoot, rel).split(path10.sep);
|
|
2255
|
+
return !segments.some((s) => skipDirs.has(s));
|
|
2256
|
+
}).map((e) => path10.join(e.parentPath, e.name));
|
|
2257
|
+
const aliases = [];
|
|
2258
|
+
for (const file of files) {
|
|
2259
|
+
let content;
|
|
2260
|
+
try {
|
|
2261
|
+
content = await readFile(file, "utf-8");
|
|
2262
|
+
} catch {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
const lines = content.split("\n");
|
|
2266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2267
|
+
const line = lines[i];
|
|
2268
|
+
importPattern.lastIndex = 0;
|
|
2269
|
+
for (let match = importPattern.exec(line); match !== null; match = importPattern.exec(line)) {
|
|
2270
|
+
const specifier = match[1] ?? match[2];
|
|
2271
|
+
if (!specifier || specifier.startsWith(".")) continue;
|
|
2272
|
+
if (!specifier.includes("/")) continue;
|
|
2273
|
+
const lastSegment = specifier.split("/").pop() ?? "";
|
|
2274
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2275
|
+
if (oldBareNames.has(bare)) {
|
|
2276
|
+
aliases.push({ file, specifier, line: i + 1 });
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return aliases;
|
|
2282
|
+
}
|
|
2283
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
2284
|
+
if (renames.length === 0) return { updates: [], skippedAliases: [] };
|
|
2154
2285
|
const { Project, SyntaxKind } = await import("ts-morph");
|
|
2155
2286
|
const renameMap = /* @__PURE__ */ new Map();
|
|
2156
2287
|
for (const r of renames) {
|
|
@@ -2165,15 +2296,44 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
2165
2296
|
});
|
|
2166
2297
|
project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
2167
2298
|
const updates = [];
|
|
2299
|
+
const skippedAliases = [];
|
|
2168
2300
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2301
|
+
const oldBareNames = /* @__PURE__ */ new Set();
|
|
2302
|
+
for (const r of renames) {
|
|
2303
|
+
const oldFilename = path10.basename(r.oldPath);
|
|
2304
|
+
oldBareNames.add(oldFilename.slice(0, oldFilename.indexOf(".")));
|
|
2305
|
+
}
|
|
2169
2306
|
for (const sourceFile of project.getSourceFiles()) {
|
|
2170
2307
|
const filePath = sourceFile.getFilePath();
|
|
2171
2308
|
const segments = filePath.split(path10.sep);
|
|
2172
|
-
|
|
2309
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
2310
|
+
"node_modules",
|
|
2311
|
+
"dist",
|
|
2312
|
+
"build",
|
|
2313
|
+
".next",
|
|
2314
|
+
".expo",
|
|
2315
|
+
".svelte-kit",
|
|
2316
|
+
".turbo",
|
|
2317
|
+
"coverage"
|
|
2318
|
+
]);
|
|
2319
|
+
if (segments.some((s) => skipDirs.has(s))) continue;
|
|
2173
2320
|
const fileDir = path10.dirname(filePath);
|
|
2174
2321
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
2175
2322
|
const specifier = decl.getModuleSpecifierValue();
|
|
2176
|
-
if (!specifier.startsWith("."))
|
|
2323
|
+
if (!specifier.startsWith(".")) {
|
|
2324
|
+
if (specifier.includes("/")) {
|
|
2325
|
+
const lastSegment = specifier.split("/").pop() ?? "";
|
|
2326
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2327
|
+
if (oldBareNames.has(bare)) {
|
|
2328
|
+
skippedAliases.push({
|
|
2329
|
+
file: filePath,
|
|
2330
|
+
specifier,
|
|
2331
|
+
line: decl.getStartLineNumber()
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2177
2337
|
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
2178
2338
|
if (!match) continue;
|
|
2179
2339
|
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
@@ -2223,7 +2383,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
2223
2383
|
if (updates.length > 0) {
|
|
2224
2384
|
await project.save();
|
|
2225
2385
|
}
|
|
2226
|
-
return updates;
|
|
2386
|
+
return { updates, skippedAliases };
|
|
2227
2387
|
}
|
|
2228
2388
|
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
2229
2389
|
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
@@ -2326,10 +2486,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2326
2486
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2327
2487
|
const testPattern = pkg?.structure?.testPattern;
|
|
2328
2488
|
if (!testPattern) return null;
|
|
2329
|
-
const
|
|
2330
|
-
const ext = path12.extname(
|
|
2489
|
+
const basename9 = path12.basename(sourceRelPath);
|
|
2490
|
+
const ext = path12.extname(basename9);
|
|
2331
2491
|
if (!ext) return null;
|
|
2332
|
-
const stem =
|
|
2492
|
+
const stem = basename9.slice(0, -ext.length);
|
|
2333
2493
|
const testSuffix = testPattern.replace("*", "");
|
|
2334
2494
|
const testFilename = `${stem}${testSuffix}`;
|
|
2335
2495
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2360,13 +2520,13 @@ async function fixCommand(options, cwd) {
|
|
|
2360
2520
|
const startDir = cwd ?? process.cwd();
|
|
2361
2521
|
const projectRoot = findProjectRoot(startDir);
|
|
2362
2522
|
if (!projectRoot) {
|
|
2363
|
-
console.error(`${
|
|
2523
|
+
console.error(`${chalk8.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
2364
2524
|
return 1;
|
|
2365
2525
|
}
|
|
2366
2526
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
2367
2527
|
if (!fs13.existsSync(configPath)) {
|
|
2368
2528
|
console.error(
|
|
2369
|
-
`${
|
|
2529
|
+
`${chalk8.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
2370
2530
|
);
|
|
2371
2531
|
return 1;
|
|
2372
2532
|
}
|
|
@@ -2375,7 +2535,7 @@ async function fixCommand(options, cwd) {
|
|
|
2375
2535
|
const isDirty = checkGitDirty(projectRoot);
|
|
2376
2536
|
if (isDirty) {
|
|
2377
2537
|
console.log(
|
|
2378
|
-
|
|
2538
|
+
chalk8.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
2379
2539
|
);
|
|
2380
2540
|
}
|
|
2381
2541
|
}
|
|
@@ -2404,13 +2564,59 @@ async function fixCommand(options, cwd) {
|
|
|
2404
2564
|
if (stub) testStubs.push(stub);
|
|
2405
2565
|
}
|
|
2406
2566
|
}
|
|
2407
|
-
|
|
2408
|
-
|
|
2567
|
+
const aliasImports = await scanForAliasImports(dedupedRenames, projectRoot);
|
|
2568
|
+
const blockedOldBareNames = /* @__PURE__ */ new Set();
|
|
2569
|
+
for (const alias of aliasImports) {
|
|
2570
|
+
const lastSegment = alias.specifier.split("/").pop() ?? "";
|
|
2571
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2572
|
+
blockedOldBareNames.add(bare);
|
|
2573
|
+
}
|
|
2574
|
+
const safeRenames = dedupedRenames.filter((r) => {
|
|
2575
|
+
const oldFilename = path13.basename(r.oldPath);
|
|
2576
|
+
const bare = oldFilename.slice(0, oldFilename.indexOf("."));
|
|
2577
|
+
return !blockedOldBareNames.has(bare);
|
|
2578
|
+
});
|
|
2579
|
+
const skippedRenames = dedupedRenames.filter((r) => {
|
|
2580
|
+
const oldFilename = path13.basename(r.oldPath);
|
|
2581
|
+
const bare = oldFilename.slice(0, oldFilename.indexOf("."));
|
|
2582
|
+
return blockedOldBareNames.has(bare);
|
|
2583
|
+
});
|
|
2584
|
+
if (safeRenames.length === 0 && testStubs.length === 0 && skippedRenames.length === 0) {
|
|
2585
|
+
console.log(`${chalk8.green("\u2713")} No fixable violations found.`);
|
|
2586
|
+
return 0;
|
|
2587
|
+
}
|
|
2588
|
+
printPlan(safeRenames, testStubs);
|
|
2589
|
+
if (skippedRenames.length > 0) {
|
|
2590
|
+
console.log("");
|
|
2591
|
+
console.log(
|
|
2592
|
+
chalk8.yellow(
|
|
2593
|
+
`Skipping ${skippedRenames.length} rename${skippedRenames.length > 1 ? "s" : ""} \u2014 aliased imports would break:`
|
|
2594
|
+
)
|
|
2595
|
+
);
|
|
2596
|
+
for (const r of skippedRenames.slice(0, 5)) {
|
|
2597
|
+
console.log(chalk8.dim(` ${r.oldPath} \u2192 ${r.newPath}`));
|
|
2598
|
+
}
|
|
2599
|
+
if (skippedRenames.length > 5) {
|
|
2600
|
+
console.log(chalk8.dim(` ... and ${skippedRenames.length - 5} more`));
|
|
2601
|
+
}
|
|
2602
|
+
console.log("");
|
|
2603
|
+
console.log(chalk8.yellow("Affected aliased imports:"));
|
|
2604
|
+
for (const alias of aliasImports.slice(0, 5)) {
|
|
2605
|
+
const relFile = path13.relative(projectRoot, alias.file);
|
|
2606
|
+
console.log(chalk8.dim(` ${relFile}:${alias.line} \u2014 ${alias.specifier}`));
|
|
2607
|
+
}
|
|
2608
|
+
if (aliasImports.length > 5) {
|
|
2609
|
+
console.log(chalk8.dim(` ... and ${aliasImports.length - 5} more`));
|
|
2610
|
+
}
|
|
2611
|
+
console.log(chalk8.dim(" Update these imports to relative paths first, then re-run fix."));
|
|
2612
|
+
}
|
|
2613
|
+
if (safeRenames.length === 0 && testStubs.length === 0) {
|
|
2614
|
+
console.log(`
|
|
2615
|
+
${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
2409
2616
|
return 0;
|
|
2410
2617
|
}
|
|
2411
|
-
printPlan(dedupedRenames, testStubs);
|
|
2412
2618
|
if (options.dryRun) {
|
|
2413
|
-
console.log(
|
|
2619
|
+
console.log(chalk8.dim("\nDry run \u2014 no changes applied."));
|
|
2414
2620
|
return 0;
|
|
2415
2621
|
}
|
|
2416
2622
|
if (!options.yes) {
|
|
@@ -2421,15 +2627,15 @@ async function fixCommand(options, cwd) {
|
|
|
2421
2627
|
}
|
|
2422
2628
|
}
|
|
2423
2629
|
let renameCount = 0;
|
|
2424
|
-
for (const rename of
|
|
2630
|
+
for (const rename of safeRenames) {
|
|
2425
2631
|
if (executeRename(rename)) {
|
|
2426
2632
|
renameCount++;
|
|
2427
2633
|
}
|
|
2428
2634
|
}
|
|
2429
2635
|
let importUpdateCount = 0;
|
|
2430
2636
|
if (renameCount > 0) {
|
|
2431
|
-
const appliedRenames =
|
|
2432
|
-
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
2637
|
+
const appliedRenames = safeRenames.filter((r) => fs13.existsSync(r.newAbsPath));
|
|
2638
|
+
const { updates } = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
2433
2639
|
importUpdateCount = updates.length;
|
|
2434
2640
|
}
|
|
2435
2641
|
let stubCount = 0;
|
|
@@ -2441,33 +2647,33 @@ async function fixCommand(options, cwd) {
|
|
|
2441
2647
|
}
|
|
2442
2648
|
console.log("");
|
|
2443
2649
|
if (renameCount > 0) {
|
|
2444
|
-
console.log(`${
|
|
2650
|
+
console.log(`${chalk8.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
2445
2651
|
}
|
|
2446
2652
|
if (importUpdateCount > 0) {
|
|
2447
2653
|
console.log(
|
|
2448
|
-
`${
|
|
2654
|
+
`${chalk8.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
2449
2655
|
);
|
|
2450
2656
|
}
|
|
2451
2657
|
if (stubCount > 0) {
|
|
2452
|
-
console.log(`${
|
|
2658
|
+
console.log(`${chalk8.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
2453
2659
|
}
|
|
2454
2660
|
return 0;
|
|
2455
2661
|
}
|
|
2456
2662
|
|
|
2457
2663
|
// src/commands/init.ts
|
|
2458
|
-
import * as
|
|
2459
|
-
import * as
|
|
2664
|
+
import * as fs19 from "fs";
|
|
2665
|
+
import * as path19 from "path";
|
|
2460
2666
|
import * as clack8 from "@clack/prompts";
|
|
2461
2667
|
import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
|
|
2462
2668
|
import { scan as scan2 } from "@viberails/scanner";
|
|
2463
|
-
import
|
|
2669
|
+
import chalk12 from "chalk";
|
|
2464
2670
|
|
|
2465
2671
|
// src/utils/check-prerequisites.ts
|
|
2466
2672
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2467
2673
|
import * as fs14 from "fs";
|
|
2468
2674
|
import * as path14 from "path";
|
|
2469
2675
|
import * as clack7 from "@clack/prompts";
|
|
2470
|
-
import
|
|
2676
|
+
import chalk9 from "chalk";
|
|
2471
2677
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2472
2678
|
const pm = scanResult.stack.packageManager.name;
|
|
2473
2679
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -2498,9 +2704,9 @@ function displayMissingPrereqs(prereqs) {
|
|
|
2498
2704
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2499
2705
|
for (const m of missing) {
|
|
2500
2706
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2501
|
-
console.log(` ${
|
|
2707
|
+
console.log(` ${chalk9.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2502
2708
|
if (m.installCommand) {
|
|
2503
|
-
console.log(` Install: ${
|
|
2709
|
+
console.log(` Install: ${chalk9.cyan(m.installCommand)}`);
|
|
2504
2710
|
}
|
|
2505
2711
|
}
|
|
2506
2712
|
}
|
|
@@ -2611,46 +2817,85 @@ function updateGitignore(projectRoot) {
|
|
|
2611
2817
|
}
|
|
2612
2818
|
|
|
2613
2819
|
// src/commands/init-hooks.ts
|
|
2820
|
+
import * as fs17 from "fs";
|
|
2821
|
+
import * as path17 from "path";
|
|
2822
|
+
import chalk10 from "chalk";
|
|
2823
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2824
|
+
|
|
2825
|
+
// src/commands/resolve-typecheck.ts
|
|
2614
2826
|
import * as fs16 from "fs";
|
|
2615
2827
|
import * as path16 from "path";
|
|
2616
|
-
|
|
2617
|
-
|
|
2828
|
+
function hasTurboTask(projectRoot, taskName) {
|
|
2829
|
+
const turboPath = path16.join(projectRoot, "turbo.json");
|
|
2830
|
+
if (!fs16.existsSync(turboPath)) return false;
|
|
2831
|
+
try {
|
|
2832
|
+
const turbo = JSON.parse(fs16.readFileSync(turboPath, "utf-8"));
|
|
2833
|
+
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
2834
|
+
return taskName in tasks;
|
|
2835
|
+
} catch {
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
2840
|
+
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
2841
|
+
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
2842
|
+
}
|
|
2843
|
+
const pkgJsonPath = path16.join(projectRoot, "package.json");
|
|
2844
|
+
if (fs16.existsSync(pkgJsonPath)) {
|
|
2845
|
+
try {
|
|
2846
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
|
|
2847
|
+
if (pkg.scripts?.typecheck) {
|
|
2848
|
+
const pm = packageManager ?? "npm";
|
|
2849
|
+
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
2850
|
+
}
|
|
2851
|
+
} catch {
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (fs16.existsSync(path16.join(projectRoot, "tsconfig.json"))) {
|
|
2855
|
+
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
2856
|
+
}
|
|
2857
|
+
return {
|
|
2858
|
+
reason: "no root tsconfig.json, no typecheck script, and no turbo typecheck task found"
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/commands/init-hooks.ts
|
|
2618
2863
|
function setupPreCommitHook(projectRoot) {
|
|
2619
|
-
const lefthookPath =
|
|
2620
|
-
if (
|
|
2864
|
+
const lefthookPath = path17.join(projectRoot, "lefthook.yml");
|
|
2865
|
+
if (fs17.existsSync(lefthookPath)) {
|
|
2621
2866
|
addLefthookPreCommit(lefthookPath);
|
|
2622
|
-
console.log(` ${
|
|
2867
|
+
console.log(` ${chalk10.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2623
2868
|
return "lefthook.yml";
|
|
2624
2869
|
}
|
|
2625
|
-
const huskyDir =
|
|
2626
|
-
if (
|
|
2870
|
+
const huskyDir = path17.join(projectRoot, ".husky");
|
|
2871
|
+
if (fs17.existsSync(huskyDir)) {
|
|
2627
2872
|
writeHuskyPreCommit(huskyDir);
|
|
2628
|
-
console.log(` ${
|
|
2873
|
+
console.log(` ${chalk10.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2629
2874
|
return ".husky/pre-commit";
|
|
2630
2875
|
}
|
|
2631
|
-
const gitDir =
|
|
2632
|
-
if (
|
|
2633
|
-
const hooksDir =
|
|
2634
|
-
if (!
|
|
2635
|
-
|
|
2876
|
+
const gitDir = path17.join(projectRoot, ".git");
|
|
2877
|
+
if (fs17.existsSync(gitDir)) {
|
|
2878
|
+
const hooksDir = path17.join(gitDir, "hooks");
|
|
2879
|
+
if (!fs17.existsSync(hooksDir)) {
|
|
2880
|
+
fs17.mkdirSync(hooksDir, { recursive: true });
|
|
2636
2881
|
}
|
|
2637
2882
|
writeGitHookPreCommit(hooksDir);
|
|
2638
|
-
console.log(` ${
|
|
2883
|
+
console.log(` ${chalk10.green("\u2713")} .git/hooks/pre-commit`);
|
|
2639
2884
|
return ".git/hooks/pre-commit";
|
|
2640
2885
|
}
|
|
2641
2886
|
return void 0;
|
|
2642
2887
|
}
|
|
2643
2888
|
function writeGitHookPreCommit(hooksDir) {
|
|
2644
|
-
const hookPath =
|
|
2645
|
-
if (
|
|
2646
|
-
const existing =
|
|
2889
|
+
const hookPath = path17.join(hooksDir, "pre-commit");
|
|
2890
|
+
if (fs17.existsSync(hookPath)) {
|
|
2891
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2647
2892
|
if (existing.includes("viberails")) return;
|
|
2648
|
-
|
|
2893
|
+
fs17.writeFileSync(
|
|
2649
2894
|
hookPath,
|
|
2650
2895
|
`${existing.trimEnd()}
|
|
2651
2896
|
|
|
2652
2897
|
# viberails check
|
|
2653
|
-
npx viberails check --staged
|
|
2898
|
+
if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi
|
|
2654
2899
|
`
|
|
2655
2900
|
);
|
|
2656
2901
|
return;
|
|
@@ -2659,13 +2904,13 @@ npx viberails check --staged
|
|
|
2659
2904
|
"#!/bin/sh",
|
|
2660
2905
|
"# Generated by viberails \u2014 https://viberails.sh",
|
|
2661
2906
|
"",
|
|
2662
|
-
"npx viberails check --staged",
|
|
2907
|
+
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
2663
2908
|
""
|
|
2664
2909
|
].join("\n");
|
|
2665
|
-
|
|
2910
|
+
fs17.writeFileSync(hookPath, script, { mode: 493 });
|
|
2666
2911
|
}
|
|
2667
2912
|
function addLefthookPreCommit(lefthookPath) {
|
|
2668
|
-
const content =
|
|
2913
|
+
const content = fs17.readFileSync(lefthookPath, "utf-8");
|
|
2669
2914
|
if (content.includes("viberails")) return;
|
|
2670
2915
|
const doc = parseYaml(content) ?? {};
|
|
2671
2916
|
if (!doc["pre-commit"]) {
|
|
@@ -2675,30 +2920,30 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
2675
2920
|
doc["pre-commit"].commands = {};
|
|
2676
2921
|
}
|
|
2677
2922
|
doc["pre-commit"].commands.viberails = {
|
|
2678
|
-
run: "npx viberails check --staged"
|
|
2923
|
+
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
2679
2924
|
};
|
|
2680
|
-
|
|
2925
|
+
fs17.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
2681
2926
|
}
|
|
2682
2927
|
function detectHookManager(projectRoot) {
|
|
2683
|
-
if (
|
|
2684
|
-
if (
|
|
2928
|
+
if (fs17.existsSync(path17.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
2929
|
+
if (fs17.existsSync(path17.join(projectRoot, ".husky"))) return "Husky";
|
|
2685
2930
|
return void 0;
|
|
2686
2931
|
}
|
|
2687
2932
|
function setupClaudeCodeHook(projectRoot) {
|
|
2688
|
-
const claudeDir =
|
|
2689
|
-
if (!
|
|
2690
|
-
|
|
2933
|
+
const claudeDir = path17.join(projectRoot, ".claude");
|
|
2934
|
+
if (!fs17.existsSync(claudeDir)) {
|
|
2935
|
+
fs17.mkdirSync(claudeDir, { recursive: true });
|
|
2691
2936
|
}
|
|
2692
|
-
const settingsPath =
|
|
2937
|
+
const settingsPath = path17.join(claudeDir, "settings.json");
|
|
2693
2938
|
let settings = {};
|
|
2694
|
-
if (
|
|
2939
|
+
if (fs17.existsSync(settingsPath)) {
|
|
2695
2940
|
try {
|
|
2696
|
-
settings = JSON.parse(
|
|
2941
|
+
settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
|
|
2697
2942
|
} catch {
|
|
2698
2943
|
console.warn(
|
|
2699
|
-
` ${
|
|
2944
|
+
` ${chalk10.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
2700
2945
|
);
|
|
2701
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2946
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk10.cyan("viberails init --force")}`);
|
|
2702
2947
|
return;
|
|
2703
2948
|
}
|
|
2704
2949
|
}
|
|
@@ -2719,30 +2964,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2719
2964
|
}
|
|
2720
2965
|
];
|
|
2721
2966
|
settings.hooks = hooks;
|
|
2722
|
-
|
|
2967
|
+
fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2723
2968
|
`);
|
|
2724
|
-
console.log(` ${
|
|
2969
|
+
console.log(` ${chalk10.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
2725
2970
|
}
|
|
2726
2971
|
function setupClaudeMdReference(projectRoot) {
|
|
2727
|
-
const claudeMdPath =
|
|
2972
|
+
const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
|
|
2728
2973
|
let content = "";
|
|
2729
|
-
if (
|
|
2730
|
-
content =
|
|
2974
|
+
if (fs17.existsSync(claudeMdPath)) {
|
|
2975
|
+
content = fs17.readFileSync(claudeMdPath, "utf-8");
|
|
2731
2976
|
}
|
|
2732
2977
|
if (content.includes("@.viberails/context.md")) return;
|
|
2733
2978
|
const ref = "\n@.viberails/context.md\n";
|
|
2734
2979
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
2735
|
-
|
|
2736
|
-
console.log(` ${
|
|
2980
|
+
fs17.writeFileSync(claudeMdPath, prefix + ref);
|
|
2981
|
+
console.log(` ${chalk10.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2737
2982
|
}
|
|
2738
2983
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
2739
|
-
const workflowDir =
|
|
2740
|
-
const workflowPath =
|
|
2741
|
-
if (
|
|
2742
|
-
const existing =
|
|
2984
|
+
const workflowDir = path17.join(projectRoot, ".github", "workflows");
|
|
2985
|
+
const workflowPath = path17.join(workflowDir, "viberails.yml");
|
|
2986
|
+
if (fs17.existsSync(workflowPath)) {
|
|
2987
|
+
const existing = fs17.readFileSync(workflowPath, "utf-8");
|
|
2743
2988
|
if (existing.includes("viberails")) return void 0;
|
|
2744
2989
|
}
|
|
2745
|
-
|
|
2990
|
+
fs17.mkdirSync(workflowDir, { recursive: true });
|
|
2746
2991
|
const pm = packageManager || "npm";
|
|
2747
2992
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
2748
2993
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -2751,7 +2996,7 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
2751
2996
|
"",
|
|
2752
2997
|
"on:",
|
|
2753
2998
|
" pull_request:",
|
|
2754
|
-
" branches: [main]",
|
|
2999
|
+
" branches: [main, master]",
|
|
2755
3000
|
"",
|
|
2756
3001
|
"jobs:",
|
|
2757
3002
|
" check:",
|
|
@@ -2769,87 +3014,94 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
2769
3014
|
" - uses: actions/setup-node@v4",
|
|
2770
3015
|
" with:",
|
|
2771
3016
|
" node-version: 22",
|
|
2772
|
-
|
|
3017
|
+
` cache: ${pm}`,
|
|
2773
3018
|
"",
|
|
2774
3019
|
` - run: ${installCmd}`
|
|
2775
3020
|
);
|
|
2776
3021
|
if (options?.typecheck) {
|
|
2777
|
-
|
|
3022
|
+
const resolved = resolveTypecheckCommand(projectRoot, pm);
|
|
3023
|
+
if (resolved.command) {
|
|
3024
|
+
const ciCmd = resolved.command.startsWith("npx ") ? `${runPrefix} ${resolved.command.slice(4)}` : resolved.command;
|
|
3025
|
+
lines.push(` - run: ${ciCmd}`);
|
|
3026
|
+
}
|
|
2778
3027
|
}
|
|
2779
3028
|
if (options?.linter) {
|
|
2780
3029
|
const lintCmd = options.linter === "biome" ? "biome check ." : "eslint .";
|
|
2781
3030
|
lines.push(` - run: ${runPrefix} ${lintCmd}`);
|
|
2782
3031
|
}
|
|
2783
3032
|
lines.push(
|
|
2784
|
-
` - run:
|
|
3033
|
+
` - run: npx viberails check --enforce --diff-base origin/\${{ github.event.pull_request.base.ref }}`,
|
|
2785
3034
|
""
|
|
2786
3035
|
);
|
|
2787
3036
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
2788
|
-
|
|
3037
|
+
fs17.writeFileSync(workflowPath, content);
|
|
2789
3038
|
return ".github/workflows/viberails.yml";
|
|
2790
3039
|
}
|
|
2791
3040
|
function writeHuskyPreCommit(huskyDir) {
|
|
2792
|
-
const hookPath =
|
|
2793
|
-
if
|
|
2794
|
-
|
|
3041
|
+
const hookPath = path17.join(huskyDir, "pre-commit");
|
|
3042
|
+
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3043
|
+
if (fs17.existsSync(hookPath)) {
|
|
3044
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2795
3045
|
if (!existing.includes("viberails")) {
|
|
2796
|
-
|
|
2797
|
-
|
|
3046
|
+
fs17.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3047
|
+
${cmd}
|
|
2798
3048
|
`);
|
|
2799
3049
|
}
|
|
2800
3050
|
return;
|
|
2801
3051
|
}
|
|
2802
|
-
|
|
3052
|
+
fs17.writeFileSync(hookPath, `#!/bin/sh
|
|
3053
|
+
${cmd}
|
|
3054
|
+
`, { mode: 493 });
|
|
2803
3055
|
}
|
|
2804
3056
|
|
|
2805
3057
|
// src/commands/init-hooks-extra.ts
|
|
2806
|
-
import * as
|
|
2807
|
-
import * as
|
|
2808
|
-
import
|
|
3058
|
+
import * as fs18 from "fs";
|
|
3059
|
+
import * as path18 from "path";
|
|
3060
|
+
import chalk11 from "chalk";
|
|
2809
3061
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
2810
3062
|
function addPreCommitStep(projectRoot, name, command, marker) {
|
|
2811
|
-
const lefthookPath =
|
|
2812
|
-
if (
|
|
2813
|
-
const content =
|
|
3063
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3064
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
3065
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
2814
3066
|
if (content.includes(marker)) return void 0;
|
|
2815
3067
|
const doc = parseYaml2(content) ?? {};
|
|
2816
3068
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
2817
3069
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
2818
3070
|
doc["pre-commit"].commands[name] = { run: command };
|
|
2819
|
-
|
|
3071
|
+
fs18.writeFileSync(lefthookPath, stringifyYaml2(doc));
|
|
2820
3072
|
return "lefthook.yml";
|
|
2821
3073
|
}
|
|
2822
|
-
const huskyDir =
|
|
2823
|
-
if (
|
|
2824
|
-
const hookPath =
|
|
2825
|
-
if (
|
|
2826
|
-
const existing =
|
|
3074
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3075
|
+
if (fs18.existsSync(huskyDir)) {
|
|
3076
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
3077
|
+
if (fs18.existsSync(hookPath)) {
|
|
3078
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2827
3079
|
if (existing.includes(marker)) return void 0;
|
|
2828
|
-
|
|
3080
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2829
3081
|
${command}
|
|
2830
3082
|
`);
|
|
2831
3083
|
} else {
|
|
2832
|
-
|
|
3084
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
2833
3085
|
${command}
|
|
2834
3086
|
`, { mode: 493 });
|
|
2835
3087
|
}
|
|
2836
3088
|
return ".husky/pre-commit";
|
|
2837
3089
|
}
|
|
2838
|
-
const gitDir =
|
|
2839
|
-
if (
|
|
2840
|
-
const hooksDir =
|
|
2841
|
-
if (!
|
|
2842
|
-
const hookPath =
|
|
2843
|
-
if (
|
|
2844
|
-
const existing =
|
|
3090
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3091
|
+
if (fs18.existsSync(gitDir)) {
|
|
3092
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3093
|
+
if (!fs18.existsSync(hooksDir)) fs18.mkdirSync(hooksDir, { recursive: true });
|
|
3094
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3095
|
+
if (fs18.existsSync(hookPath)) {
|
|
3096
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2845
3097
|
if (existing.includes(marker)) return void 0;
|
|
2846
|
-
|
|
3098
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2847
3099
|
|
|
2848
3100
|
# ${name}
|
|
2849
3101
|
${command}
|
|
2850
3102
|
`);
|
|
2851
3103
|
} else {
|
|
2852
|
-
|
|
3104
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
2853
3105
|
# Generated by viberails
|
|
2854
3106
|
|
|
2855
3107
|
# ${name}
|
|
@@ -2862,10 +3114,15 @@ ${command}
|
|
|
2862
3114
|
}
|
|
2863
3115
|
return void 0;
|
|
2864
3116
|
}
|
|
2865
|
-
function setupTypecheckHook(projectRoot) {
|
|
2866
|
-
const
|
|
3117
|
+
function setupTypecheckHook(projectRoot, packageManager) {
|
|
3118
|
+
const resolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3119
|
+
if (!resolved.command) {
|
|
3120
|
+
console.log(` ${chalk11.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
|
|
3121
|
+
return void 0;
|
|
3122
|
+
}
|
|
3123
|
+
const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
|
|
2867
3124
|
if (target) {
|
|
2868
|
-
console.log(` ${
|
|
3125
|
+
console.log(` ${chalk11.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
|
|
2869
3126
|
}
|
|
2870
3127
|
return target;
|
|
2871
3128
|
}
|
|
@@ -2874,7 +3131,7 @@ function setupLintHook(projectRoot, linter) {
|
|
|
2874
3131
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
2875
3132
|
const target = addPreCommitStep(projectRoot, "lint", command, linter);
|
|
2876
3133
|
if (target) {
|
|
2877
|
-
console.log(` ${
|
|
3134
|
+
console.log(` ${chalk11.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
2878
3135
|
}
|
|
2879
3136
|
return target;
|
|
2880
3137
|
}
|
|
@@ -2885,7 +3142,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
2885
3142
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
2886
3143
|
}
|
|
2887
3144
|
if (integrations.typecheckHook) {
|
|
2888
|
-
const t = setupTypecheckHook(projectRoot);
|
|
3145
|
+
const t = setupTypecheckHook(projectRoot, opts.packageManager);
|
|
2889
3146
|
if (t) created.push(`${t} \u2014 added typecheck`);
|
|
2890
3147
|
}
|
|
2891
3148
|
if (integrations.lintHook && opts.linter) {
|
|
@@ -2922,11 +3179,11 @@ async function initCommand(options, cwd) {
|
|
|
2922
3179
|
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
2923
3180
|
);
|
|
2924
3181
|
}
|
|
2925
|
-
const configPath =
|
|
2926
|
-
if (
|
|
3182
|
+
const configPath = path19.join(projectRoot, CONFIG_FILE5);
|
|
3183
|
+
if (fs19.existsSync(configPath) && !options.force) {
|
|
2927
3184
|
console.log(
|
|
2928
|
-
`${
|
|
2929
|
-
Run ${
|
|
3185
|
+
`${chalk12.yellow("!")} viberails is already initialized.
|
|
3186
|
+
Run ${chalk12.cyan("viberails config")} to edit rules, ${chalk12.cyan("viberails sync")} to update, or ${chalk12.cyan("viberails init --force")} to start fresh.`
|
|
2930
3187
|
);
|
|
2931
3188
|
return;
|
|
2932
3189
|
}
|
|
@@ -2934,7 +3191,7 @@ async function initCommand(options, cwd) {
|
|
|
2934
3191
|
await initInteractive(projectRoot, configPath, options);
|
|
2935
3192
|
}
|
|
2936
3193
|
async function initNonInteractive(projectRoot, configPath) {
|
|
2937
|
-
console.log(
|
|
3194
|
+
console.log(chalk12.dim("Scanning project..."));
|
|
2938
3195
|
const scanResult = await scan2(projectRoot);
|
|
2939
3196
|
const config = generateConfig(scanResult);
|
|
2940
3197
|
for (const pkg of config.packages) {
|
|
@@ -2947,11 +3204,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2947
3204
|
const exempted = getExemptedPackages(config);
|
|
2948
3205
|
if (exempted.length > 0) {
|
|
2949
3206
|
console.log(
|
|
2950
|
-
` ${
|
|
3207
|
+
` ${chalk12.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk12.dim("(types-only)")}`
|
|
2951
3208
|
);
|
|
2952
3209
|
}
|
|
2953
3210
|
if (config.packages.length > 1) {
|
|
2954
|
-
console.log(
|
|
3211
|
+
console.log(chalk12.dim("Building import graph..."));
|
|
2955
3212
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2956
3213
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2957
3214
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -2964,16 +3221,16 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2964
3221
|
}
|
|
2965
3222
|
}
|
|
2966
3223
|
const compacted = compactConfig3(config);
|
|
2967
|
-
|
|
3224
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2968
3225
|
`);
|
|
2969
3226
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2970
3227
|
updateGitignore(projectRoot);
|
|
2971
3228
|
setupClaudeCodeHook(projectRoot);
|
|
2972
3229
|
setupClaudeMdReference(projectRoot);
|
|
2973
3230
|
const rootPkg = config.packages[0];
|
|
2974
|
-
const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
|
|
3231
|
+
const rootPkgPm = rootPkg?.stack?.packageManager?.split("@")[0] ?? "npm";
|
|
2975
3232
|
const linter = rootPkg?.stack?.linter?.split("@")[0];
|
|
2976
|
-
const isTypeScript = rootPkg?.stack?.language === "typescript";
|
|
3233
|
+
const isTypeScript = rootPkg?.stack?.language?.split("@")[0] === "typescript";
|
|
2977
3234
|
const actionTarget = setupGithubAction(projectRoot, rootPkgPm, {
|
|
2978
3235
|
linter,
|
|
2979
3236
|
typecheck: isTypeScript
|
|
@@ -2981,17 +3238,17 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2981
3238
|
const hookManager = detectHookManager(projectRoot);
|
|
2982
3239
|
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
2983
3240
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
2984
|
-
const ok =
|
|
3241
|
+
const ok = chalk12.green("\u2713");
|
|
2985
3242
|
const created = [
|
|
2986
|
-
`${ok} ${
|
|
3243
|
+
`${ok} ${path19.basename(configPath)}`,
|
|
2987
3244
|
`${ok} .viberails/context.md`,
|
|
2988
3245
|
`${ok} .viberails/scan-result.json`,
|
|
2989
3246
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
2990
3247
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
2991
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
3248
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk12.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
2992
3249
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
2993
3250
|
].filter(Boolean);
|
|
2994
|
-
if (hasHookManager &&
|
|
3251
|
+
if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
|
|
2995
3252
|
if (hasHookManager && linter) setupLintHook(projectRoot, linter);
|
|
2996
3253
|
console.log(`
|
|
2997
3254
|
Created:
|
|
@@ -2999,9 +3256,9 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
|
|
|
2999
3256
|
}
|
|
3000
3257
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3001
3258
|
clack8.intro("viberails");
|
|
3002
|
-
if (
|
|
3259
|
+
if (fs19.existsSync(configPath) && options.force) {
|
|
3003
3260
|
const replace = await confirmDangerous(
|
|
3004
|
-
`${
|
|
3261
|
+
`${path19.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3005
3262
|
);
|
|
3006
3263
|
if (!replace) {
|
|
3007
3264
|
clack8.outro("Aborted. No files were written.");
|
|
@@ -3013,13 +3270,6 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3013
3270
|
const scanResult = await scan2(projectRoot);
|
|
3014
3271
|
const config = generateConfig(scanResult);
|
|
3015
3272
|
s.stop("Scan complete");
|
|
3016
|
-
const prereqResult = await promptMissingPrereqs(
|
|
3017
|
-
projectRoot,
|
|
3018
|
-
checkCoveragePrereqs(projectRoot, scanResult)
|
|
3019
|
-
);
|
|
3020
|
-
if (prereqResult.disableCoverage) {
|
|
3021
|
-
config.rules.testCoverage = 0;
|
|
3022
|
-
}
|
|
3023
3273
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3024
3274
|
clack8.log.warn(
|
|
3025
3275
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
@@ -3060,20 +3310,28 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3060
3310
|
if (denyCount > 0) {
|
|
3061
3311
|
config.boundaries = inferred;
|
|
3062
3312
|
config.rules.enforceBoundaries = true;
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
clack8.note(boundaryLines, "Boundary rules");
|
|
3313
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3314
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3066
3315
|
} else {
|
|
3067
3316
|
bs.stop("No boundary rules inferred");
|
|
3068
3317
|
}
|
|
3069
3318
|
}
|
|
3070
3319
|
}
|
|
3071
3320
|
const hookManager = detectHookManager(projectRoot);
|
|
3321
|
+
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3322
|
+
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3323
|
+
if (hasMissingPrereqs) {
|
|
3324
|
+
clack8.log.info("Some dependencies are needed for full functionality.");
|
|
3325
|
+
}
|
|
3326
|
+
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3327
|
+
if (prereqResult.disableCoverage) {
|
|
3328
|
+
config.rules.testCoverage = 0;
|
|
3329
|
+
}
|
|
3072
3330
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
3073
3331
|
const integrations = await promptIntegrations(projectRoot, hookManager, {
|
|
3074
|
-
isTypeScript: rootPkgStack?.language === "typescript",
|
|
3332
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3075
3333
|
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3076
|
-
packageManager: rootPkgStack?.packageManager,
|
|
3334
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3077
3335
|
isWorkspace: config.packages.length > 1
|
|
3078
3336
|
});
|
|
3079
3337
|
const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
|
|
@@ -3082,40 +3340,37 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3082
3340
|
return;
|
|
3083
3341
|
}
|
|
3084
3342
|
const compacted = compactConfig3(config);
|
|
3085
|
-
|
|
3343
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3086
3344
|
`);
|
|
3087
3345
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3088
3346
|
updateGitignore(projectRoot);
|
|
3089
|
-
const
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
];
|
|
3098
|
-
clack8.log.success(`Created:
|
|
3099
|
-
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
3347
|
+
const ok = chalk12.green("\u2713");
|
|
3348
|
+
clack8.log.step(`${ok} ${path19.basename(configPath)}`);
|
|
3349
|
+
clack8.log.step(`${ok} .viberails/context.md`);
|
|
3350
|
+
clack8.log.step(`${ok} .viberails/scan-result.json`);
|
|
3351
|
+
setupSelectedIntegrations(projectRoot, integrations, {
|
|
3352
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3353
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0]
|
|
3354
|
+
});
|
|
3100
3355
|
clack8.outro(
|
|
3101
3356
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3102
|
-
${
|
|
3357
|
+
${chalk12.dim("Tip: use")} ${chalk12.cyan("viberails check --enforce")} ${chalk12.dim("in CI to block PRs on violations.")}`
|
|
3103
3358
|
);
|
|
3104
3359
|
}
|
|
3105
3360
|
|
|
3106
3361
|
// src/commands/sync.ts
|
|
3107
|
-
import * as
|
|
3108
|
-
import * as
|
|
3362
|
+
import * as fs20 from "fs";
|
|
3363
|
+
import * as path20 from "path";
|
|
3109
3364
|
import * as clack9 from "@clack/prompts";
|
|
3110
3365
|
import { compactConfig as compactConfig4, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
|
|
3111
3366
|
import { scan as scan3 } from "@viberails/scanner";
|
|
3112
|
-
import
|
|
3367
|
+
import chalk13 from "chalk";
|
|
3113
3368
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3114
3369
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3115
3370
|
function loadPreviousStats(projectRoot) {
|
|
3116
|
-
const scanResultPath =
|
|
3371
|
+
const scanResultPath = path20.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3117
3372
|
try {
|
|
3118
|
-
const raw =
|
|
3373
|
+
const raw = fs20.readFileSync(scanResultPath, "utf-8");
|
|
3119
3374
|
const parsed = JSON.parse(raw);
|
|
3120
3375
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3121
3376
|
return parsed.statistics;
|
|
@@ -3132,15 +3387,15 @@ async function syncCommand(options, cwd) {
|
|
|
3132
3387
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
3133
3388
|
);
|
|
3134
3389
|
}
|
|
3135
|
-
const configPath =
|
|
3390
|
+
const configPath = path20.join(projectRoot, CONFIG_FILE6);
|
|
3136
3391
|
const existing = await loadConfig5(configPath);
|
|
3137
3392
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3138
|
-
console.log(
|
|
3393
|
+
console.log(chalk13.dim("Scanning project..."));
|
|
3139
3394
|
const scanResult = await scan3(projectRoot);
|
|
3140
3395
|
const merged = mergeConfig2(existing, scanResult);
|
|
3141
3396
|
const compacted = compactConfig4(merged);
|
|
3142
3397
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3143
|
-
const rawDisk =
|
|
3398
|
+
const rawDisk = fs20.readFileSync(configPath, "utf-8").trim();
|
|
3144
3399
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3145
3400
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3146
3401
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3148,13 +3403,13 @@ async function syncCommand(options, cwd) {
|
|
|
3148
3403
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3149
3404
|
if (changes.length > 0 || statsDelta) {
|
|
3150
3405
|
console.log(`
|
|
3151
|
-
${
|
|
3406
|
+
${chalk13.bold("Changes:")}`);
|
|
3152
3407
|
for (const change of changes) {
|
|
3153
|
-
const icon = change.type === "removed" ?
|
|
3408
|
+
const icon = change.type === "removed" ? chalk13.red("-") : chalk13.green("+");
|
|
3154
3409
|
console.log(` ${icon} ${change.description}`);
|
|
3155
3410
|
}
|
|
3156
3411
|
if (statsDelta) {
|
|
3157
|
-
console.log(` ${
|
|
3412
|
+
console.log(` ${chalk13.dim(statsDelta)}`);
|
|
3158
3413
|
}
|
|
3159
3414
|
}
|
|
3160
3415
|
if (options?.interactive) {
|
|
@@ -3187,7 +3442,7 @@ ${chalk12.bold("Changes:")}`);
|
|
|
3187
3442
|
});
|
|
3188
3443
|
applyRuleOverrides(merged, overrides);
|
|
3189
3444
|
const recompacted = compactConfig4(merged);
|
|
3190
|
-
|
|
3445
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3191
3446
|
`);
|
|
3192
3447
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3193
3448
|
clack9.log.success("Updated config with your customizations.");
|
|
@@ -3195,22 +3450,22 @@ ${chalk12.bold("Changes:")}`);
|
|
|
3195
3450
|
return;
|
|
3196
3451
|
}
|
|
3197
3452
|
}
|
|
3198
|
-
|
|
3453
|
+
fs20.writeFileSync(configPath, `${compactedJson}
|
|
3199
3454
|
`);
|
|
3200
3455
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3201
3456
|
console.log(`
|
|
3202
|
-
${
|
|
3457
|
+
${chalk13.bold("Synced:")}`);
|
|
3203
3458
|
if (configChanged) {
|
|
3204
|
-
console.log(` ${
|
|
3459
|
+
console.log(` ${chalk13.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3205
3460
|
} else {
|
|
3206
|
-
console.log(` ${
|
|
3461
|
+
console.log(` ${chalk13.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3207
3462
|
}
|
|
3208
|
-
console.log(` ${
|
|
3209
|
-
console.log(` ${
|
|
3463
|
+
console.log(` ${chalk13.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
3464
|
+
console.log(` ${chalk13.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
3210
3465
|
}
|
|
3211
3466
|
|
|
3212
3467
|
// src/index.ts
|
|
3213
|
-
var VERSION = "0.
|
|
3468
|
+
var VERSION = "0.6.0";
|
|
3214
3469
|
var program = new Command();
|
|
3215
3470
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3216
3471
|
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) => {
|
|
@@ -3218,7 +3473,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3218
3473
|
await initCommand(options);
|
|
3219
3474
|
} catch (err) {
|
|
3220
3475
|
const message = err instanceof Error ? err.message : String(err);
|
|
3221
|
-
console.error(`${
|
|
3476
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3222
3477
|
process.exit(1);
|
|
3223
3478
|
}
|
|
3224
3479
|
});
|
|
@@ -3227,7 +3482,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3227
3482
|
await syncCommand(options);
|
|
3228
3483
|
} catch (err) {
|
|
3229
3484
|
const message = err instanceof Error ? err.message : String(err);
|
|
3230
|
-
console.error(`${
|
|
3485
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3231
3486
|
process.exit(1);
|
|
3232
3487
|
}
|
|
3233
3488
|
});
|
|
@@ -3236,7 +3491,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3236
3491
|
await configCommand(options);
|
|
3237
3492
|
} catch (err) {
|
|
3238
3493
|
const message = err instanceof Error ? err.message : String(err);
|
|
3239
|
-
console.error(`${
|
|
3494
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3240
3495
|
process.exit(1);
|
|
3241
3496
|
}
|
|
3242
3497
|
});
|
|
@@ -3257,7 +3512,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3257
3512
|
process.exit(exitCode);
|
|
3258
3513
|
} catch (err) {
|
|
3259
3514
|
const message = err instanceof Error ? err.message : String(err);
|
|
3260
|
-
console.error(`${
|
|
3515
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3261
3516
|
process.exit(1);
|
|
3262
3517
|
}
|
|
3263
3518
|
}
|
|
@@ -3268,7 +3523,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3268
3523
|
process.exit(exitCode);
|
|
3269
3524
|
} catch (err) {
|
|
3270
3525
|
const message = err instanceof Error ? err.message : String(err);
|
|
3271
|
-
console.error(`${
|
|
3526
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3272
3527
|
process.exit(1);
|
|
3273
3528
|
}
|
|
3274
3529
|
});
|
|
@@ -3277,7 +3532,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3277
3532
|
await boundariesCommand(options);
|
|
3278
3533
|
} catch (err) {
|
|
3279
3534
|
const message = err instanceof Error ? err.message : String(err);
|
|
3280
|
-
console.error(`${
|
|
3535
|
+
console.error(`${chalk14.red("Error:")} ${message}`);
|
|
3281
3536
|
process.exit(1);
|
|
3282
3537
|
}
|
|
3283
3538
|
});
|