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.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
VERSION: () => VERSION
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_chalk14 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -95,11 +95,11 @@ async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace
|
|
|
95
95
|
stdio: "pipe"
|
|
96
96
|
});
|
|
97
97
|
if (result.status === 0) {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const lefthookPath =
|
|
101
|
-
if (!
|
|
102
|
-
|
|
98
|
+
const fs21 = await import("fs");
|
|
99
|
+
const path21 = await import("path");
|
|
100
|
+
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
101
|
+
if (!fs21.existsSync(lefthookPath)) {
|
|
102
|
+
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
103
103
|
}
|
|
104
104
|
s.stop("Installed Lefthook");
|
|
105
105
|
return "Lefthook";
|
|
@@ -131,7 +131,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
|
131
131
|
options.push({
|
|
132
132
|
value: "typecheck",
|
|
133
133
|
label: "Typecheck (tsc --noEmit)",
|
|
134
|
-
hint: "
|
|
134
|
+
hint: "pre-commit hook + CI check"
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
if (tools?.linter) {
|
|
@@ -139,7 +139,7 @@ async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
|
139
139
|
options.push({
|
|
140
140
|
value: "lint",
|
|
141
141
|
label: `Lint check (${linterName})`,
|
|
142
|
-
hint: "
|
|
142
|
+
hint: "pre-commit hook + CI check"
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
options.push(
|
|
@@ -731,7 +731,7 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
|
|
|
731
731
|
var fs7 = __toESM(require("fs"), 1);
|
|
732
732
|
var path7 = __toESM(require("path"), 1);
|
|
733
733
|
var import_config5 = require("@viberails/config");
|
|
734
|
-
var
|
|
734
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
735
735
|
|
|
736
736
|
// src/commands/check-config.ts
|
|
737
737
|
var import_config2 = require("@viberails/config");
|
|
@@ -822,7 +822,8 @@ function runCoverageCommand(pkgRoot, command) {
|
|
|
822
822
|
cwd: pkgRoot,
|
|
823
823
|
shell: true,
|
|
824
824
|
encoding: "utf-8",
|
|
825
|
-
stdio: "pipe"
|
|
825
|
+
stdio: "pipe",
|
|
826
|
+
timeout: 3e5
|
|
826
827
|
});
|
|
827
828
|
if (result.status === 0) return { ok: true };
|
|
828
829
|
const stderr = result.stderr?.trim() ?? "";
|
|
@@ -985,7 +986,7 @@ function checkNaming(relPath, conventions) {
|
|
|
985
986
|
}
|
|
986
987
|
function getStagedFiles(projectRoot) {
|
|
987
988
|
try {
|
|
988
|
-
const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=
|
|
989
|
+
const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
|
|
989
990
|
cwd: projectRoot,
|
|
990
991
|
encoding: "utf-8",
|
|
991
992
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1012,7 +1013,62 @@ function getDiffFiles(projectRoot, base) {
|
|
|
1012
1013
|
added: addedOutput.trim().split("\n").filter(Boolean)
|
|
1013
1014
|
};
|
|
1014
1015
|
} catch {
|
|
1015
|
-
|
|
1016
|
+
const msg = `git diff failed for base '${base}' \u2014 no files will be checked`;
|
|
1017
|
+
process.stderr.write(`Warning: ${msg}
|
|
1018
|
+
`);
|
|
1019
|
+
return { all: [], added: [], error: msg };
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function testFileToSourceFile(testFile) {
|
|
1023
|
+
const match = testFile.match(/^(.+)\.(test|spec)(\.[^.]+)$/);
|
|
1024
|
+
if (!match) return null;
|
|
1025
|
+
return `${match[1]}${match[3]}`;
|
|
1026
|
+
}
|
|
1027
|
+
function deletedTestFileToSourceFile(deletedTestFile, config) {
|
|
1028
|
+
const normalized = deletedTestFile.replaceAll("\\", "/");
|
|
1029
|
+
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
1030
|
+
for (const pkg of sortedPackages) {
|
|
1031
|
+
const relInPkg = pkg.path === "." ? normalized : normalized.startsWith(`${pkg.path}/`) ? normalized.slice(pkg.path.length + 1) : null;
|
|
1032
|
+
if (relInPkg === null) continue;
|
|
1033
|
+
const srcDir = pkg.structure?.srcDir;
|
|
1034
|
+
if (!srcDir) continue;
|
|
1035
|
+
const testsDir = pkg.structure?.tests;
|
|
1036
|
+
if (testsDir && relInPkg.startsWith(`${testsDir}/`)) {
|
|
1037
|
+
const relWithinTests = relInPkg.slice(testsDir.length + 1);
|
|
1038
|
+
const relWithinSrc = testFileToSourceFile(relWithinTests);
|
|
1039
|
+
if (relWithinSrc) {
|
|
1040
|
+
return pkg.path === "." ? path5.posix.join(srcDir, relWithinSrc) : path5.posix.join(pkg.path, srcDir, relWithinSrc);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const colocated = testFileToSourceFile(relInPkg);
|
|
1044
|
+
if (colocated) {
|
|
1045
|
+
return pkg.path === "." ? colocated : path5.posix.join(pkg.path, colocated);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
1051
|
+
try {
|
|
1052
|
+
const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=D", {
|
|
1053
|
+
cwd: projectRoot,
|
|
1054
|
+
encoding: "utf-8",
|
|
1055
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1056
|
+
});
|
|
1057
|
+
return output.trim().split("\n").filter(Boolean).map((file) => deletedTestFileToSourceFile(file, config)).filter((f) => f !== null);
|
|
1058
|
+
} catch {
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
|
|
1063
|
+
try {
|
|
1064
|
+
const output = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
|
|
1065
|
+
cwd: projectRoot,
|
|
1066
|
+
encoding: "utf-8",
|
|
1067
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1068
|
+
});
|
|
1069
|
+
return output.trim().split("\n").filter(Boolean).map((file) => deletedTestFileToSourceFile(file, config)).filter((f) => f !== null);
|
|
1070
|
+
} catch {
|
|
1071
|
+
return [];
|
|
1016
1072
|
}
|
|
1017
1073
|
}
|
|
1018
1074
|
function getAllSourceFiles(projectRoot, config) {
|
|
@@ -1055,7 +1111,7 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
1055
1111
|
}
|
|
1056
1112
|
for (const entry of entries) {
|
|
1057
1113
|
if (entry.isDirectory()) {
|
|
1058
|
-
if (entry.name
|
|
1114
|
+
if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;
|
|
1059
1115
|
walk(path5.join(d, entry.name));
|
|
1060
1116
|
} else if (entry.isFile()) {
|
|
1061
1117
|
files.push(path5.relative(projectRoot, path5.join(d, entry.name)));
|
|
@@ -1066,6 +1122,55 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
1066
1122
|
return files;
|
|
1067
1123
|
}
|
|
1068
1124
|
|
|
1125
|
+
// src/commands/check-print.ts
|
|
1126
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
1127
|
+
function printGroupedViolations(violations, limit) {
|
|
1128
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const v of violations) {
|
|
1130
|
+
const existing = groups.get(v.rule) ?? [];
|
|
1131
|
+
existing.push(v);
|
|
1132
|
+
groups.set(v.rule, existing);
|
|
1133
|
+
}
|
|
1134
|
+
const ruleOrder = [
|
|
1135
|
+
"file-size",
|
|
1136
|
+
"file-naming",
|
|
1137
|
+
"missing-test",
|
|
1138
|
+
"test-coverage",
|
|
1139
|
+
"boundary-violation"
|
|
1140
|
+
];
|
|
1141
|
+
const sortedKeys = [...groups.keys()].sort(
|
|
1142
|
+
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
1143
|
+
);
|
|
1144
|
+
let totalShown = 0;
|
|
1145
|
+
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
1146
|
+
for (const rule of sortedKeys) {
|
|
1147
|
+
const group = groups.get(rule);
|
|
1148
|
+
if (!group) continue;
|
|
1149
|
+
const remaining = totalLimit - totalShown;
|
|
1150
|
+
if (remaining <= 0) break;
|
|
1151
|
+
const toShow = group.slice(0, remaining);
|
|
1152
|
+
const hidden = group.length - toShow.length;
|
|
1153
|
+
for (const v of toShow) {
|
|
1154
|
+
const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
|
|
1155
|
+
console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
1156
|
+
}
|
|
1157
|
+
totalShown += toShow.length;
|
|
1158
|
+
if (hidden > 0) {
|
|
1159
|
+
console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function printSummary(violations) {
|
|
1164
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1165
|
+
for (const v of violations) {
|
|
1166
|
+
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
1167
|
+
}
|
|
1168
|
+
const word = violations.length === 1 ? "violation" : "violations";
|
|
1169
|
+
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
1170
|
+
console.log(`
|
|
1171
|
+
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1069
1174
|
// src/commands/check-tests.ts
|
|
1070
1175
|
var fs6 = __toESM(require("fs"), 1);
|
|
1071
1176
|
var path6 = __toESM(require("path"), 1);
|
|
@@ -1095,18 +1200,19 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1095
1200
|
const testSuffix = testPattern.replace("*", "");
|
|
1096
1201
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1097
1202
|
for (const relFile of sourceFiles) {
|
|
1098
|
-
const
|
|
1099
|
-
if (
|
|
1203
|
+
const basename9 = path6.basename(relFile);
|
|
1204
|
+
if (basename9.includes(".test.") || basename9.includes(".spec.") || basename9.startsWith("index.") || basename9.endsWith(".d.ts")) {
|
|
1100
1205
|
continue;
|
|
1101
1206
|
}
|
|
1102
|
-
const ext = path6.extname(
|
|
1207
|
+
const ext = path6.extname(basename9);
|
|
1103
1208
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1104
|
-
const stem =
|
|
1209
|
+
const stem = basename9.slice(0, -ext.length);
|
|
1105
1210
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1106
1211
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1107
1212
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
1108
1213
|
const testsDir = pkg.structure?.tests;
|
|
1109
|
-
const
|
|
1214
|
+
const relToSrc = path6.relative(srcPath, path6.join(projectRoot, path6.dirname(relFile)));
|
|
1215
|
+
const dedicatedTest = testsDir ? path6.join(packageRoot2, testsDir, relToSrc, expectedTestFile) : null;
|
|
1110
1216
|
const hasTest = fs6.existsSync(colocatedTest) || dedicatedTest !== null && fs6.existsSync(dedicatedTest);
|
|
1111
1217
|
if (!hasTest) {
|
|
1112
1218
|
violations.push({
|
|
@@ -1136,91 +1242,52 @@ function isTestFile(relPath) {
|
|
|
1136
1242
|
const filename = path7.basename(relPath);
|
|
1137
1243
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
1138
1244
|
}
|
|
1139
|
-
function printGroupedViolations(violations, limit) {
|
|
1140
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1141
|
-
for (const v of violations) {
|
|
1142
|
-
const existing = groups.get(v.rule) ?? [];
|
|
1143
|
-
existing.push(v);
|
|
1144
|
-
groups.set(v.rule, existing);
|
|
1145
|
-
}
|
|
1146
|
-
const ruleOrder = [
|
|
1147
|
-
"file-size",
|
|
1148
|
-
"file-naming",
|
|
1149
|
-
"missing-test",
|
|
1150
|
-
"test-coverage",
|
|
1151
|
-
"boundary-violation"
|
|
1152
|
-
];
|
|
1153
|
-
const sortedKeys = [...groups.keys()].sort(
|
|
1154
|
-
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
1155
|
-
);
|
|
1156
|
-
let totalShown = 0;
|
|
1157
|
-
const totalLimit = limit ?? Number.POSITIVE_INFINITY;
|
|
1158
|
-
for (const rule of sortedKeys) {
|
|
1159
|
-
const group = groups.get(rule);
|
|
1160
|
-
if (!group) continue;
|
|
1161
|
-
const remaining = totalLimit - totalShown;
|
|
1162
|
-
if (remaining <= 0) break;
|
|
1163
|
-
const toShow = group.slice(0, remaining);
|
|
1164
|
-
const hidden = group.length - toShow.length;
|
|
1165
|
-
for (const v of toShow) {
|
|
1166
|
-
const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
|
|
1167
|
-
console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
1168
|
-
}
|
|
1169
|
-
totalShown += toShow.length;
|
|
1170
|
-
if (hidden > 0) {
|
|
1171
|
-
console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
function printSummary(violations) {
|
|
1176
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1177
|
-
for (const v of violations) {
|
|
1178
|
-
counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
|
|
1179
|
-
}
|
|
1180
|
-
const word = violations.length === 1 ? "violation" : "violations";
|
|
1181
|
-
const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
|
|
1182
|
-
console.log(`
|
|
1183
|
-
${violations.length} ${word} found (${parts.join(", ")}).`);
|
|
1184
|
-
}
|
|
1185
1245
|
async function checkCommand(options, cwd) {
|
|
1186
1246
|
const startDir = cwd ?? process.cwd();
|
|
1187
1247
|
const projectRoot = findProjectRoot(startDir);
|
|
1188
1248
|
if (!projectRoot) {
|
|
1189
|
-
console.error(`${
|
|
1249
|
+
console.error(`${import_chalk3.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
1190
1250
|
return 1;
|
|
1191
1251
|
}
|
|
1192
1252
|
const configPath = path7.join(projectRoot, CONFIG_FILE2);
|
|
1193
1253
|
if (!fs7.existsSync(configPath)) {
|
|
1194
1254
|
console.error(
|
|
1195
|
-
`${
|
|
1255
|
+
`${import_chalk3.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
1196
1256
|
);
|
|
1197
1257
|
return 1;
|
|
1198
1258
|
}
|
|
1199
1259
|
const config = await (0, import_config5.loadConfig)(configPath);
|
|
1200
1260
|
let filesToCheck;
|
|
1201
1261
|
let diffAddedFiles = null;
|
|
1262
|
+
let deletedTestSourceFiles = [];
|
|
1202
1263
|
if (options.staged) {
|
|
1203
|
-
filesToCheck = getStagedFiles(projectRoot);
|
|
1264
|
+
filesToCheck = getStagedFiles(projectRoot).filter((f) => SOURCE_EXTS.has(path7.extname(f)));
|
|
1265
|
+
deletedTestSourceFiles = getStagedDeletedTestSourceFiles(projectRoot, config);
|
|
1204
1266
|
} else if (options.diffBase) {
|
|
1205
1267
|
const diff = getDiffFiles(projectRoot, options.diffBase);
|
|
1268
|
+
if (diff.error && options.enforce) {
|
|
1269
|
+
console.error(`${import_chalk3.default.red("Error:")} ${diff.error}`);
|
|
1270
|
+
return 1;
|
|
1271
|
+
}
|
|
1206
1272
|
filesToCheck = diff.all.filter((f) => SOURCE_EXTS.has(path7.extname(f)));
|
|
1207
1273
|
diffAddedFiles = new Set(diff.added);
|
|
1274
|
+
deletedTestSourceFiles = getDiffDeletedTestSourceFiles(projectRoot, options.diffBase, config);
|
|
1208
1275
|
} else if (options.files && options.files.length > 0) {
|
|
1209
1276
|
filesToCheck = options.files;
|
|
1210
1277
|
} else {
|
|
1211
1278
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
1212
1279
|
}
|
|
1213
|
-
if (filesToCheck.length === 0) {
|
|
1280
|
+
if (filesToCheck.length === 0 && deletedTestSourceFiles.length === 0) {
|
|
1214
1281
|
if (options.format === "json") {
|
|
1215
1282
|
console.log(JSON.stringify({ violations: [], checkedFiles: 0 }));
|
|
1216
1283
|
} else {
|
|
1217
|
-
console.log(`${
|
|
1284
|
+
console.log(`${import_chalk3.default.green("\u2713")} No files to check.`);
|
|
1218
1285
|
}
|
|
1219
1286
|
return 0;
|
|
1220
1287
|
}
|
|
1221
1288
|
const violations = [];
|
|
1222
1289
|
const severity = options.enforce ? "error" : "warn";
|
|
1223
|
-
const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(
|
|
1290
|
+
const log7 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
|
|
1224
1291
|
};
|
|
1225
1292
|
log7(" Checking files...");
|
|
1226
1293
|
for (const file of filesToCheck) {
|
|
@@ -1256,12 +1323,20 @@ async function checkCommand(options, cwd) {
|
|
|
1256
1323
|
}
|
|
1257
1324
|
}
|
|
1258
1325
|
log7(" done\n");
|
|
1259
|
-
if (!options.
|
|
1326
|
+
if (!options.files) {
|
|
1260
1327
|
log7(" Checking missing tests...");
|
|
1261
1328
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1329
|
+
if (options.staged) {
|
|
1330
|
+
const stagedSet = new Set(filesToCheck);
|
|
1331
|
+
for (const f of deletedTestSourceFiles) stagedSet.add(f);
|
|
1332
|
+
violations.push(...testViolations.filter((v) => stagedSet.has(v.file)));
|
|
1333
|
+
} else if (diffAddedFiles) {
|
|
1334
|
+
const checkSet = new Set(diffAddedFiles);
|
|
1335
|
+
for (const f of deletedTestSourceFiles) checkSet.add(f);
|
|
1336
|
+
violations.push(...testViolations.filter((v) => checkSet.has(v.file)));
|
|
1337
|
+
} else {
|
|
1338
|
+
violations.push(...testViolations);
|
|
1339
|
+
}
|
|
1265
1340
|
log7(" done\n");
|
|
1266
1341
|
}
|
|
1267
1342
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
@@ -1307,7 +1382,7 @@ async function checkCommand(options, cwd) {
|
|
|
1307
1382
|
return options.enforce && violations.length > 0 ? 1 : 0;
|
|
1308
1383
|
}
|
|
1309
1384
|
if (violations.length === 0) {
|
|
1310
|
-
console.log(`${
|
|
1385
|
+
console.log(`${import_chalk3.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
1311
1386
|
return 0;
|
|
1312
1387
|
}
|
|
1313
1388
|
if (!options.quiet) {
|
|
@@ -1315,7 +1390,7 @@ async function checkCommand(options, cwd) {
|
|
|
1315
1390
|
}
|
|
1316
1391
|
printSummary(violations);
|
|
1317
1392
|
if (options.enforce) {
|
|
1318
|
-
console.log(
|
|
1393
|
+
console.log(import_chalk3.default.red("Fix violations before committing."));
|
|
1319
1394
|
return 1;
|
|
1320
1395
|
}
|
|
1321
1396
|
return 0;
|
|
@@ -1373,14 +1448,14 @@ var path9 = __toESM(require("path"), 1);
|
|
|
1373
1448
|
var clack6 = __toESM(require("@clack/prompts"), 1);
|
|
1374
1449
|
var import_config6 = require("@viberails/config");
|
|
1375
1450
|
var import_scanner = require("@viberails/scanner");
|
|
1376
|
-
var
|
|
1451
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1377
1452
|
|
|
1378
1453
|
// src/display-text.ts
|
|
1379
1454
|
var import_types4 = require("@viberails/types");
|
|
1380
1455
|
|
|
1381
1456
|
// src/display.ts
|
|
1382
1457
|
var import_types3 = require("@viberails/types");
|
|
1383
|
-
var
|
|
1458
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1384
1459
|
|
|
1385
1460
|
// src/display-helpers.ts
|
|
1386
1461
|
var import_types = require("@viberails/types");
|
|
@@ -1433,7 +1508,7 @@ function formatRoleGroup(group) {
|
|
|
1433
1508
|
|
|
1434
1509
|
// src/display-monorepo.ts
|
|
1435
1510
|
var import_types2 = require("@viberails/types");
|
|
1436
|
-
var
|
|
1511
|
+
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
1437
1512
|
function formatPackageSummary(pkg) {
|
|
1438
1513
|
const parts = [];
|
|
1439
1514
|
if (pkg.stack.framework) {
|
|
@@ -1442,30 +1517,31 @@ function formatPackageSummary(pkg) {
|
|
|
1442
1517
|
if (pkg.stack.styling) {
|
|
1443
1518
|
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1444
1519
|
}
|
|
1445
|
-
const
|
|
1520
|
+
const n = pkg.statistics.totalFiles;
|
|
1521
|
+
const files = `${n} ${n === 1 ? "file" : "files"}`;
|
|
1446
1522
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1447
1523
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1448
1524
|
}
|
|
1449
1525
|
function displayMonorepoResults(scanResult) {
|
|
1450
1526
|
const { stack, packages } = scanResult;
|
|
1451
1527
|
console.log(`
|
|
1452
|
-
${
|
|
1453
|
-
console.log(` ${
|
|
1528
|
+
${import_chalk4.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1529
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1454
1530
|
if (stack.packageManager) {
|
|
1455
|
-
console.log(` ${
|
|
1531
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1456
1532
|
}
|
|
1457
1533
|
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1458
|
-
console.log(` ${
|
|
1534
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1459
1535
|
} else {
|
|
1460
1536
|
if (stack.linter) {
|
|
1461
|
-
console.log(` ${
|
|
1537
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1462
1538
|
}
|
|
1463
1539
|
if (stack.formatter) {
|
|
1464
|
-
console.log(` ${
|
|
1540
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1465
1541
|
}
|
|
1466
1542
|
}
|
|
1467
1543
|
if (stack.testRunner) {
|
|
1468
|
-
console.log(` ${
|
|
1544
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1469
1545
|
}
|
|
1470
1546
|
console.log("");
|
|
1471
1547
|
for (const pkg of packages) {
|
|
@@ -1476,13 +1552,13 @@ ${import_chalk3.default.bold(`Detected: (monorepo, ${packages.length} packages)`
|
|
|
1476
1552
|
);
|
|
1477
1553
|
if (packagesWithDirs.length > 0) {
|
|
1478
1554
|
console.log(`
|
|
1479
|
-
${
|
|
1555
|
+
${import_chalk4.default.bold("Structure:")}`);
|
|
1480
1556
|
for (const pkg of packagesWithDirs) {
|
|
1481
1557
|
const groups = groupByRole(pkg.structure.directories);
|
|
1482
1558
|
if (groups.length === 0) continue;
|
|
1483
1559
|
console.log(` ${pkg.relativePath}:`);
|
|
1484
1560
|
for (const group of groups) {
|
|
1485
|
-
console.log(` ${
|
|
1561
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1486
1562
|
}
|
|
1487
1563
|
}
|
|
1488
1564
|
}
|
|
@@ -1498,7 +1574,8 @@ function formatPackageSummaryPlain(pkg) {
|
|
|
1498
1574
|
if (pkg.stack.styling) {
|
|
1499
1575
|
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1500
1576
|
}
|
|
1501
|
-
const
|
|
1577
|
+
const n = pkg.statistics.totalFiles;
|
|
1578
|
+
const files = `${n} ${n === 1 ? "file" : "files"}`;
|
|
1502
1579
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1503
1580
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1504
1581
|
}
|
|
@@ -1562,7 +1639,7 @@ function displayConventions(scanResult) {
|
|
|
1562
1639
|
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1563
1640
|
if (conventionEntries.length === 0) return;
|
|
1564
1641
|
console.log(`
|
|
1565
|
-
${
|
|
1642
|
+
${import_chalk5.default.bold("Conventions:")}`);
|
|
1566
1643
|
for (const [key, convention] of conventionEntries) {
|
|
1567
1644
|
if (convention.confidence === "low") continue;
|
|
1568
1645
|
const label = import_types3.CONVENTION_LABELS[key] ?? key;
|
|
@@ -1570,19 +1647,19 @@ ${import_chalk4.default.bold("Conventions:")}`);
|
|
|
1570
1647
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1571
1648
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1572
1649
|
if (allSame || pkgValues.length <= 1) {
|
|
1573
|
-
const ind = convention.confidence === "high" ?
|
|
1574
|
-
const detail =
|
|
1650
|
+
const ind = convention.confidence === "high" ? import_chalk5.default.green("\u2713") : import_chalk5.default.yellow("~");
|
|
1651
|
+
const detail = import_chalk5.default.dim(`(${confidenceLabel(convention)})`);
|
|
1575
1652
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1576
1653
|
} else {
|
|
1577
|
-
console.log(` ${
|
|
1654
|
+
console.log(` ${import_chalk5.default.yellow("~")} ${label}: varies by package`);
|
|
1578
1655
|
for (const pv of pkgValues) {
|
|
1579
1656
|
const pct = Math.round(pv.convention.consistency);
|
|
1580
1657
|
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1581
1658
|
}
|
|
1582
1659
|
}
|
|
1583
1660
|
} else {
|
|
1584
|
-
const ind = convention.confidence === "high" ?
|
|
1585
|
-
const detail =
|
|
1661
|
+
const ind = convention.confidence === "high" ? import_chalk5.default.green("\u2713") : import_chalk5.default.yellow("~");
|
|
1662
|
+
const detail = import_chalk5.default.dim(`(${confidenceLabel(convention)})`);
|
|
1586
1663
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1587
1664
|
}
|
|
1588
1665
|
}
|
|
@@ -1590,7 +1667,7 @@ ${import_chalk4.default.bold("Conventions:")}`);
|
|
|
1590
1667
|
function displaySummarySection(scanResult) {
|
|
1591
1668
|
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1592
1669
|
console.log(`
|
|
1593
|
-
${
|
|
1670
|
+
${import_chalk5.default.bold("Summary:")}`);
|
|
1594
1671
|
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1595
1672
|
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1596
1673
|
if (ext) {
|
|
@@ -1604,47 +1681,47 @@ function displayScanResults(scanResult) {
|
|
|
1604
1681
|
}
|
|
1605
1682
|
const { stack } = scanResult;
|
|
1606
1683
|
console.log(`
|
|
1607
|
-
${
|
|
1684
|
+
${import_chalk5.default.bold("Detected:")}`);
|
|
1608
1685
|
if (stack.framework) {
|
|
1609
|
-
console.log(` ${
|
|
1686
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
1610
1687
|
}
|
|
1611
|
-
console.log(` ${
|
|
1688
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1612
1689
|
if (stack.styling) {
|
|
1613
|
-
console.log(` ${
|
|
1690
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
1614
1691
|
}
|
|
1615
1692
|
if (stack.backend) {
|
|
1616
|
-
console.log(` ${
|
|
1693
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
1617
1694
|
}
|
|
1618
1695
|
if (stack.orm) {
|
|
1619
|
-
console.log(` ${
|
|
1696
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
|
|
1620
1697
|
}
|
|
1621
1698
|
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1622
|
-
console.log(` ${
|
|
1699
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1623
1700
|
} else {
|
|
1624
1701
|
if (stack.linter) {
|
|
1625
|
-
console.log(` ${
|
|
1702
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1626
1703
|
}
|
|
1627
1704
|
if (stack.formatter) {
|
|
1628
|
-
console.log(` ${
|
|
1705
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1629
1706
|
}
|
|
1630
1707
|
}
|
|
1631
1708
|
if (stack.testRunner) {
|
|
1632
|
-
console.log(` ${
|
|
1709
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1633
1710
|
}
|
|
1634
1711
|
if (stack.packageManager) {
|
|
1635
|
-
console.log(` ${
|
|
1712
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1636
1713
|
}
|
|
1637
1714
|
if (stack.libraries.length > 0) {
|
|
1638
1715
|
for (const lib of stack.libraries) {
|
|
1639
|
-
console.log(` ${
|
|
1716
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
1640
1717
|
}
|
|
1641
1718
|
}
|
|
1642
1719
|
const groups = groupByRole(scanResult.structure.directories);
|
|
1643
1720
|
if (groups.length > 0) {
|
|
1644
1721
|
console.log(`
|
|
1645
|
-
${
|
|
1722
|
+
${import_chalk5.default.bold("Structure:")}`);
|
|
1646
1723
|
for (const group of groups) {
|
|
1647
|
-
console.log(` ${
|
|
1724
|
+
console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1648
1725
|
}
|
|
1649
1726
|
}
|
|
1650
1727
|
displayConventions(scanResult);
|
|
@@ -1654,49 +1731,49 @@ ${import_chalk4.default.bold("Structure:")}`);
|
|
|
1654
1731
|
function displayRulesPreview(config) {
|
|
1655
1732
|
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1656
1733
|
console.log(
|
|
1657
|
-
`${
|
|
1734
|
+
`${import_chalk5.default.bold("Rules:")} ${import_chalk5.default.dim("(warns on violation; use --enforce in CI to block)")}`
|
|
1658
1735
|
);
|
|
1659
|
-
console.log(` ${
|
|
1736
|
+
console.log(` ${import_chalk5.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1660
1737
|
if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
|
|
1661
1738
|
console.log(
|
|
1662
|
-
` ${
|
|
1739
|
+
` ${import_chalk5.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
|
|
1663
1740
|
);
|
|
1664
1741
|
} else if (config.rules.testCoverage > 0) {
|
|
1665
|
-
console.log(` ${
|
|
1742
|
+
console.log(` ${import_chalk5.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
|
|
1666
1743
|
} else {
|
|
1667
|
-
console.log(` ${
|
|
1744
|
+
console.log(` ${import_chalk5.default.dim("\u2022")} Test coverage target: disabled`);
|
|
1668
1745
|
}
|
|
1669
1746
|
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
1670
|
-
console.log(` ${
|
|
1747
|
+
console.log(` ${import_chalk5.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1671
1748
|
} else {
|
|
1672
|
-
console.log(` ${
|
|
1749
|
+
console.log(` ${import_chalk5.default.dim("\u2022")} Enforce file naming: no`);
|
|
1673
1750
|
}
|
|
1674
1751
|
console.log(
|
|
1675
|
-
` ${
|
|
1752
|
+
` ${import_chalk5.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1676
1753
|
);
|
|
1677
1754
|
console.log("");
|
|
1678
1755
|
}
|
|
1679
1756
|
function displayInitSummary(config, exemptedPackages) {
|
|
1680
1757
|
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1681
1758
|
const isMonorepo = config.packages.length > 1;
|
|
1682
|
-
const ok =
|
|
1683
|
-
const off =
|
|
1759
|
+
const ok = import_chalk5.default.green("\u2713");
|
|
1760
|
+
const off = import_chalk5.default.dim("\u25CB");
|
|
1684
1761
|
console.log("");
|
|
1685
|
-
console.log(` ${
|
|
1686
|
-
console.log(` ${ok} Max file size: ${
|
|
1762
|
+
console.log(` ${import_chalk5.default.bold("Rules to apply:")}`);
|
|
1763
|
+
console.log(` ${ok} Max file size: ${import_chalk5.default.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1687
1764
|
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1688
1765
|
if (config.rules.enforceNaming && fileNaming) {
|
|
1689
|
-
console.log(` ${ok} File naming: ${
|
|
1766
|
+
console.log(` ${ok} File naming: ${import_chalk5.default.cyan(fileNaming)}`);
|
|
1690
1767
|
} else {
|
|
1691
|
-
console.log(` ${off} File naming: ${
|
|
1768
|
+
console.log(` ${off} File naming: ${import_chalk5.default.dim("not enforced")}`);
|
|
1692
1769
|
}
|
|
1693
1770
|
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1694
1771
|
if (config.rules.enforceMissingTests && testPattern) {
|
|
1695
|
-
console.log(` ${ok} Missing tests: ${
|
|
1772
|
+
console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan(`enforced (${testPattern})`)}`);
|
|
1696
1773
|
} else if (config.rules.enforceMissingTests) {
|
|
1697
|
-
console.log(` ${ok} Missing tests: ${
|
|
1774
|
+
console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan("enforced")}`);
|
|
1698
1775
|
} else {
|
|
1699
|
-
console.log(` ${off} Missing tests: ${
|
|
1776
|
+
console.log(` ${off} Missing tests: ${import_chalk5.default.dim("not enforced")}`);
|
|
1700
1777
|
}
|
|
1701
1778
|
if (config.rules.testCoverage > 0) {
|
|
1702
1779
|
if (isMonorepo) {
|
|
@@ -1704,27 +1781,27 @@ function displayInitSummary(config, exemptedPackages) {
|
|
|
1704
1781
|
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1705
1782
|
);
|
|
1706
1783
|
console.log(
|
|
1707
|
-
` ${ok} Coverage: ${
|
|
1784
|
+
` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk5.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1708
1785
|
);
|
|
1709
1786
|
} else {
|
|
1710
|
-
console.log(` ${ok} Coverage: ${
|
|
1787
|
+
console.log(` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1711
1788
|
}
|
|
1712
1789
|
} else {
|
|
1713
|
-
console.log(` ${off} Coverage: ${
|
|
1790
|
+
console.log(` ${off} Coverage: ${import_chalk5.default.dim("disabled")}`);
|
|
1714
1791
|
}
|
|
1715
1792
|
if (exemptedPackages.length > 0) {
|
|
1716
1793
|
console.log(
|
|
1717
|
-
` ${
|
|
1794
|
+
` ${import_chalk5.default.dim(" exempted:")} ${import_chalk5.default.dim(exemptedPackages.join(", "))} ${import_chalk5.default.dim("(types-only)")}`
|
|
1718
1795
|
);
|
|
1719
1796
|
}
|
|
1720
1797
|
if (isMonorepo) {
|
|
1721
1798
|
console.log(
|
|
1722
1799
|
`
|
|
1723
|
-
${
|
|
1800
|
+
${import_chalk5.default.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
|
|
1724
1801
|
);
|
|
1725
1802
|
} else {
|
|
1726
1803
|
console.log(`
|
|
1727
|
-
${
|
|
1804
|
+
${import_chalk5.default.dim("warns on violation \xB7 use --enforce in CI to block")}`);
|
|
1728
1805
|
}
|
|
1729
1806
|
console.log("");
|
|
1730
1807
|
}
|
|
@@ -2036,7 +2113,7 @@ async function configCommand(options, cwd) {
|
|
|
2036
2113
|
}
|
|
2037
2114
|
const configPath = path9.join(projectRoot, CONFIG_FILE3);
|
|
2038
2115
|
if (!fs10.existsSync(configPath)) {
|
|
2039
|
-
console.log(`${
|
|
2116
|
+
console.log(`${import_chalk6.default.yellow("!")} No config found. Run ${import_chalk6.default.cyan("viberails init")} first.`);
|
|
2040
2117
|
return;
|
|
2041
2118
|
}
|
|
2042
2119
|
clack6.intro("viberails config");
|
|
@@ -2121,22 +2198,22 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
2121
2198
|
var fs13 = __toESM(require("fs"), 1);
|
|
2122
2199
|
var path13 = __toESM(require("path"), 1);
|
|
2123
2200
|
var import_config7 = require("@viberails/config");
|
|
2124
|
-
var
|
|
2201
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
2125
2202
|
|
|
2126
2203
|
// src/commands/fix-helpers.ts
|
|
2127
2204
|
var import_node_child_process4 = require("child_process");
|
|
2128
|
-
var
|
|
2205
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
2129
2206
|
function printPlan(renames, stubs) {
|
|
2130
2207
|
if (renames.length > 0) {
|
|
2131
|
-
console.log(
|
|
2208
|
+
console.log(import_chalk7.default.bold("\nFile renames:"));
|
|
2132
2209
|
for (const r of renames) {
|
|
2133
|
-
console.log(` ${
|
|
2210
|
+
console.log(` ${import_chalk7.default.red(r.oldPath)} \u2192 ${import_chalk7.default.green(r.newPath)}`);
|
|
2134
2211
|
}
|
|
2135
2212
|
}
|
|
2136
2213
|
if (stubs.length > 0) {
|
|
2137
|
-
console.log(
|
|
2214
|
+
console.log(import_chalk7.default.bold("\nTest stubs to create:"));
|
|
2138
2215
|
for (const s of stubs) {
|
|
2139
|
-
console.log(` ${
|
|
2216
|
+
console.log(` ${import_chalk7.default.green("+")} ${s.path}`);
|
|
2140
2217
|
}
|
|
2141
2218
|
}
|
|
2142
2219
|
}
|
|
@@ -2170,8 +2247,62 @@ function computeNewSpecifier(oldSpecifier, newBare) {
|
|
|
2170
2247
|
const newSpec = prefix + newBare;
|
|
2171
2248
|
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
2172
2249
|
}
|
|
2173
|
-
async function
|
|
2250
|
+
async function scanForAliasImports(renames, projectRoot) {
|
|
2174
2251
|
if (renames.length === 0) return [];
|
|
2252
|
+
const { readFile, readdir } = await import("fs/promises");
|
|
2253
|
+
const oldBareNames = /* @__PURE__ */ new Set();
|
|
2254
|
+
for (const r of renames) {
|
|
2255
|
+
const oldFilename = path10.basename(r.oldPath);
|
|
2256
|
+
oldBareNames.add(oldFilename.slice(0, oldFilename.indexOf(".")));
|
|
2257
|
+
}
|
|
2258
|
+
const importPattern = /(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
2259
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
2260
|
+
"node_modules",
|
|
2261
|
+
"dist",
|
|
2262
|
+
"build",
|
|
2263
|
+
".next",
|
|
2264
|
+
".expo",
|
|
2265
|
+
".turbo",
|
|
2266
|
+
"coverage"
|
|
2267
|
+
]);
|
|
2268
|
+
const sourceExts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2269
|
+
const allEntries = await readdir(projectRoot, { recursive: true, withFileTypes: true });
|
|
2270
|
+
const files = allEntries.filter((e) => {
|
|
2271
|
+
if (!e.isFile()) return false;
|
|
2272
|
+
const ext = path10.extname(e.name);
|
|
2273
|
+
if (!sourceExts.has(ext)) return false;
|
|
2274
|
+
const rel = path10.join(e.parentPath, e.name);
|
|
2275
|
+
const segments = path10.relative(projectRoot, rel).split(path10.sep);
|
|
2276
|
+
return !segments.some((s) => skipDirs.has(s));
|
|
2277
|
+
}).map((e) => path10.join(e.parentPath, e.name));
|
|
2278
|
+
const aliases = [];
|
|
2279
|
+
for (const file of files) {
|
|
2280
|
+
let content;
|
|
2281
|
+
try {
|
|
2282
|
+
content = await readFile(file, "utf-8");
|
|
2283
|
+
} catch {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
const lines = content.split("\n");
|
|
2287
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2288
|
+
const line = lines[i];
|
|
2289
|
+
importPattern.lastIndex = 0;
|
|
2290
|
+
for (let match = importPattern.exec(line); match !== null; match = importPattern.exec(line)) {
|
|
2291
|
+
const specifier = match[1] ?? match[2];
|
|
2292
|
+
if (!specifier || specifier.startsWith(".")) continue;
|
|
2293
|
+
if (!specifier.includes("/")) continue;
|
|
2294
|
+
const lastSegment = specifier.split("/").pop() ?? "";
|
|
2295
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2296
|
+
if (oldBareNames.has(bare)) {
|
|
2297
|
+
aliases.push({ file, specifier, line: i + 1 });
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return aliases;
|
|
2303
|
+
}
|
|
2304
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
2305
|
+
if (renames.length === 0) return { updates: [], skippedAliases: [] };
|
|
2175
2306
|
const { Project, SyntaxKind } = await import("ts-morph");
|
|
2176
2307
|
const renameMap = /* @__PURE__ */ new Map();
|
|
2177
2308
|
for (const r of renames) {
|
|
@@ -2186,15 +2317,44 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
2186
2317
|
});
|
|
2187
2318
|
project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
2188
2319
|
const updates = [];
|
|
2320
|
+
const skippedAliases = [];
|
|
2189
2321
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2322
|
+
const oldBareNames = /* @__PURE__ */ new Set();
|
|
2323
|
+
for (const r of renames) {
|
|
2324
|
+
const oldFilename = path10.basename(r.oldPath);
|
|
2325
|
+
oldBareNames.add(oldFilename.slice(0, oldFilename.indexOf(".")));
|
|
2326
|
+
}
|
|
2190
2327
|
for (const sourceFile of project.getSourceFiles()) {
|
|
2191
2328
|
const filePath = sourceFile.getFilePath();
|
|
2192
2329
|
const segments = filePath.split(path10.sep);
|
|
2193
|
-
|
|
2330
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
2331
|
+
"node_modules",
|
|
2332
|
+
"dist",
|
|
2333
|
+
"build",
|
|
2334
|
+
".next",
|
|
2335
|
+
".expo",
|
|
2336
|
+
".svelte-kit",
|
|
2337
|
+
".turbo",
|
|
2338
|
+
"coverage"
|
|
2339
|
+
]);
|
|
2340
|
+
if (segments.some((s) => skipDirs.has(s))) continue;
|
|
2194
2341
|
const fileDir = path10.dirname(filePath);
|
|
2195
2342
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
2196
2343
|
const specifier = decl.getModuleSpecifierValue();
|
|
2197
|
-
if (!specifier.startsWith("."))
|
|
2344
|
+
if (!specifier.startsWith(".")) {
|
|
2345
|
+
if (specifier.includes("/")) {
|
|
2346
|
+
const lastSegment = specifier.split("/").pop() ?? "";
|
|
2347
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2348
|
+
if (oldBareNames.has(bare)) {
|
|
2349
|
+
skippedAliases.push({
|
|
2350
|
+
file: filePath,
|
|
2351
|
+
specifier,
|
|
2352
|
+
line: decl.getStartLineNumber()
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2198
2358
|
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
2199
2359
|
if (!match) continue;
|
|
2200
2360
|
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
@@ -2244,7 +2404,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
2244
2404
|
if (updates.length > 0) {
|
|
2245
2405
|
await project.save();
|
|
2246
2406
|
}
|
|
2247
|
-
return updates;
|
|
2407
|
+
return { updates, skippedAliases };
|
|
2248
2408
|
}
|
|
2249
2409
|
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
2250
2410
|
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
@@ -2347,10 +2507,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2347
2507
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2348
2508
|
const testPattern = pkg?.structure?.testPattern;
|
|
2349
2509
|
if (!testPattern) return null;
|
|
2350
|
-
const
|
|
2351
|
-
const ext = path12.extname(
|
|
2510
|
+
const basename9 = path12.basename(sourceRelPath);
|
|
2511
|
+
const ext = path12.extname(basename9);
|
|
2352
2512
|
if (!ext) return null;
|
|
2353
|
-
const stem =
|
|
2513
|
+
const stem = basename9.slice(0, -ext.length);
|
|
2354
2514
|
const testSuffix = testPattern.replace("*", "");
|
|
2355
2515
|
const testFilename = `${stem}${testSuffix}`;
|
|
2356
2516
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2381,13 +2541,13 @@ async function fixCommand(options, cwd) {
|
|
|
2381
2541
|
const startDir = cwd ?? process.cwd();
|
|
2382
2542
|
const projectRoot = findProjectRoot(startDir);
|
|
2383
2543
|
if (!projectRoot) {
|
|
2384
|
-
console.error(`${
|
|
2544
|
+
console.error(`${import_chalk8.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
2385
2545
|
return 1;
|
|
2386
2546
|
}
|
|
2387
2547
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
2388
2548
|
if (!fs13.existsSync(configPath)) {
|
|
2389
2549
|
console.error(
|
|
2390
|
-
`${
|
|
2550
|
+
`${import_chalk8.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
2391
2551
|
);
|
|
2392
2552
|
return 1;
|
|
2393
2553
|
}
|
|
@@ -2396,7 +2556,7 @@ async function fixCommand(options, cwd) {
|
|
|
2396
2556
|
const isDirty = checkGitDirty(projectRoot);
|
|
2397
2557
|
if (isDirty) {
|
|
2398
2558
|
console.log(
|
|
2399
|
-
|
|
2559
|
+
import_chalk8.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
2400
2560
|
);
|
|
2401
2561
|
}
|
|
2402
2562
|
}
|
|
@@ -2425,13 +2585,59 @@ async function fixCommand(options, cwd) {
|
|
|
2425
2585
|
if (stub) testStubs.push(stub);
|
|
2426
2586
|
}
|
|
2427
2587
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2588
|
+
const aliasImports = await scanForAliasImports(dedupedRenames, projectRoot);
|
|
2589
|
+
const blockedOldBareNames = /* @__PURE__ */ new Set();
|
|
2590
|
+
for (const alias of aliasImports) {
|
|
2591
|
+
const lastSegment = alias.specifier.split("/").pop() ?? "";
|
|
2592
|
+
const bare = lastSegment.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2593
|
+
blockedOldBareNames.add(bare);
|
|
2594
|
+
}
|
|
2595
|
+
const safeRenames = dedupedRenames.filter((r) => {
|
|
2596
|
+
const oldFilename = path13.basename(r.oldPath);
|
|
2597
|
+
const bare = oldFilename.slice(0, oldFilename.indexOf("."));
|
|
2598
|
+
return !blockedOldBareNames.has(bare);
|
|
2599
|
+
});
|
|
2600
|
+
const skippedRenames = dedupedRenames.filter((r) => {
|
|
2601
|
+
const oldFilename = path13.basename(r.oldPath);
|
|
2602
|
+
const bare = oldFilename.slice(0, oldFilename.indexOf("."));
|
|
2603
|
+
return blockedOldBareNames.has(bare);
|
|
2604
|
+
});
|
|
2605
|
+
if (safeRenames.length === 0 && testStubs.length === 0 && skippedRenames.length === 0) {
|
|
2606
|
+
console.log(`${import_chalk8.default.green("\u2713")} No fixable violations found.`);
|
|
2607
|
+
return 0;
|
|
2608
|
+
}
|
|
2609
|
+
printPlan(safeRenames, testStubs);
|
|
2610
|
+
if (skippedRenames.length > 0) {
|
|
2611
|
+
console.log("");
|
|
2612
|
+
console.log(
|
|
2613
|
+
import_chalk8.default.yellow(
|
|
2614
|
+
`Skipping ${skippedRenames.length} rename${skippedRenames.length > 1 ? "s" : ""} \u2014 aliased imports would break:`
|
|
2615
|
+
)
|
|
2616
|
+
);
|
|
2617
|
+
for (const r of skippedRenames.slice(0, 5)) {
|
|
2618
|
+
console.log(import_chalk8.default.dim(` ${r.oldPath} \u2192 ${r.newPath}`));
|
|
2619
|
+
}
|
|
2620
|
+
if (skippedRenames.length > 5) {
|
|
2621
|
+
console.log(import_chalk8.default.dim(` ... and ${skippedRenames.length - 5} more`));
|
|
2622
|
+
}
|
|
2623
|
+
console.log("");
|
|
2624
|
+
console.log(import_chalk8.default.yellow("Affected aliased imports:"));
|
|
2625
|
+
for (const alias of aliasImports.slice(0, 5)) {
|
|
2626
|
+
const relFile = path13.relative(projectRoot, alias.file);
|
|
2627
|
+
console.log(import_chalk8.default.dim(` ${relFile}:${alias.line} \u2014 ${alias.specifier}`));
|
|
2628
|
+
}
|
|
2629
|
+
if (aliasImports.length > 5) {
|
|
2630
|
+
console.log(import_chalk8.default.dim(` ... and ${aliasImports.length - 5} more`));
|
|
2631
|
+
}
|
|
2632
|
+
console.log(import_chalk8.default.dim(" Update these imports to relative paths first, then re-run fix."));
|
|
2633
|
+
}
|
|
2634
|
+
if (safeRenames.length === 0 && testStubs.length === 0) {
|
|
2635
|
+
console.log(`
|
|
2636
|
+
${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
2430
2637
|
return 0;
|
|
2431
2638
|
}
|
|
2432
|
-
printPlan(dedupedRenames, testStubs);
|
|
2433
2639
|
if (options.dryRun) {
|
|
2434
|
-
console.log(
|
|
2640
|
+
console.log(import_chalk8.default.dim("\nDry run \u2014 no changes applied."));
|
|
2435
2641
|
return 0;
|
|
2436
2642
|
}
|
|
2437
2643
|
if (!options.yes) {
|
|
@@ -2442,15 +2648,15 @@ async function fixCommand(options, cwd) {
|
|
|
2442
2648
|
}
|
|
2443
2649
|
}
|
|
2444
2650
|
let renameCount = 0;
|
|
2445
|
-
for (const rename of
|
|
2651
|
+
for (const rename of safeRenames) {
|
|
2446
2652
|
if (executeRename(rename)) {
|
|
2447
2653
|
renameCount++;
|
|
2448
2654
|
}
|
|
2449
2655
|
}
|
|
2450
2656
|
let importUpdateCount = 0;
|
|
2451
2657
|
if (renameCount > 0) {
|
|
2452
|
-
const appliedRenames =
|
|
2453
|
-
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
2658
|
+
const appliedRenames = safeRenames.filter((r) => fs13.existsSync(r.newAbsPath));
|
|
2659
|
+
const { updates } = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
2454
2660
|
importUpdateCount = updates.length;
|
|
2455
2661
|
}
|
|
2456
2662
|
let stubCount = 0;
|
|
@@ -2462,33 +2668,33 @@ async function fixCommand(options, cwd) {
|
|
|
2462
2668
|
}
|
|
2463
2669
|
console.log("");
|
|
2464
2670
|
if (renameCount > 0) {
|
|
2465
|
-
console.log(`${
|
|
2671
|
+
console.log(`${import_chalk8.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
2466
2672
|
}
|
|
2467
2673
|
if (importUpdateCount > 0) {
|
|
2468
2674
|
console.log(
|
|
2469
|
-
`${
|
|
2675
|
+
`${import_chalk8.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
2470
2676
|
);
|
|
2471
2677
|
}
|
|
2472
2678
|
if (stubCount > 0) {
|
|
2473
|
-
console.log(`${
|
|
2679
|
+
console.log(`${import_chalk8.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
2474
2680
|
}
|
|
2475
2681
|
return 0;
|
|
2476
2682
|
}
|
|
2477
2683
|
|
|
2478
2684
|
// src/commands/init.ts
|
|
2479
|
-
var
|
|
2480
|
-
var
|
|
2685
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
2686
|
+
var path19 = __toESM(require("path"), 1);
|
|
2481
2687
|
var clack8 = __toESM(require("@clack/prompts"), 1);
|
|
2482
2688
|
var import_config8 = require("@viberails/config");
|
|
2483
2689
|
var import_scanner2 = require("@viberails/scanner");
|
|
2484
|
-
var
|
|
2690
|
+
var import_chalk12 = __toESM(require("chalk"), 1);
|
|
2485
2691
|
|
|
2486
2692
|
// src/utils/check-prerequisites.ts
|
|
2487
2693
|
var import_node_child_process5 = require("child_process");
|
|
2488
2694
|
var fs14 = __toESM(require("fs"), 1);
|
|
2489
2695
|
var path14 = __toESM(require("path"), 1);
|
|
2490
2696
|
var clack7 = __toESM(require("@clack/prompts"), 1);
|
|
2491
|
-
var
|
|
2697
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
2492
2698
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2493
2699
|
const pm = scanResult.stack.packageManager.name;
|
|
2494
2700
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -2519,9 +2725,9 @@ function displayMissingPrereqs(prereqs) {
|
|
|
2519
2725
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2520
2726
|
for (const m of missing) {
|
|
2521
2727
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2522
|
-
console.log(` ${
|
|
2728
|
+
console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2523
2729
|
if (m.installCommand) {
|
|
2524
|
-
console.log(` Install: ${
|
|
2730
|
+
console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
|
|
2525
2731
|
}
|
|
2526
2732
|
}
|
|
2527
2733
|
}
|
|
@@ -2632,46 +2838,85 @@ function updateGitignore(projectRoot) {
|
|
|
2632
2838
|
}
|
|
2633
2839
|
|
|
2634
2840
|
// src/commands/init-hooks.ts
|
|
2841
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
2842
|
+
var path17 = __toESM(require("path"), 1);
|
|
2843
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
2844
|
+
var import_yaml = require("yaml");
|
|
2845
|
+
|
|
2846
|
+
// src/commands/resolve-typecheck.ts
|
|
2635
2847
|
var fs16 = __toESM(require("fs"), 1);
|
|
2636
2848
|
var path16 = __toESM(require("path"), 1);
|
|
2637
|
-
|
|
2638
|
-
|
|
2849
|
+
function hasTurboTask(projectRoot, taskName) {
|
|
2850
|
+
const turboPath = path16.join(projectRoot, "turbo.json");
|
|
2851
|
+
if (!fs16.existsSync(turboPath)) return false;
|
|
2852
|
+
try {
|
|
2853
|
+
const turbo = JSON.parse(fs16.readFileSync(turboPath, "utf-8"));
|
|
2854
|
+
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
2855
|
+
return taskName in tasks;
|
|
2856
|
+
} catch {
|
|
2857
|
+
return false;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
2861
|
+
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
2862
|
+
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
2863
|
+
}
|
|
2864
|
+
const pkgJsonPath = path16.join(projectRoot, "package.json");
|
|
2865
|
+
if (fs16.existsSync(pkgJsonPath)) {
|
|
2866
|
+
try {
|
|
2867
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
|
|
2868
|
+
if (pkg.scripts?.typecheck) {
|
|
2869
|
+
const pm = packageManager ?? "npm";
|
|
2870
|
+
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
2871
|
+
}
|
|
2872
|
+
} catch {
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (fs16.existsSync(path16.join(projectRoot, "tsconfig.json"))) {
|
|
2876
|
+
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
reason: "no root tsconfig.json, no typecheck script, and no turbo typecheck task found"
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/commands/init-hooks.ts
|
|
2639
2884
|
function setupPreCommitHook(projectRoot) {
|
|
2640
|
-
const lefthookPath =
|
|
2641
|
-
if (
|
|
2885
|
+
const lefthookPath = path17.join(projectRoot, "lefthook.yml");
|
|
2886
|
+
if (fs17.existsSync(lefthookPath)) {
|
|
2642
2887
|
addLefthookPreCommit(lefthookPath);
|
|
2643
|
-
console.log(` ${
|
|
2888
|
+
console.log(` ${import_chalk10.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2644
2889
|
return "lefthook.yml";
|
|
2645
2890
|
}
|
|
2646
|
-
const huskyDir =
|
|
2647
|
-
if (
|
|
2891
|
+
const huskyDir = path17.join(projectRoot, ".husky");
|
|
2892
|
+
if (fs17.existsSync(huskyDir)) {
|
|
2648
2893
|
writeHuskyPreCommit(huskyDir);
|
|
2649
|
-
console.log(` ${
|
|
2894
|
+
console.log(` ${import_chalk10.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2650
2895
|
return ".husky/pre-commit";
|
|
2651
2896
|
}
|
|
2652
|
-
const gitDir =
|
|
2653
|
-
if (
|
|
2654
|
-
const hooksDir =
|
|
2655
|
-
if (!
|
|
2656
|
-
|
|
2897
|
+
const gitDir = path17.join(projectRoot, ".git");
|
|
2898
|
+
if (fs17.existsSync(gitDir)) {
|
|
2899
|
+
const hooksDir = path17.join(gitDir, "hooks");
|
|
2900
|
+
if (!fs17.existsSync(hooksDir)) {
|
|
2901
|
+
fs17.mkdirSync(hooksDir, { recursive: true });
|
|
2657
2902
|
}
|
|
2658
2903
|
writeGitHookPreCommit(hooksDir);
|
|
2659
|
-
console.log(` ${
|
|
2904
|
+
console.log(` ${import_chalk10.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
2660
2905
|
return ".git/hooks/pre-commit";
|
|
2661
2906
|
}
|
|
2662
2907
|
return void 0;
|
|
2663
2908
|
}
|
|
2664
2909
|
function writeGitHookPreCommit(hooksDir) {
|
|
2665
|
-
const hookPath =
|
|
2666
|
-
if (
|
|
2667
|
-
const existing =
|
|
2910
|
+
const hookPath = path17.join(hooksDir, "pre-commit");
|
|
2911
|
+
if (fs17.existsSync(hookPath)) {
|
|
2912
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2668
2913
|
if (existing.includes("viberails")) return;
|
|
2669
|
-
|
|
2914
|
+
fs17.writeFileSync(
|
|
2670
2915
|
hookPath,
|
|
2671
2916
|
`${existing.trimEnd()}
|
|
2672
2917
|
|
|
2673
2918
|
# viberails check
|
|
2674
|
-
npx viberails check --staged
|
|
2919
|
+
if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi
|
|
2675
2920
|
`
|
|
2676
2921
|
);
|
|
2677
2922
|
return;
|
|
@@ -2680,13 +2925,13 @@ npx viberails check --staged
|
|
|
2680
2925
|
"#!/bin/sh",
|
|
2681
2926
|
"# Generated by viberails \u2014 https://viberails.sh",
|
|
2682
2927
|
"",
|
|
2683
|
-
"npx viberails check --staged",
|
|
2928
|
+
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
2684
2929
|
""
|
|
2685
2930
|
].join("\n");
|
|
2686
|
-
|
|
2931
|
+
fs17.writeFileSync(hookPath, script, { mode: 493 });
|
|
2687
2932
|
}
|
|
2688
2933
|
function addLefthookPreCommit(lefthookPath) {
|
|
2689
|
-
const content =
|
|
2934
|
+
const content = fs17.readFileSync(lefthookPath, "utf-8");
|
|
2690
2935
|
if (content.includes("viberails")) return;
|
|
2691
2936
|
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
2692
2937
|
if (!doc["pre-commit"]) {
|
|
@@ -2696,30 +2941,30 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
2696
2941
|
doc["pre-commit"].commands = {};
|
|
2697
2942
|
}
|
|
2698
2943
|
doc["pre-commit"].commands.viberails = {
|
|
2699
|
-
run: "npx viberails check --staged"
|
|
2944
|
+
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
2700
2945
|
};
|
|
2701
|
-
|
|
2946
|
+
fs17.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
2702
2947
|
}
|
|
2703
2948
|
function detectHookManager(projectRoot) {
|
|
2704
|
-
if (
|
|
2705
|
-
if (
|
|
2949
|
+
if (fs17.existsSync(path17.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
2950
|
+
if (fs17.existsSync(path17.join(projectRoot, ".husky"))) return "Husky";
|
|
2706
2951
|
return void 0;
|
|
2707
2952
|
}
|
|
2708
2953
|
function setupClaudeCodeHook(projectRoot) {
|
|
2709
|
-
const claudeDir =
|
|
2710
|
-
if (!
|
|
2711
|
-
|
|
2954
|
+
const claudeDir = path17.join(projectRoot, ".claude");
|
|
2955
|
+
if (!fs17.existsSync(claudeDir)) {
|
|
2956
|
+
fs17.mkdirSync(claudeDir, { recursive: true });
|
|
2712
2957
|
}
|
|
2713
|
-
const settingsPath =
|
|
2958
|
+
const settingsPath = path17.join(claudeDir, "settings.json");
|
|
2714
2959
|
let settings = {};
|
|
2715
|
-
if (
|
|
2960
|
+
if (fs17.existsSync(settingsPath)) {
|
|
2716
2961
|
try {
|
|
2717
|
-
settings = JSON.parse(
|
|
2962
|
+
settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
|
|
2718
2963
|
} catch {
|
|
2719
2964
|
console.warn(
|
|
2720
|
-
` ${
|
|
2965
|
+
` ${import_chalk10.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
2721
2966
|
);
|
|
2722
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2967
|
+
console.warn(` Fix the JSON manually, then re-run ${import_chalk10.default.cyan("viberails init --force")}`);
|
|
2723
2968
|
return;
|
|
2724
2969
|
}
|
|
2725
2970
|
}
|
|
@@ -2740,30 +2985,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2740
2985
|
}
|
|
2741
2986
|
];
|
|
2742
2987
|
settings.hooks = hooks;
|
|
2743
|
-
|
|
2988
|
+
fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2744
2989
|
`);
|
|
2745
|
-
console.log(` ${
|
|
2990
|
+
console.log(` ${import_chalk10.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
2746
2991
|
}
|
|
2747
2992
|
function setupClaudeMdReference(projectRoot) {
|
|
2748
|
-
const claudeMdPath =
|
|
2993
|
+
const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
|
|
2749
2994
|
let content = "";
|
|
2750
|
-
if (
|
|
2751
|
-
content =
|
|
2995
|
+
if (fs17.existsSync(claudeMdPath)) {
|
|
2996
|
+
content = fs17.readFileSync(claudeMdPath, "utf-8");
|
|
2752
2997
|
}
|
|
2753
2998
|
if (content.includes("@.viberails/context.md")) return;
|
|
2754
2999
|
const ref = "\n@.viberails/context.md\n";
|
|
2755
3000
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
2756
|
-
|
|
2757
|
-
console.log(` ${
|
|
3001
|
+
fs17.writeFileSync(claudeMdPath, prefix + ref);
|
|
3002
|
+
console.log(` ${import_chalk10.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2758
3003
|
}
|
|
2759
3004
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
2760
|
-
const workflowDir =
|
|
2761
|
-
const workflowPath =
|
|
2762
|
-
if (
|
|
2763
|
-
const existing =
|
|
3005
|
+
const workflowDir = path17.join(projectRoot, ".github", "workflows");
|
|
3006
|
+
const workflowPath = path17.join(workflowDir, "viberails.yml");
|
|
3007
|
+
if (fs17.existsSync(workflowPath)) {
|
|
3008
|
+
const existing = fs17.readFileSync(workflowPath, "utf-8");
|
|
2764
3009
|
if (existing.includes("viberails")) return void 0;
|
|
2765
3010
|
}
|
|
2766
|
-
|
|
3011
|
+
fs17.mkdirSync(workflowDir, { recursive: true });
|
|
2767
3012
|
const pm = packageManager || "npm";
|
|
2768
3013
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
2769
3014
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -2772,7 +3017,7 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
2772
3017
|
"",
|
|
2773
3018
|
"on:",
|
|
2774
3019
|
" pull_request:",
|
|
2775
|
-
" branches: [main]",
|
|
3020
|
+
" branches: [main, master]",
|
|
2776
3021
|
"",
|
|
2777
3022
|
"jobs:",
|
|
2778
3023
|
" check:",
|
|
@@ -2790,87 +3035,94 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
2790
3035
|
" - uses: actions/setup-node@v4",
|
|
2791
3036
|
" with:",
|
|
2792
3037
|
" node-version: 22",
|
|
2793
|
-
|
|
3038
|
+
` cache: ${pm}`,
|
|
2794
3039
|
"",
|
|
2795
3040
|
` - run: ${installCmd}`
|
|
2796
3041
|
);
|
|
2797
3042
|
if (options?.typecheck) {
|
|
2798
|
-
|
|
3043
|
+
const resolved = resolveTypecheckCommand(projectRoot, pm);
|
|
3044
|
+
if (resolved.command) {
|
|
3045
|
+
const ciCmd = resolved.command.startsWith("npx ") ? `${runPrefix} ${resolved.command.slice(4)}` : resolved.command;
|
|
3046
|
+
lines.push(` - run: ${ciCmd}`);
|
|
3047
|
+
}
|
|
2799
3048
|
}
|
|
2800
3049
|
if (options?.linter) {
|
|
2801
3050
|
const lintCmd = options.linter === "biome" ? "biome check ." : "eslint .";
|
|
2802
3051
|
lines.push(` - run: ${runPrefix} ${lintCmd}`);
|
|
2803
3052
|
}
|
|
2804
3053
|
lines.push(
|
|
2805
|
-
` - run:
|
|
3054
|
+
` - run: npx viberails check --enforce --diff-base origin/\${{ github.event.pull_request.base.ref }}`,
|
|
2806
3055
|
""
|
|
2807
3056
|
);
|
|
2808
3057
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
2809
|
-
|
|
3058
|
+
fs17.writeFileSync(workflowPath, content);
|
|
2810
3059
|
return ".github/workflows/viberails.yml";
|
|
2811
3060
|
}
|
|
2812
3061
|
function writeHuskyPreCommit(huskyDir) {
|
|
2813
|
-
const hookPath =
|
|
2814
|
-
if
|
|
2815
|
-
|
|
3062
|
+
const hookPath = path17.join(huskyDir, "pre-commit");
|
|
3063
|
+
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3064
|
+
if (fs17.existsSync(hookPath)) {
|
|
3065
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2816
3066
|
if (!existing.includes("viberails")) {
|
|
2817
|
-
|
|
2818
|
-
|
|
3067
|
+
fs17.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3068
|
+
${cmd}
|
|
2819
3069
|
`);
|
|
2820
3070
|
}
|
|
2821
3071
|
return;
|
|
2822
3072
|
}
|
|
2823
|
-
|
|
3073
|
+
fs17.writeFileSync(hookPath, `#!/bin/sh
|
|
3074
|
+
${cmd}
|
|
3075
|
+
`, { mode: 493 });
|
|
2824
3076
|
}
|
|
2825
3077
|
|
|
2826
3078
|
// src/commands/init-hooks-extra.ts
|
|
2827
|
-
var
|
|
2828
|
-
var
|
|
2829
|
-
var
|
|
3079
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
3080
|
+
var path18 = __toESM(require("path"), 1);
|
|
3081
|
+
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
2830
3082
|
var import_yaml2 = require("yaml");
|
|
2831
3083
|
function addPreCommitStep(projectRoot, name, command, marker) {
|
|
2832
|
-
const lefthookPath =
|
|
2833
|
-
if (
|
|
2834
|
-
const content =
|
|
3084
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3085
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
3086
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
2835
3087
|
if (content.includes(marker)) return void 0;
|
|
2836
3088
|
const doc = (0, import_yaml2.parse)(content) ?? {};
|
|
2837
3089
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
2838
3090
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
2839
3091
|
doc["pre-commit"].commands[name] = { run: command };
|
|
2840
|
-
|
|
3092
|
+
fs18.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
|
|
2841
3093
|
return "lefthook.yml";
|
|
2842
3094
|
}
|
|
2843
|
-
const huskyDir =
|
|
2844
|
-
if (
|
|
2845
|
-
const hookPath =
|
|
2846
|
-
if (
|
|
2847
|
-
const existing =
|
|
3095
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3096
|
+
if (fs18.existsSync(huskyDir)) {
|
|
3097
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
3098
|
+
if (fs18.existsSync(hookPath)) {
|
|
3099
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2848
3100
|
if (existing.includes(marker)) return void 0;
|
|
2849
|
-
|
|
3101
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2850
3102
|
${command}
|
|
2851
3103
|
`);
|
|
2852
3104
|
} else {
|
|
2853
|
-
|
|
3105
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
2854
3106
|
${command}
|
|
2855
3107
|
`, { mode: 493 });
|
|
2856
3108
|
}
|
|
2857
3109
|
return ".husky/pre-commit";
|
|
2858
3110
|
}
|
|
2859
|
-
const gitDir =
|
|
2860
|
-
if (
|
|
2861
|
-
const hooksDir =
|
|
2862
|
-
if (!
|
|
2863
|
-
const hookPath =
|
|
2864
|
-
if (
|
|
2865
|
-
const existing =
|
|
3111
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3112
|
+
if (fs18.existsSync(gitDir)) {
|
|
3113
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3114
|
+
if (!fs18.existsSync(hooksDir)) fs18.mkdirSync(hooksDir, { recursive: true });
|
|
3115
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3116
|
+
if (fs18.existsSync(hookPath)) {
|
|
3117
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2866
3118
|
if (existing.includes(marker)) return void 0;
|
|
2867
|
-
|
|
3119
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2868
3120
|
|
|
2869
3121
|
# ${name}
|
|
2870
3122
|
${command}
|
|
2871
3123
|
`);
|
|
2872
3124
|
} else {
|
|
2873
|
-
|
|
3125
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
2874
3126
|
# Generated by viberails
|
|
2875
3127
|
|
|
2876
3128
|
# ${name}
|
|
@@ -2883,10 +3135,15 @@ ${command}
|
|
|
2883
3135
|
}
|
|
2884
3136
|
return void 0;
|
|
2885
3137
|
}
|
|
2886
|
-
function setupTypecheckHook(projectRoot) {
|
|
2887
|
-
const
|
|
3138
|
+
function setupTypecheckHook(projectRoot, packageManager) {
|
|
3139
|
+
const resolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3140
|
+
if (!resolved.command) {
|
|
3141
|
+
console.log(` ${import_chalk11.default.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
|
|
3142
|
+
return void 0;
|
|
3143
|
+
}
|
|
3144
|
+
const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
|
|
2888
3145
|
if (target) {
|
|
2889
|
-
console.log(` ${
|
|
3146
|
+
console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
|
|
2890
3147
|
}
|
|
2891
3148
|
return target;
|
|
2892
3149
|
}
|
|
@@ -2895,7 +3152,7 @@ function setupLintHook(projectRoot, linter) {
|
|
|
2895
3152
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
2896
3153
|
const target = addPreCommitStep(projectRoot, "lint", command, linter);
|
|
2897
3154
|
if (target) {
|
|
2898
|
-
console.log(` ${
|
|
3155
|
+
console.log(` ${import_chalk11.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
2899
3156
|
}
|
|
2900
3157
|
return target;
|
|
2901
3158
|
}
|
|
@@ -2906,7 +3163,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
2906
3163
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
2907
3164
|
}
|
|
2908
3165
|
if (integrations.typecheckHook) {
|
|
2909
|
-
const t = setupTypecheckHook(projectRoot);
|
|
3166
|
+
const t = setupTypecheckHook(projectRoot, opts.packageManager);
|
|
2910
3167
|
if (t) created.push(`${t} \u2014 added typecheck`);
|
|
2911
3168
|
}
|
|
2912
3169
|
if (integrations.lintHook && opts.linter) {
|
|
@@ -2943,11 +3200,11 @@ async function initCommand(options, cwd) {
|
|
|
2943
3200
|
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
2944
3201
|
);
|
|
2945
3202
|
}
|
|
2946
|
-
const configPath =
|
|
2947
|
-
if (
|
|
3203
|
+
const configPath = path19.join(projectRoot, CONFIG_FILE5);
|
|
3204
|
+
if (fs19.existsSync(configPath) && !options.force) {
|
|
2948
3205
|
console.log(
|
|
2949
|
-
`${
|
|
2950
|
-
Run ${
|
|
3206
|
+
`${import_chalk12.default.yellow("!")} viberails is already initialized.
|
|
3207
|
+
Run ${import_chalk12.default.cyan("viberails config")} to edit rules, ${import_chalk12.default.cyan("viberails sync")} to update, or ${import_chalk12.default.cyan("viberails init --force")} to start fresh.`
|
|
2951
3208
|
);
|
|
2952
3209
|
return;
|
|
2953
3210
|
}
|
|
@@ -2955,7 +3212,7 @@ async function initCommand(options, cwd) {
|
|
|
2955
3212
|
await initInteractive(projectRoot, configPath, options);
|
|
2956
3213
|
}
|
|
2957
3214
|
async function initNonInteractive(projectRoot, configPath) {
|
|
2958
|
-
console.log(
|
|
3215
|
+
console.log(import_chalk12.default.dim("Scanning project..."));
|
|
2959
3216
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
2960
3217
|
const config = (0, import_config8.generateConfig)(scanResult);
|
|
2961
3218
|
for (const pkg of config.packages) {
|
|
@@ -2968,11 +3225,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2968
3225
|
const exempted = getExemptedPackages(config);
|
|
2969
3226
|
if (exempted.length > 0) {
|
|
2970
3227
|
console.log(
|
|
2971
|
-
` ${
|
|
3228
|
+
` ${import_chalk12.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk12.default.dim("(types-only)")}`
|
|
2972
3229
|
);
|
|
2973
3230
|
}
|
|
2974
3231
|
if (config.packages.length > 1) {
|
|
2975
|
-
console.log(
|
|
3232
|
+
console.log(import_chalk12.default.dim("Building import graph..."));
|
|
2976
3233
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2977
3234
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2978
3235
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -2985,16 +3242,16 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2985
3242
|
}
|
|
2986
3243
|
}
|
|
2987
3244
|
const compacted = (0, import_config8.compactConfig)(config);
|
|
2988
|
-
|
|
3245
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2989
3246
|
`);
|
|
2990
3247
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2991
3248
|
updateGitignore(projectRoot);
|
|
2992
3249
|
setupClaudeCodeHook(projectRoot);
|
|
2993
3250
|
setupClaudeMdReference(projectRoot);
|
|
2994
3251
|
const rootPkg = config.packages[0];
|
|
2995
|
-
const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
|
|
3252
|
+
const rootPkgPm = rootPkg?.stack?.packageManager?.split("@")[0] ?? "npm";
|
|
2996
3253
|
const linter = rootPkg?.stack?.linter?.split("@")[0];
|
|
2997
|
-
const isTypeScript = rootPkg?.stack?.language === "typescript";
|
|
3254
|
+
const isTypeScript = rootPkg?.stack?.language?.split("@")[0] === "typescript";
|
|
2998
3255
|
const actionTarget = setupGithubAction(projectRoot, rootPkgPm, {
|
|
2999
3256
|
linter,
|
|
3000
3257
|
typecheck: isTypeScript
|
|
@@ -3002,17 +3259,17 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3002
3259
|
const hookManager = detectHookManager(projectRoot);
|
|
3003
3260
|
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
3004
3261
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3005
|
-
const ok =
|
|
3262
|
+
const ok = import_chalk12.default.green("\u2713");
|
|
3006
3263
|
const created = [
|
|
3007
|
-
`${ok} ${
|
|
3264
|
+
`${ok} ${path19.basename(configPath)}`,
|
|
3008
3265
|
`${ok} .viberails/context.md`,
|
|
3009
3266
|
`${ok} .viberails/scan-result.json`,
|
|
3010
3267
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
3011
3268
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
3012
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
3269
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk12.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
3013
3270
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
3014
3271
|
].filter(Boolean);
|
|
3015
|
-
if (hasHookManager &&
|
|
3272
|
+
if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
|
|
3016
3273
|
if (hasHookManager && linter) setupLintHook(projectRoot, linter);
|
|
3017
3274
|
console.log(`
|
|
3018
3275
|
Created:
|
|
@@ -3020,9 +3277,9 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
|
|
|
3020
3277
|
}
|
|
3021
3278
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3022
3279
|
clack8.intro("viberails");
|
|
3023
|
-
if (
|
|
3280
|
+
if (fs19.existsSync(configPath) && options.force) {
|
|
3024
3281
|
const replace = await confirmDangerous(
|
|
3025
|
-
`${
|
|
3282
|
+
`${path19.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3026
3283
|
);
|
|
3027
3284
|
if (!replace) {
|
|
3028
3285
|
clack8.outro("Aborted. No files were written.");
|
|
@@ -3034,13 +3291,6 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3034
3291
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
3035
3292
|
const config = (0, import_config8.generateConfig)(scanResult);
|
|
3036
3293
|
s.stop("Scan complete");
|
|
3037
|
-
const prereqResult = await promptMissingPrereqs(
|
|
3038
|
-
projectRoot,
|
|
3039
|
-
checkCoveragePrereqs(projectRoot, scanResult)
|
|
3040
|
-
);
|
|
3041
|
-
if (prereqResult.disableCoverage) {
|
|
3042
|
-
config.rules.testCoverage = 0;
|
|
3043
|
-
}
|
|
3044
3294
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3045
3295
|
clack8.log.warn(
|
|
3046
3296
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
@@ -3081,20 +3331,28 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3081
3331
|
if (denyCount > 0) {
|
|
3082
3332
|
config.boundaries = inferred;
|
|
3083
3333
|
config.rules.enforceBoundaries = true;
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
clack8.note(boundaryLines, "Boundary rules");
|
|
3334
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3335
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3087
3336
|
} else {
|
|
3088
3337
|
bs.stop("No boundary rules inferred");
|
|
3089
3338
|
}
|
|
3090
3339
|
}
|
|
3091
3340
|
}
|
|
3092
3341
|
const hookManager = detectHookManager(projectRoot);
|
|
3342
|
+
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3343
|
+
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3344
|
+
if (hasMissingPrereqs) {
|
|
3345
|
+
clack8.log.info("Some dependencies are needed for full functionality.");
|
|
3346
|
+
}
|
|
3347
|
+
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3348
|
+
if (prereqResult.disableCoverage) {
|
|
3349
|
+
config.rules.testCoverage = 0;
|
|
3350
|
+
}
|
|
3093
3351
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
3094
3352
|
const integrations = await promptIntegrations(projectRoot, hookManager, {
|
|
3095
|
-
isTypeScript: rootPkgStack?.language === "typescript",
|
|
3353
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3096
3354
|
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3097
|
-
packageManager: rootPkgStack?.packageManager,
|
|
3355
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3098
3356
|
isWorkspace: config.packages.length > 1
|
|
3099
3357
|
});
|
|
3100
3358
|
const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
|
|
@@ -3103,40 +3361,37 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3103
3361
|
return;
|
|
3104
3362
|
}
|
|
3105
3363
|
const compacted = (0, import_config8.compactConfig)(config);
|
|
3106
|
-
|
|
3364
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3107
3365
|
`);
|
|
3108
3366
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3109
3367
|
updateGitignore(projectRoot);
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
];
|
|
3119
|
-
clack8.log.success(`Created:
|
|
3120
|
-
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
3368
|
+
const ok = import_chalk12.default.green("\u2713");
|
|
3369
|
+
clack8.log.step(`${ok} ${path19.basename(configPath)}`);
|
|
3370
|
+
clack8.log.step(`${ok} .viberails/context.md`);
|
|
3371
|
+
clack8.log.step(`${ok} .viberails/scan-result.json`);
|
|
3372
|
+
setupSelectedIntegrations(projectRoot, integrations, {
|
|
3373
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3374
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0]
|
|
3375
|
+
});
|
|
3121
3376
|
clack8.outro(
|
|
3122
3377
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3123
|
-
${
|
|
3378
|
+
${import_chalk12.default.dim("Tip: use")} ${import_chalk12.default.cyan("viberails check --enforce")} ${import_chalk12.default.dim("in CI to block PRs on violations.")}`
|
|
3124
3379
|
);
|
|
3125
3380
|
}
|
|
3126
3381
|
|
|
3127
3382
|
// src/commands/sync.ts
|
|
3128
|
-
var
|
|
3129
|
-
var
|
|
3383
|
+
var fs20 = __toESM(require("fs"), 1);
|
|
3384
|
+
var path20 = __toESM(require("path"), 1);
|
|
3130
3385
|
var clack9 = __toESM(require("@clack/prompts"), 1);
|
|
3131
3386
|
var import_config9 = require("@viberails/config");
|
|
3132
3387
|
var import_scanner3 = require("@viberails/scanner");
|
|
3133
|
-
var
|
|
3388
|
+
var import_chalk13 = __toESM(require("chalk"), 1);
|
|
3134
3389
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3135
3390
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3136
3391
|
function loadPreviousStats(projectRoot) {
|
|
3137
|
-
const scanResultPath =
|
|
3392
|
+
const scanResultPath = path20.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3138
3393
|
try {
|
|
3139
|
-
const raw =
|
|
3394
|
+
const raw = fs20.readFileSync(scanResultPath, "utf-8");
|
|
3140
3395
|
const parsed = JSON.parse(raw);
|
|
3141
3396
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3142
3397
|
return parsed.statistics;
|
|
@@ -3153,15 +3408,15 @@ async function syncCommand(options, cwd) {
|
|
|
3153
3408
|
"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"
|
|
3154
3409
|
);
|
|
3155
3410
|
}
|
|
3156
|
-
const configPath =
|
|
3411
|
+
const configPath = path20.join(projectRoot, CONFIG_FILE6);
|
|
3157
3412
|
const existing = await (0, import_config9.loadConfig)(configPath);
|
|
3158
3413
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3159
|
-
console.log(
|
|
3414
|
+
console.log(import_chalk13.default.dim("Scanning project..."));
|
|
3160
3415
|
const scanResult = await (0, import_scanner3.scan)(projectRoot);
|
|
3161
3416
|
const merged = (0, import_config9.mergeConfig)(existing, scanResult);
|
|
3162
3417
|
const compacted = (0, import_config9.compactConfig)(merged);
|
|
3163
3418
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3164
|
-
const rawDisk =
|
|
3419
|
+
const rawDisk = fs20.readFileSync(configPath, "utf-8").trim();
|
|
3165
3420
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3166
3421
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3167
3422
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3169,13 +3424,13 @@ async function syncCommand(options, cwd) {
|
|
|
3169
3424
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3170
3425
|
if (changes.length > 0 || statsDelta) {
|
|
3171
3426
|
console.log(`
|
|
3172
|
-
${
|
|
3427
|
+
${import_chalk13.default.bold("Changes:")}`);
|
|
3173
3428
|
for (const change of changes) {
|
|
3174
|
-
const icon = change.type === "removed" ?
|
|
3429
|
+
const icon = change.type === "removed" ? import_chalk13.default.red("-") : import_chalk13.default.green("+");
|
|
3175
3430
|
console.log(` ${icon} ${change.description}`);
|
|
3176
3431
|
}
|
|
3177
3432
|
if (statsDelta) {
|
|
3178
|
-
console.log(` ${
|
|
3433
|
+
console.log(` ${import_chalk13.default.dim(statsDelta)}`);
|
|
3179
3434
|
}
|
|
3180
3435
|
}
|
|
3181
3436
|
if (options?.interactive) {
|
|
@@ -3208,7 +3463,7 @@ ${import_chalk12.default.bold("Changes:")}`);
|
|
|
3208
3463
|
});
|
|
3209
3464
|
applyRuleOverrides(merged, overrides);
|
|
3210
3465
|
const recompacted = (0, import_config9.compactConfig)(merged);
|
|
3211
|
-
|
|
3466
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3212
3467
|
`);
|
|
3213
3468
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3214
3469
|
clack9.log.success("Updated config with your customizations.");
|
|
@@ -3216,22 +3471,22 @@ ${import_chalk12.default.bold("Changes:")}`);
|
|
|
3216
3471
|
return;
|
|
3217
3472
|
}
|
|
3218
3473
|
}
|
|
3219
|
-
|
|
3474
|
+
fs20.writeFileSync(configPath, `${compactedJson}
|
|
3220
3475
|
`);
|
|
3221
3476
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3222
3477
|
console.log(`
|
|
3223
|
-
${
|
|
3478
|
+
${import_chalk13.default.bold("Synced:")}`);
|
|
3224
3479
|
if (configChanged) {
|
|
3225
|
-
console.log(` ${
|
|
3480
|
+
console.log(` ${import_chalk13.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3226
3481
|
} else {
|
|
3227
|
-
console.log(` ${
|
|
3482
|
+
console.log(` ${import_chalk13.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3228
3483
|
}
|
|
3229
|
-
console.log(` ${
|
|
3230
|
-
console.log(` ${
|
|
3484
|
+
console.log(` ${import_chalk13.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
3485
|
+
console.log(` ${import_chalk13.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
3231
3486
|
}
|
|
3232
3487
|
|
|
3233
3488
|
// src/index.ts
|
|
3234
|
-
var VERSION = "0.
|
|
3489
|
+
var VERSION = "0.6.0";
|
|
3235
3490
|
var program = new import_commander.Command();
|
|
3236
3491
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3237
3492
|
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) => {
|
|
@@ -3239,7 +3494,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3239
3494
|
await initCommand(options);
|
|
3240
3495
|
} catch (err) {
|
|
3241
3496
|
const message = err instanceof Error ? err.message : String(err);
|
|
3242
|
-
console.error(`${
|
|
3497
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3243
3498
|
process.exit(1);
|
|
3244
3499
|
}
|
|
3245
3500
|
});
|
|
@@ -3248,7 +3503,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3248
3503
|
await syncCommand(options);
|
|
3249
3504
|
} catch (err) {
|
|
3250
3505
|
const message = err instanceof Error ? err.message : String(err);
|
|
3251
|
-
console.error(`${
|
|
3506
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3252
3507
|
process.exit(1);
|
|
3253
3508
|
}
|
|
3254
3509
|
});
|
|
@@ -3257,7 +3512,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3257
3512
|
await configCommand(options);
|
|
3258
3513
|
} catch (err) {
|
|
3259
3514
|
const message = err instanceof Error ? err.message : String(err);
|
|
3260
|
-
console.error(`${
|
|
3515
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3261
3516
|
process.exit(1);
|
|
3262
3517
|
}
|
|
3263
3518
|
});
|
|
@@ -3278,7 +3533,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3278
3533
|
process.exit(exitCode);
|
|
3279
3534
|
} catch (err) {
|
|
3280
3535
|
const message = err instanceof Error ? err.message : String(err);
|
|
3281
|
-
console.error(`${
|
|
3536
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3282
3537
|
process.exit(1);
|
|
3283
3538
|
}
|
|
3284
3539
|
}
|
|
@@ -3289,7 +3544,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3289
3544
|
process.exit(exitCode);
|
|
3290
3545
|
} catch (err) {
|
|
3291
3546
|
const message = err instanceof Error ? err.message : String(err);
|
|
3292
|
-
console.error(`${
|
|
3547
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3293
3548
|
process.exit(1);
|
|
3294
3549
|
}
|
|
3295
3550
|
});
|
|
@@ -3298,7 +3553,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3298
3553
|
await boundariesCommand(options);
|
|
3299
3554
|
} catch (err) {
|
|
3300
3555
|
const message = err instanceof Error ? err.message : String(err);
|
|
3301
|
-
console.error(`${
|
|
3556
|
+
console.error(`${import_chalk14.default.red("Error:")} ${message}`);
|
|
3302
3557
|
process.exit(1);
|
|
3303
3558
|
}
|
|
3304
3559
|
});
|