viberails 0.5.0 → 0.5.1
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 +1166 -955
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1168 -957
- 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_chalk13 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -64,14 +64,66 @@ function findProjectRoot(startDir) {
|
|
|
64
64
|
var clack5 = __toESM(require("@clack/prompts"), 1);
|
|
65
65
|
|
|
66
66
|
// src/utils/prompt-integrations.ts
|
|
67
|
+
var import_node_child_process = require("child_process");
|
|
67
68
|
var clack = __toESM(require("@clack/prompts"), 1);
|
|
68
|
-
async function
|
|
69
|
-
const
|
|
69
|
+
async function promptHookManagerInstall(projectRoot, packageManager) {
|
|
70
|
+
const choice = await clack.select({
|
|
71
|
+
message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
value: "install",
|
|
75
|
+
label: "Yes, install Lefthook",
|
|
76
|
+
hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: "skip",
|
|
80
|
+
label: "No, skip",
|
|
81
|
+
hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
assertNotCancelled(choice);
|
|
86
|
+
if (choice !== "install") return void 0;
|
|
87
|
+
const pm = packageManager || "npm";
|
|
88
|
+
const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? "pnpm add -D lefthook" : "npm install -D lefthook";
|
|
89
|
+
const s = clack.spinner();
|
|
90
|
+
s.start("Installing Lefthook...");
|
|
91
|
+
const result = (0, import_node_child_process.spawnSync)(installCmd, {
|
|
92
|
+
cwd: projectRoot,
|
|
93
|
+
shell: true,
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
stdio: "pipe"
|
|
96
|
+
});
|
|
97
|
+
if (result.status === 0) {
|
|
98
|
+
const fs20 = await import("fs");
|
|
99
|
+
const path20 = await import("path");
|
|
100
|
+
const lefthookPath = path20.join(projectRoot, "lefthook.yml");
|
|
101
|
+
if (!fs20.existsSync(lefthookPath)) {
|
|
102
|
+
fs20.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
103
|
+
}
|
|
104
|
+
s.stop("Installed Lefthook");
|
|
105
|
+
return "Lefthook";
|
|
106
|
+
}
|
|
107
|
+
s.stop("Failed to install Lefthook");
|
|
108
|
+
clack.log.warn(`Install manually: ${installCmd}`);
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
112
|
+
let resolvedHookManager = hookManager;
|
|
113
|
+
if (!resolvedHookManager) {
|
|
114
|
+
resolvedHookManager = await promptHookManagerInstall(
|
|
115
|
+
projectRoot,
|
|
116
|
+
tools?.packageManager ?? "npm"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const isBareHook = !resolvedHookManager;
|
|
120
|
+
const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
|
|
121
|
+
const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
|
|
70
122
|
const options = [
|
|
71
123
|
{
|
|
72
124
|
value: "preCommit",
|
|
73
125
|
label: hookLabel,
|
|
74
|
-
hint:
|
|
126
|
+
hint: hookHint
|
|
75
127
|
}
|
|
76
128
|
];
|
|
77
129
|
if (tools?.isTypeScript) {
|
|
@@ -106,7 +158,7 @@ async function promptIntegrations(hookManager, tools) {
|
|
|
106
158
|
hint: "blocks PRs that fail viberails check"
|
|
107
159
|
}
|
|
108
160
|
);
|
|
109
|
-
const initialValues = options.map((o) => o.value);
|
|
161
|
+
const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
|
|
110
162
|
const result = await clack.multiselect({
|
|
111
163
|
message: "Set up integrations?",
|
|
112
164
|
options,
|
|
@@ -678,7 +730,7 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
|
|
|
678
730
|
// src/commands/check.ts
|
|
679
731
|
var fs7 = __toESM(require("fs"), 1);
|
|
680
732
|
var path7 = __toESM(require("path"), 1);
|
|
681
|
-
var
|
|
733
|
+
var import_config5 = require("@viberails/config");
|
|
682
734
|
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
683
735
|
|
|
684
736
|
// src/commands/check-config.ts
|
|
@@ -723,9 +775,10 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
723
775
|
}
|
|
724
776
|
|
|
725
777
|
// src/commands/check-coverage.ts
|
|
726
|
-
var
|
|
778
|
+
var import_node_child_process2 = require("child_process");
|
|
727
779
|
var fs4 = __toESM(require("fs"), 1);
|
|
728
780
|
var path4 = __toESM(require("path"), 1);
|
|
781
|
+
var import_config3 = require("@viberails/config");
|
|
729
782
|
var DEFAULT_SUMMARY_PATH = "coverage/coverage-summary.json";
|
|
730
783
|
function packageRoot(projectRoot, pkg) {
|
|
731
784
|
return pkg.path === "." ? projectRoot : path4.join(projectRoot, pkg.path);
|
|
@@ -765,7 +818,7 @@ function readCoveragePercentage(summaryPath) {
|
|
|
765
818
|
}
|
|
766
819
|
}
|
|
767
820
|
function runCoverageCommand(pkgRoot, command) {
|
|
768
|
-
const result = (0,
|
|
821
|
+
const result = (0, import_node_child_process2.spawnSync)(command, {
|
|
769
822
|
cwd: pkgRoot,
|
|
770
823
|
shell: true,
|
|
771
824
|
encoding: "utf-8",
|
|
@@ -806,13 +859,14 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
|
806
859
|
const violations = [];
|
|
807
860
|
for (const target of targets) {
|
|
808
861
|
if (target.rules.testCoverage <= 0) continue;
|
|
862
|
+
if (!target.pkg.stack?.testRunner) continue;
|
|
809
863
|
const pkgRoot = packageRoot(projectRoot, target.pkg);
|
|
810
864
|
const summaryPath = target.coverage.summaryPath ?? DEFAULT_SUMMARY_PATH;
|
|
811
865
|
const summaryAbs = path4.join(pkgRoot, summaryPath);
|
|
812
866
|
const summaryRel = violationFilePath(projectRoot, pkgRoot, summaryPath);
|
|
813
867
|
let pct = readCoveragePercentage(summaryAbs);
|
|
814
868
|
if (pct === void 0 && !options.staged) {
|
|
815
|
-
const command = target.coverage.command;
|
|
869
|
+
const command = target.coverage.command ?? (0, import_config3.inferCoverageCommand)(target.pkg.stack.testRunner);
|
|
816
870
|
if (!command) {
|
|
817
871
|
const pkgLabel = target.pkg.path === "." ? "root package" : target.pkg.path;
|
|
818
872
|
pushViolation(
|
|
@@ -823,6 +877,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
|
823
877
|
);
|
|
824
878
|
continue;
|
|
825
879
|
}
|
|
880
|
+
options.onProgress?.(target.pkg.path === "." ? "root" : target.pkg.path);
|
|
826
881
|
const run = runCoverageCommand(pkgRoot, command);
|
|
827
882
|
if (!run.ok) {
|
|
828
883
|
pushViolation(
|
|
@@ -857,10 +912,10 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
|
857
912
|
}
|
|
858
913
|
|
|
859
914
|
// src/commands/check-files.ts
|
|
860
|
-
var
|
|
915
|
+
var import_node_child_process3 = require("child_process");
|
|
861
916
|
var fs5 = __toESM(require("fs"), 1);
|
|
862
917
|
var path5 = __toESM(require("path"), 1);
|
|
863
|
-
var
|
|
918
|
+
var import_config4 = require("@viberails/config");
|
|
864
919
|
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
865
920
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
866
921
|
"node_modules",
|
|
@@ -930,7 +985,7 @@ function checkNaming(relPath, conventions) {
|
|
|
930
985
|
}
|
|
931
986
|
function getStagedFiles(projectRoot) {
|
|
932
987
|
try {
|
|
933
|
-
const output = (0,
|
|
988
|
+
const output = (0, import_node_child_process3.execSync)("git diff --cached --name-only --diff-filter=ACM", {
|
|
934
989
|
cwd: projectRoot,
|
|
935
990
|
encoding: "utf-8",
|
|
936
991
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -942,12 +997,12 @@ function getStagedFiles(projectRoot) {
|
|
|
942
997
|
}
|
|
943
998
|
function getDiffFiles(projectRoot, base) {
|
|
944
999
|
try {
|
|
945
|
-
const allOutput = (0,
|
|
1000
|
+
const allOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
|
|
946
1001
|
cwd: projectRoot,
|
|
947
1002
|
encoding: "utf-8",
|
|
948
1003
|
stdio: ["ignore", "pipe", "ignore"]
|
|
949
1004
|
});
|
|
950
|
-
const addedOutput = (0,
|
|
1005
|
+
const addedOutput = (0, import_node_child_process3.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
|
|
951
1006
|
cwd: projectRoot,
|
|
952
1007
|
encoding: "utf-8",
|
|
953
1008
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -961,7 +1016,7 @@ function getDiffFiles(projectRoot, base) {
|
|
|
961
1016
|
}
|
|
962
1017
|
}
|
|
963
1018
|
function getAllSourceFiles(projectRoot, config) {
|
|
964
|
-
const effectiveIgnore = [...
|
|
1019
|
+
const effectiveIgnore = [...import_config4.BUILTIN_IGNORE, ...config.ignore ?? []];
|
|
965
1020
|
const files = [];
|
|
966
1021
|
const walk = (dir) => {
|
|
967
1022
|
let entries;
|
|
@@ -1141,7 +1196,7 @@ async function checkCommand(options, cwd) {
|
|
|
1141
1196
|
);
|
|
1142
1197
|
return 1;
|
|
1143
1198
|
}
|
|
1144
|
-
const config = await (0,
|
|
1199
|
+
const config = await (0, import_config5.loadConfig)(configPath);
|
|
1145
1200
|
let filesToCheck;
|
|
1146
1201
|
let diffAddedFiles = null;
|
|
1147
1202
|
if (options.staged) {
|
|
@@ -1165,6 +1220,9 @@ async function checkCommand(options, cwd) {
|
|
|
1165
1220
|
}
|
|
1166
1221
|
const violations = [];
|
|
1167
1222
|
const severity = options.enforce ? "error" : "warn";
|
|
1223
|
+
const log7 = options.format !== "json" && !options.hook ? (msg) => process.stderr.write(import_chalk2.default.dim(msg)) : () => {
|
|
1224
|
+
};
|
|
1225
|
+
log7(" Checking files...");
|
|
1168
1226
|
for (const file of filesToCheck) {
|
|
1169
1227
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1170
1228
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1197,18 +1255,22 @@ async function checkCommand(options, cwd) {
|
|
|
1197
1255
|
}
|
|
1198
1256
|
}
|
|
1199
1257
|
}
|
|
1258
|
+
log7(" done\n");
|
|
1200
1259
|
if (!options.staged && !options.files) {
|
|
1260
|
+
log7(" Checking missing tests...");
|
|
1201
1261
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1262
|
+
violations.push(
|
|
1263
|
+
...diffAddedFiles ? testViolations.filter((v) => diffAddedFiles.has(v.file)) : testViolations
|
|
1264
|
+
);
|
|
1265
|
+
log7(" done\n");
|
|
1207
1266
|
}
|
|
1208
1267
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1268
|
+
log7(" Running test coverage...\n");
|
|
1209
1269
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1210
1270
|
staged: options.staged,
|
|
1211
|
-
enforce: options.enforce
|
|
1271
|
+
enforce: options.enforce,
|
|
1272
|
+
onProgress: (pkg) => log7(` Coverage: ${pkg}...
|
|
1273
|
+
`)
|
|
1212
1274
|
});
|
|
1213
1275
|
violations.push(...coverageViolations);
|
|
1214
1276
|
}
|
|
@@ -1232,10 +1294,8 @@ async function checkCommand(options, cwd) {
|
|
|
1232
1294
|
severity
|
|
1233
1295
|
});
|
|
1234
1296
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
1238
|
-
}
|
|
1297
|
+
log7(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1298
|
+
`);
|
|
1239
1299
|
}
|
|
1240
1300
|
if (options.format === "json") {
|
|
1241
1301
|
console.log(
|
|
@@ -1307,431 +1367,317 @@ async function hookCheckCommand(cwd) {
|
|
|
1307
1367
|
}
|
|
1308
1368
|
}
|
|
1309
1369
|
|
|
1310
|
-
// src/commands/
|
|
1311
|
-
var
|
|
1312
|
-
var
|
|
1313
|
-
var
|
|
1370
|
+
// src/commands/config.ts
|
|
1371
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
1372
|
+
var path9 = __toESM(require("path"), 1);
|
|
1373
|
+
var clack6 = __toESM(require("@clack/prompts"), 1);
|
|
1374
|
+
var import_config6 = require("@viberails/config");
|
|
1375
|
+
var import_scanner = require("@viberails/scanner");
|
|
1376
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1377
|
+
|
|
1378
|
+
// src/display-text.ts
|
|
1379
|
+
var import_types4 = require("@viberails/types");
|
|
1380
|
+
|
|
1381
|
+
// src/display.ts
|
|
1382
|
+
var import_types3 = require("@viberails/types");
|
|
1314
1383
|
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
1315
1384
|
|
|
1316
|
-
// src/
|
|
1317
|
-
var
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1385
|
+
// src/display-helpers.ts
|
|
1386
|
+
var import_types = require("@viberails/types");
|
|
1387
|
+
function groupByRole(directories) {
|
|
1388
|
+
const map = /* @__PURE__ */ new Map();
|
|
1389
|
+
for (const dir of directories) {
|
|
1390
|
+
if (dir.role === "unknown") continue;
|
|
1391
|
+
const existing = map.get(dir.role);
|
|
1392
|
+
if (existing) {
|
|
1393
|
+
existing.dirs.push(dir);
|
|
1394
|
+
} else {
|
|
1395
|
+
map.set(dir.role, { dirs: [dir] });
|
|
1324
1396
|
}
|
|
1325
1397
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1398
|
+
const groups = [];
|
|
1399
|
+
for (const [role, { dirs }] of map) {
|
|
1400
|
+
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
1401
|
+
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1402
|
+
groups.push({
|
|
1403
|
+
role,
|
|
1404
|
+
label,
|
|
1405
|
+
dirCount: dirs.length,
|
|
1406
|
+
totalFiles,
|
|
1407
|
+
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1408
|
+
});
|
|
1331
1409
|
}
|
|
1410
|
+
return groups;
|
|
1332
1411
|
}
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
encoding: "utf-8",
|
|
1338
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1339
|
-
});
|
|
1340
|
-
return output.trim().length > 0;
|
|
1341
|
-
} catch {
|
|
1342
|
-
return false;
|
|
1412
|
+
function formatSummary(stats, packageCount) {
|
|
1413
|
+
const parts = [];
|
|
1414
|
+
if (packageCount && packageCount > 1) {
|
|
1415
|
+
parts.push(`${packageCount} packages`);
|
|
1343
1416
|
}
|
|
1417
|
+
parts.push(`${stats.totalFiles.toLocaleString()} source files`);
|
|
1418
|
+
parts.push(`${stats.totalLines.toLocaleString()} lines`);
|
|
1419
|
+
parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
|
|
1420
|
+
return parts.join(" \xB7 ");
|
|
1344
1421
|
}
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1347
|
-
return void 0;
|
|
1422
|
+
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
1423
|
+
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
1348
1424
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1425
|
+
function formatRoleGroup(group) {
|
|
1426
|
+
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1427
|
+
if (group.singlePath) {
|
|
1428
|
+
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1429
|
+
}
|
|
1430
|
+
const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
|
|
1431
|
+
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1354
1432
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1433
|
+
|
|
1434
|
+
// src/display-monorepo.ts
|
|
1435
|
+
var import_types2 = require("@viberails/types");
|
|
1436
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
1437
|
+
function formatPackageSummary(pkg) {
|
|
1438
|
+
const parts = [];
|
|
1439
|
+
if (pkg.stack.framework) {
|
|
1440
|
+
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
1441
|
+
}
|
|
1442
|
+
if (pkg.stack.styling) {
|
|
1443
|
+
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1444
|
+
}
|
|
1445
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1446
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1447
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1362
1448
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
1371
|
-
renameMap.set(oldStripped, { newBare: newName });
|
|
1449
|
+
function displayMonorepoResults(scanResult) {
|
|
1450
|
+
const { stack, packages } = scanResult;
|
|
1451
|
+
console.log(`
|
|
1452
|
+
${import_chalk3.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1453
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1454
|
+
if (stack.packageManager) {
|
|
1455
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1372
1456
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const updates = [];
|
|
1379
|
-
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
1380
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
1381
|
-
const filePath = sourceFile.getFilePath();
|
|
1382
|
-
const segments = filePath.split(path8.sep);
|
|
1383
|
-
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
1384
|
-
const fileDir = path8.dirname(filePath);
|
|
1385
|
-
for (const decl of sourceFile.getImportDeclarations()) {
|
|
1386
|
-
const specifier = decl.getModuleSpecifierValue();
|
|
1387
|
-
if (!specifier.startsWith(".")) continue;
|
|
1388
|
-
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
1389
|
-
if (!match) continue;
|
|
1390
|
-
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
1391
|
-
updates.push({
|
|
1392
|
-
file: filePath,
|
|
1393
|
-
oldSpecifier: specifier,
|
|
1394
|
-
newSpecifier: newSpec,
|
|
1395
|
-
line: decl.getStartLineNumber()
|
|
1396
|
-
});
|
|
1397
|
-
decl.setModuleSpecifier(newSpec);
|
|
1398
|
-
}
|
|
1399
|
-
for (const decl of sourceFile.getExportDeclarations()) {
|
|
1400
|
-
const specifier = decl.getModuleSpecifierValue();
|
|
1401
|
-
if (!specifier || !specifier.startsWith(".")) continue;
|
|
1402
|
-
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
1403
|
-
if (!match) continue;
|
|
1404
|
-
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
1405
|
-
updates.push({
|
|
1406
|
-
file: filePath,
|
|
1407
|
-
oldSpecifier: specifier,
|
|
1408
|
-
newSpecifier: newSpec,
|
|
1409
|
-
line: decl.getStartLineNumber()
|
|
1410
|
-
});
|
|
1411
|
-
decl.setModuleSpecifier(newSpec);
|
|
1457
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1458
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1459
|
+
} else {
|
|
1460
|
+
if (stack.linter) {
|
|
1461
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1412
1462
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const args = call.getArguments();
|
|
1416
|
-
if (args.length === 0) continue;
|
|
1417
|
-
const arg = args[0];
|
|
1418
|
-
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
1419
|
-
const specifier = arg.getText().slice(1, -1);
|
|
1420
|
-
if (!specifier.startsWith(".")) continue;
|
|
1421
|
-
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
1422
|
-
if (!match) continue;
|
|
1423
|
-
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
1424
|
-
updates.push({
|
|
1425
|
-
file: filePath,
|
|
1426
|
-
oldSpecifier: specifier,
|
|
1427
|
-
newSpecifier: newSpec,
|
|
1428
|
-
line: call.getStartLineNumber()
|
|
1429
|
-
});
|
|
1430
|
-
const quote = arg.getText()[0];
|
|
1431
|
-
arg.replaceWithText(`${quote}${newSpec}${quote}`);
|
|
1463
|
+
if (stack.formatter) {
|
|
1464
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1432
1465
|
}
|
|
1433
1466
|
}
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1467
|
+
if (stack.testRunner) {
|
|
1468
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1436
1469
|
}
|
|
1437
|
-
|
|
1470
|
+
console.log("");
|
|
1471
|
+
for (const pkg of packages) {
|
|
1472
|
+
console.log(formatPackageSummary(pkg));
|
|
1473
|
+
}
|
|
1474
|
+
const packagesWithDirs = packages.filter(
|
|
1475
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1476
|
+
);
|
|
1477
|
+
if (packagesWithDirs.length > 0) {
|
|
1478
|
+
console.log(`
|
|
1479
|
+
${import_chalk3.default.bold("Structure:")}`);
|
|
1480
|
+
for (const pkg of packagesWithDirs) {
|
|
1481
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1482
|
+
if (groups.length === 0) continue;
|
|
1483
|
+
console.log(` ${pkg.relativePath}:`);
|
|
1484
|
+
for (const group of groups) {
|
|
1485
|
+
console.log(` ${import_chalk3.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
displayConventions(scanResult);
|
|
1490
|
+
displaySummarySection(scanResult);
|
|
1491
|
+
console.log("");
|
|
1438
1492
|
}
|
|
1439
|
-
function
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
const candidate = resolved + ext;
|
|
1444
|
-
const stripped = stripExtension(candidate);
|
|
1445
|
-
const match = renameMap.get(stripped);
|
|
1446
|
-
if (match) return match;
|
|
1493
|
+
function formatPackageSummaryPlain(pkg) {
|
|
1494
|
+
const parts = [];
|
|
1495
|
+
if (pkg.stack.framework) {
|
|
1496
|
+
parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
|
|
1447
1497
|
}
|
|
1448
|
-
|
|
1498
|
+
if (pkg.stack.styling) {
|
|
1499
|
+
parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
|
|
1500
|
+
}
|
|
1501
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1502
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1503
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1449
1504
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if (
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1505
|
+
function formatMonorepoResultsText(scanResult) {
|
|
1506
|
+
const lines = [];
|
|
1507
|
+
const { stack, packages } = scanResult;
|
|
1508
|
+
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1509
|
+
const sharedParts = [formatItem(stack.language)];
|
|
1510
|
+
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1511
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1512
|
+
sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
|
|
1513
|
+
} else {
|
|
1514
|
+
if (stack.linter) sharedParts.push(formatItem(stack.linter));
|
|
1515
|
+
if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
|
|
1516
|
+
}
|
|
1517
|
+
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1518
|
+
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1519
|
+
lines.push("");
|
|
1520
|
+
for (const pkg of packages) {
|
|
1521
|
+
lines.push(formatPackageSummaryPlain(pkg));
|
|
1522
|
+
}
|
|
1523
|
+
const packagesWithDirs = packages.filter(
|
|
1524
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1525
|
+
);
|
|
1526
|
+
if (packagesWithDirs.length > 0) {
|
|
1527
|
+
lines.push("");
|
|
1528
|
+
lines.push("Structure:");
|
|
1529
|
+
for (const pkg of packagesWithDirs) {
|
|
1530
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1531
|
+
if (groups.length === 0) continue;
|
|
1532
|
+
lines.push(` ${pkg.relativePath}:`);
|
|
1533
|
+
for (const group of groups) {
|
|
1534
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1472
1535
|
}
|
|
1473
|
-
current += ch;
|
|
1474
1536
|
}
|
|
1475
|
-
if (current) words.push(current.toLowerCase());
|
|
1476
1537
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
return words.join("-");
|
|
1485
|
-
case "camelCase":
|
|
1486
|
-
return words[0] + words.slice(1).map(capitalize).join("");
|
|
1487
|
-
case "PascalCase":
|
|
1488
|
-
return words.map(capitalize).join("");
|
|
1489
|
-
case "snake_case":
|
|
1490
|
-
return words.join("_");
|
|
1491
|
-
default:
|
|
1492
|
-
return bare;
|
|
1538
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1539
|
+
const pkgCount = packages.length > 1 ? packages.length : void 0;
|
|
1540
|
+
lines.push("");
|
|
1541
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1542
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1543
|
+
if (ext) {
|
|
1544
|
+
lines.push(ext);
|
|
1493
1545
|
}
|
|
1494
|
-
|
|
1495
|
-
function capitalize(word) {
|
|
1496
|
-
if (word.length === 0) return word;
|
|
1497
|
-
return word[0].toUpperCase() + word.slice(1);
|
|
1546
|
+
return lines.join("\n");
|
|
1498
1547
|
}
|
|
1499
1548
|
|
|
1500
|
-
// src/
|
|
1501
|
-
function
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
const dotIndex = filename.indexOf(".");
|
|
1505
|
-
if (dotIndex === -1) return null;
|
|
1506
|
-
const bare = filename.slice(0, dotIndex);
|
|
1507
|
-
const suffix = filename.slice(dotIndex);
|
|
1508
|
-
const newBare = convertName(bare, targetConvention);
|
|
1509
|
-
if (newBare === bare) return null;
|
|
1510
|
-
const newFilename = newBare + suffix;
|
|
1511
|
-
const newRelPath = path9.join(dir, newFilename);
|
|
1512
|
-
const oldAbsPath = path9.join(projectRoot, relPath);
|
|
1513
|
-
const newAbsPath = path9.join(projectRoot, newRelPath);
|
|
1514
|
-
if (fs9.existsSync(newAbsPath)) return null;
|
|
1515
|
-
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
1516
|
-
}
|
|
1517
|
-
function executeRename(rename) {
|
|
1518
|
-
if (fs9.existsSync(rename.newAbsPath)) return false;
|
|
1519
|
-
fs9.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
1520
|
-
return true;
|
|
1549
|
+
// src/display.ts
|
|
1550
|
+
function formatItem(item, nameMap) {
|
|
1551
|
+
const name = nameMap?.[item.name] ?? item.name;
|
|
1552
|
+
return item.version ? `${name} ${item.version}` : name;
|
|
1521
1553
|
}
|
|
1522
|
-
function
|
|
1523
|
-
const
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if (seen.has(r.newAbsPath)) continue;
|
|
1527
|
-
seen.add(r.newAbsPath);
|
|
1528
|
-
result.push(r);
|
|
1554
|
+
function confidenceLabel(convention) {
|
|
1555
|
+
const pct = Math.round(convention.consistency);
|
|
1556
|
+
if (convention.confidence === "high") {
|
|
1557
|
+
return `${pct}% \u2014 high confidence, will enforce`;
|
|
1529
1558
|
}
|
|
1530
|
-
return
|
|
1559
|
+
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
1531
1560
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1561
|
+
function displayConventions(scanResult) {
|
|
1562
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1563
|
+
if (conventionEntries.length === 0) return;
|
|
1564
|
+
console.log(`
|
|
1565
|
+
${import_chalk4.default.bold("Conventions:")}`);
|
|
1566
|
+
for (const [key, convention] of conventionEntries) {
|
|
1567
|
+
if (convention.confidence === "low") continue;
|
|
1568
|
+
const label = import_types3.CONVENTION_LABELS[key] ?? key;
|
|
1569
|
+
if (scanResult.packages.length > 1) {
|
|
1570
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1571
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1572
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1573
|
+
const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
|
|
1574
|
+
const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
|
|
1575
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1576
|
+
} else {
|
|
1577
|
+
console.log(` ${import_chalk4.default.yellow("~")} ${label}: varies by package`);
|
|
1578
|
+
for (const pv of pkgValues) {
|
|
1579
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1580
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
} else {
|
|
1584
|
+
const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
|
|
1585
|
+
const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
|
|
1586
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1554
1589
|
}
|
|
1555
|
-
function
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
});
|
|
1563
|
-
|
|
1564
|
-
fs10.mkdirSync(path10.dirname(stub.absPath), { recursive: true });
|
|
1565
|
-
fs10.writeFileSync(stub.absPath, content);
|
|
1590
|
+
function displaySummarySection(scanResult) {
|
|
1591
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1592
|
+
console.log(`
|
|
1593
|
+
${import_chalk4.default.bold("Summary:")}`);
|
|
1594
|
+
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1595
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1596
|
+
if (ext) {
|
|
1597
|
+
console.log(` ${ext}`);
|
|
1598
|
+
}
|
|
1566
1599
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const startDir = cwd ?? process.cwd();
|
|
1572
|
-
const projectRoot = findProjectRoot(startDir);
|
|
1573
|
-
if (!projectRoot) {
|
|
1574
|
-
console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
1575
|
-
return 1;
|
|
1600
|
+
function displayScanResults(scanResult) {
|
|
1601
|
+
if (scanResult.packages.length > 1) {
|
|
1602
|
+
displayMonorepoResults(scanResult);
|
|
1603
|
+
return;
|
|
1576
1604
|
}
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
);
|
|
1582
|
-
return 1;
|
|
1605
|
+
const { stack } = scanResult;
|
|
1606
|
+
console.log(`
|
|
1607
|
+
${import_chalk4.default.bold("Detected:")}`);
|
|
1608
|
+
if (stack.framework) {
|
|
1609
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
|
|
1583
1610
|
}
|
|
1584
|
-
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1587
|
-
if (isDirty) {
|
|
1588
|
-
console.log(
|
|
1589
|
-
import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
1590
|
-
);
|
|
1591
|
-
}
|
|
1611
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1612
|
+
if (stack.styling) {
|
|
1613
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
|
|
1592
1614
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const allFiles = getAllSourceFiles(projectRoot, config);
|
|
1596
|
-
const renames = [];
|
|
1597
|
-
if (shouldFixNaming) {
|
|
1598
|
-
for (const file of allFiles) {
|
|
1599
|
-
const resolved = resolveConfigForFile(file, config);
|
|
1600
|
-
if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
|
|
1601
|
-
const violation = checkNaming(file, resolved.conventions);
|
|
1602
|
-
if (!violation) continue;
|
|
1603
|
-
const convention = getConventionValue(resolved.conventions.fileNaming);
|
|
1604
|
-
if (!convention) continue;
|
|
1605
|
-
const rename = computeRename(file, convention, projectRoot);
|
|
1606
|
-
if (rename) renames.push(rename);
|
|
1607
|
-
}
|
|
1615
|
+
if (stack.backend) {
|
|
1616
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
|
|
1608
1617
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
if (shouldFixTests) {
|
|
1612
|
-
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
1613
|
-
for (const v of testViolations) {
|
|
1614
|
-
const stub = generateTestStub(v.file, config, projectRoot);
|
|
1615
|
-
if (stub) testStubs.push(stub);
|
|
1616
|
-
}
|
|
1618
|
+
if (stack.orm) {
|
|
1619
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
|
|
1617
1620
|
}
|
|
1618
|
-
if (
|
|
1619
|
-
console.log(
|
|
1620
|
-
|
|
1621
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1622
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1623
|
+
} else {
|
|
1624
|
+
if (stack.linter) {
|
|
1625
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (stack.formatter) {
|
|
1628
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1629
|
+
}
|
|
1621
1630
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
|
|
1625
|
-
return 0;
|
|
1631
|
+
if (stack.testRunner) {
|
|
1632
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1626
1633
|
}
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
if (!confirmed) {
|
|
1630
|
-
console.log("Aborted.");
|
|
1631
|
-
return 0;
|
|
1632
|
-
}
|
|
1634
|
+
if (stack.packageManager) {
|
|
1635
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1633
1636
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
renameCount++;
|
|
1637
|
+
if (stack.libraries.length > 0) {
|
|
1638
|
+
for (const lib of stack.libraries) {
|
|
1639
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
|
|
1638
1640
|
}
|
|
1639
1641
|
}
|
|
1640
|
-
|
|
1641
|
-
if (
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
let stubCount = 0;
|
|
1647
|
-
for (const stub of testStubs) {
|
|
1648
|
-
if (!fs11.existsSync(stub.absPath)) {
|
|
1649
|
-
writeTestStub(stub, config);
|
|
1650
|
-
stubCount++;
|
|
1642
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1643
|
+
if (groups.length > 0) {
|
|
1644
|
+
console.log(`
|
|
1645
|
+
${import_chalk4.default.bold("Structure:")}`);
|
|
1646
|
+
for (const group of groups) {
|
|
1647
|
+
console.log(` ${import_chalk4.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1651
1648
|
}
|
|
1652
1649
|
}
|
|
1650
|
+
displayConventions(scanResult);
|
|
1651
|
+
displaySummarySection(scanResult);
|
|
1653
1652
|
console.log("");
|
|
1654
|
-
if (renameCount > 0) {
|
|
1655
|
-
console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
1656
|
-
}
|
|
1657
|
-
if (importUpdateCount > 0) {
|
|
1658
|
-
console.log(
|
|
1659
|
-
`${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
if (stubCount > 0) {
|
|
1663
|
-
console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
1664
|
-
}
|
|
1665
|
-
return 0;
|
|
1666
1653
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
var import_types = require("@viberails/types");
|
|
1682
|
-
function groupByRole(directories) {
|
|
1683
|
-
const map = /* @__PURE__ */ new Map();
|
|
1684
|
-
for (const dir of directories) {
|
|
1685
|
-
if (dir.role === "unknown") continue;
|
|
1686
|
-
const existing = map.get(dir.role);
|
|
1687
|
-
if (existing) {
|
|
1688
|
-
existing.dirs.push(dir);
|
|
1689
|
-
} else {
|
|
1690
|
-
map.set(dir.role, { dirs: [dir] });
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
const groups = [];
|
|
1694
|
-
for (const [role, { dirs }] of map) {
|
|
1695
|
-
const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
|
|
1696
|
-
const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
|
|
1697
|
-
groups.push({
|
|
1698
|
-
role,
|
|
1699
|
-
label,
|
|
1700
|
-
dirCount: dirs.length,
|
|
1701
|
-
totalFiles,
|
|
1702
|
-
singlePath: dirs.length === 1 ? dirs[0].path : void 0
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
return groups;
|
|
1706
|
-
}
|
|
1707
|
-
function formatSummary(stats, packageCount) {
|
|
1708
|
-
const parts = [];
|
|
1709
|
-
if (packageCount && packageCount > 1) {
|
|
1710
|
-
parts.push(`${packageCount} packages`);
|
|
1654
|
+
function displayRulesPreview(config) {
|
|
1655
|
+
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1656
|
+
console.log(
|
|
1657
|
+
`${import_chalk4.default.bold("Rules:")} ${import_chalk4.default.dim("(warns on violation; use --enforce in CI to block)")}`
|
|
1658
|
+
);
|
|
1659
|
+
console.log(` ${import_chalk4.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1660
|
+
if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
|
|
1661
|
+
console.log(
|
|
1662
|
+
` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
|
|
1663
|
+
);
|
|
1664
|
+
} else if (config.rules.testCoverage > 0) {
|
|
1665
|
+
console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
|
|
1666
|
+
} else {
|
|
1667
|
+
console.log(` ${import_chalk4.default.dim("\u2022")} Test coverage target: disabled`);
|
|
1711
1668
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
}
|
|
1717
|
-
function formatExtensions(filesByExtension, maxEntries = 4) {
|
|
1718
|
-
return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
|
|
1719
|
-
}
|
|
1720
|
-
function formatRoleGroup(group) {
|
|
1721
|
-
const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
|
|
1722
|
-
if (group.singlePath) {
|
|
1723
|
-
return `${group.label} \u2014 ${group.singlePath} (${files})`;
|
|
1669
|
+
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
1670
|
+
console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1671
|
+
} else {
|
|
1672
|
+
console.log(` ${import_chalk4.default.dim("\u2022")} Enforce file naming: no`);
|
|
1724
1673
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1674
|
+
console.log(
|
|
1675
|
+
` ${import_chalk4.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1676
|
+
);
|
|
1677
|
+
console.log("");
|
|
1727
1678
|
}
|
|
1728
1679
|
|
|
1729
|
-
// src/display-monorepo.ts
|
|
1730
|
-
var import_types3 = require("@viberails/types");
|
|
1731
|
-
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1732
|
-
|
|
1733
1680
|
// src/display-text.ts
|
|
1734
|
-
var import_types2 = require("@viberails/types");
|
|
1735
1681
|
function plainConfidenceLabel(convention) {
|
|
1736
1682
|
const pct = Math.round(convention.consistency);
|
|
1737
1683
|
if (convention.confidence === "high") {
|
|
@@ -1747,7 +1693,7 @@ function formatConventionsText(scanResult) {
|
|
|
1747
1693
|
lines.push("Conventions:");
|
|
1748
1694
|
for (const [key, convention] of conventionEntries) {
|
|
1749
1695
|
if (convention.confidence === "low") continue;
|
|
1750
|
-
const label =
|
|
1696
|
+
const label = import_types4.CONVENTION_LABELS[key] ?? key;
|
|
1751
1697
|
if (scanResult.packages.length > 1) {
|
|
1752
1698
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1753
1699
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1798,17 +1744,17 @@ function formatScanResultsText(scanResult) {
|
|
|
1798
1744
|
const { stack } = scanResult;
|
|
1799
1745
|
lines.push("Detected:");
|
|
1800
1746
|
if (stack.framework) {
|
|
1801
|
-
lines.push(` \u2713 ${formatItem(stack.framework,
|
|
1747
|
+
lines.push(` \u2713 ${formatItem(stack.framework, import_types4.FRAMEWORK_NAMES)}`);
|
|
1802
1748
|
}
|
|
1803
1749
|
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1804
1750
|
if (stack.styling) {
|
|
1805
|
-
lines.push(` \u2713 ${formatItem(stack.styling,
|
|
1751
|
+
lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
|
|
1806
1752
|
}
|
|
1807
1753
|
if (stack.backend) {
|
|
1808
|
-
lines.push(` \u2713 ${formatItem(stack.backend,
|
|
1754
|
+
lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
|
|
1809
1755
|
}
|
|
1810
1756
|
if (stack.orm) {
|
|
1811
|
-
lines.push(` \u2713 ${formatItem(stack.orm,
|
|
1757
|
+
lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
|
|
1812
1758
|
}
|
|
1813
1759
|
const secondaryParts = [];
|
|
1814
1760
|
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
@@ -1820,7 +1766,7 @@ function formatScanResultsText(scanResult) {
|
|
|
1820
1766
|
}
|
|
1821
1767
|
if (stack.libraries.length > 0) {
|
|
1822
1768
|
for (const lib of stack.libraries) {
|
|
1823
|
-
lines.push(` \u2713 ${formatItem(lib,
|
|
1769
|
+
lines.push(` \u2713 ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
|
|
1824
1770
|
}
|
|
1825
1771
|
}
|
|
1826
1772
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1842,256 +1788,652 @@ function formatScanResultsText(scanResult) {
|
|
|
1842
1788
|
return lines.join("\n");
|
|
1843
1789
|
}
|
|
1844
1790
|
|
|
1845
|
-
// src/
|
|
1846
|
-
function
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1791
|
+
// src/utils/apply-rule-overrides.ts
|
|
1792
|
+
function applyRuleOverrides(config, overrides) {
|
|
1793
|
+
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1794
|
+
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1795
|
+
config.rules.testCoverage = overrides.testCoverage;
|
|
1796
|
+
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
1797
|
+
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1798
|
+
for (const pkg of config.packages) {
|
|
1799
|
+
pkg.coverage = pkg.coverage ?? {};
|
|
1800
|
+
if (pkg.coverage.summaryPath === void 0) {
|
|
1801
|
+
pkg.coverage.summaryPath = overrides.coverageSummaryPath;
|
|
1802
|
+
}
|
|
1803
|
+
if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
|
|
1804
|
+
pkg.coverage.command = overrides.coverageCommand;
|
|
1805
|
+
}
|
|
1850
1806
|
}
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
1807
|
+
if (overrides.fileNamingValue) {
|
|
1808
|
+
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1809
|
+
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
1810
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
1811
|
+
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
1812
|
+
if (oldNaming && oldNaming !== overrides.fileNamingValue) {
|
|
1813
|
+
for (const pkg of config.packages) {
|
|
1814
|
+
if (pkg.conventions?.fileNaming === oldNaming) {
|
|
1815
|
+
pkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1853
1819
|
}
|
|
1854
|
-
const files = `${pkg.statistics.totalFiles} files`;
|
|
1855
|
-
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1856
|
-
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1857
1820
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
if (
|
|
1864
|
-
|
|
1821
|
+
|
|
1822
|
+
// src/utils/diff-configs.ts
|
|
1823
|
+
var import_types5 = require("@viberails/types");
|
|
1824
|
+
function parseStackString(s) {
|
|
1825
|
+
const atIdx = s.indexOf("@");
|
|
1826
|
+
if (atIdx > 0) {
|
|
1827
|
+
return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
|
|
1865
1828
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1829
|
+
return { name: s };
|
|
1830
|
+
}
|
|
1831
|
+
function displayStackName(s) {
|
|
1832
|
+
const { name, version } = parseStackString(s);
|
|
1833
|
+
const allMaps = {
|
|
1834
|
+
...import_types5.FRAMEWORK_NAMES,
|
|
1835
|
+
...import_types5.STYLING_NAMES,
|
|
1836
|
+
...import_types5.ORM_NAMES
|
|
1837
|
+
};
|
|
1838
|
+
const display = allMaps[name] ?? name;
|
|
1839
|
+
return version ? `${display} ${version}` : display;
|
|
1840
|
+
}
|
|
1841
|
+
function isNewlyDetected(config, pkgPath, key) {
|
|
1842
|
+
return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
|
|
1843
|
+
}
|
|
1844
|
+
var STACK_FIELDS = [
|
|
1845
|
+
"framework",
|
|
1846
|
+
"styling",
|
|
1847
|
+
"backend",
|
|
1848
|
+
"orm",
|
|
1849
|
+
"linter",
|
|
1850
|
+
"formatter",
|
|
1851
|
+
"testRunner"
|
|
1852
|
+
];
|
|
1853
|
+
var CONVENTION_KEYS = [
|
|
1854
|
+
"fileNaming",
|
|
1855
|
+
"componentNaming",
|
|
1856
|
+
"hookNaming",
|
|
1857
|
+
"importAlias"
|
|
1858
|
+
];
|
|
1859
|
+
var STRUCTURE_FIELDS = [
|
|
1860
|
+
{ key: "srcDir", label: "source directory" },
|
|
1861
|
+
{ key: "pages", label: "pages directory" },
|
|
1862
|
+
{ key: "components", label: "components directory" },
|
|
1863
|
+
{ key: "hooks", label: "hooks directory" },
|
|
1864
|
+
{ key: "utils", label: "utilities directory" },
|
|
1865
|
+
{ key: "types", label: "types directory" },
|
|
1866
|
+
{ key: "tests", label: "tests directory" },
|
|
1867
|
+
{ key: "testPattern", label: "test pattern" }
|
|
1868
|
+
];
|
|
1869
|
+
function diffPackage(existing, merged, mergedConfig) {
|
|
1870
|
+
const changes = [];
|
|
1871
|
+
const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
|
|
1872
|
+
for (const field of STACK_FIELDS) {
|
|
1873
|
+
const oldVal = existing.stack?.[field];
|
|
1874
|
+
const newVal = merged.stack?.[field];
|
|
1875
|
+
if (!oldVal && newVal) {
|
|
1876
|
+
changes.push({
|
|
1877
|
+
type: "added",
|
|
1878
|
+
description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
|
|
1879
|
+
});
|
|
1880
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1881
|
+
changes.push({
|
|
1882
|
+
type: "changed",
|
|
1883
|
+
description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1884
|
+
});
|
|
1871
1885
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1886
|
+
}
|
|
1887
|
+
for (const key of CONVENTION_KEYS) {
|
|
1888
|
+
const oldVal = existing.conventions?.[key];
|
|
1889
|
+
const newVal = merged.conventions?.[key];
|
|
1890
|
+
const label = import_types5.CONVENTION_LABELS[key] ?? key;
|
|
1891
|
+
if (!oldVal && newVal) {
|
|
1892
|
+
changes.push({
|
|
1893
|
+
type: "added",
|
|
1894
|
+
description: `${pkgPrefix}New convention: ${label} (${newVal})`
|
|
1895
|
+
});
|
|
1896
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1897
|
+
const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
|
|
1898
|
+
changes.push({
|
|
1899
|
+
type: "changed",
|
|
1900
|
+
description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
|
|
1901
|
+
});
|
|
1874
1902
|
}
|
|
1875
1903
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1904
|
+
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1905
|
+
const oldVal = existing.structure?.[key];
|
|
1906
|
+
const newVal = merged.structure?.[key];
|
|
1907
|
+
if (!oldVal && newVal) {
|
|
1908
|
+
changes.push({
|
|
1909
|
+
type: "added",
|
|
1910
|
+
description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1878
1913
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1914
|
+
return changes;
|
|
1915
|
+
}
|
|
1916
|
+
function diffConfigs(existing, merged) {
|
|
1917
|
+
const changes = [];
|
|
1918
|
+
const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
|
|
1919
|
+
const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
|
|
1920
|
+
for (const existingPkg of existing.packages) {
|
|
1921
|
+
const mergedPkg = mergedByPath.get(existingPkg.path);
|
|
1922
|
+
if (mergedPkg) {
|
|
1923
|
+
changes.push(...diffPackage(existingPkg, mergedPkg, merged));
|
|
1924
|
+
}
|
|
1882
1925
|
}
|
|
1883
|
-
const
|
|
1884
|
-
(
|
|
1885
|
-
|
|
1886
|
-
if (packagesWithDirs.length > 0) {
|
|
1887
|
-
console.log(`
|
|
1888
|
-
${import_chalk5.default.bold("Structure:")}`);
|
|
1889
|
-
for (const pkg of packagesWithDirs) {
|
|
1890
|
-
const groups = groupByRole(pkg.structure.directories);
|
|
1891
|
-
if (groups.length === 0) continue;
|
|
1892
|
-
console.log(` ${pkg.relativePath}:`);
|
|
1893
|
-
for (const group of groups) {
|
|
1894
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1895
|
-
}
|
|
1926
|
+
for (const mergedPkg of merged.packages) {
|
|
1927
|
+
if (!existingByPath.has(mergedPkg.path)) {
|
|
1928
|
+
changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
|
|
1896
1929
|
}
|
|
1897
1930
|
}
|
|
1898
|
-
|
|
1899
|
-
displaySummarySection(scanResult);
|
|
1900
|
-
console.log("");
|
|
1931
|
+
return changes;
|
|
1901
1932
|
}
|
|
1902
|
-
function
|
|
1933
|
+
function formatStatsDelta(oldStats, newStats) {
|
|
1934
|
+
const fileDelta = newStats.totalFiles - oldStats.totalFiles;
|
|
1935
|
+
const lineDelta = newStats.totalLines - oldStats.totalLines;
|
|
1936
|
+
if (fileDelta === 0 && lineDelta === 0) return void 0;
|
|
1903
1937
|
const parts = [];
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
1938
|
+
if (fileDelta !== 0) {
|
|
1939
|
+
const sign = fileDelta > 0 ? "+" : "";
|
|
1940
|
+
parts.push(`${sign}${fileDelta.toLocaleString()} files`);
|
|
1906
1941
|
}
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
1942
|
+
if (lineDelta !== 0) {
|
|
1943
|
+
const sign = lineDelta > 0 ? "+" : "";
|
|
1944
|
+
parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
|
|
1909
1945
|
}
|
|
1910
|
-
|
|
1911
|
-
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1912
|
-
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1946
|
+
return `${parts.join(", ")} since last sync`;
|
|
1913
1947
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1948
|
+
|
|
1949
|
+
// src/utils/write-generated-files.ts
|
|
1950
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
1951
|
+
var path8 = __toESM(require("path"), 1);
|
|
1952
|
+
var import_context = require("@viberails/context");
|
|
1953
|
+
var CONTEXT_DIR = ".viberails";
|
|
1954
|
+
var CONTEXT_FILE = "context.md";
|
|
1955
|
+
var SCAN_RESULT_FILE = "scan-result.json";
|
|
1956
|
+
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1957
|
+
const contextDir = path8.join(projectRoot, CONTEXT_DIR);
|
|
1958
|
+
try {
|
|
1959
|
+
if (!fs9.existsSync(contextDir)) {
|
|
1960
|
+
fs9.mkdirSync(contextDir, { recursive: true });
|
|
1961
|
+
}
|
|
1962
|
+
const context = (0, import_context.generateContext)(config);
|
|
1963
|
+
fs9.writeFileSync(path8.join(contextDir, CONTEXT_FILE), context);
|
|
1964
|
+
fs9.writeFileSync(
|
|
1965
|
+
path8.join(contextDir, SCAN_RESULT_FILE),
|
|
1966
|
+
`${JSON.stringify(scanResult, null, 2)}
|
|
1967
|
+
`
|
|
1968
|
+
);
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1971
|
+
throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
|
|
1925
1972
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/commands/config.ts
|
|
1976
|
+
var CONFIG_FILE3 = "viberails.config.json";
|
|
1977
|
+
async function configCommand(options, cwd) {
|
|
1978
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
1979
|
+
if (!projectRoot) {
|
|
1980
|
+
throw new Error("No package.json found. Make sure you are inside a JS/TS project.");
|
|
1931
1981
|
}
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1982
|
+
const configPath = path9.join(projectRoot, CONFIG_FILE3);
|
|
1983
|
+
if (!fs10.existsSync(configPath)) {
|
|
1984
|
+
console.log(`${import_chalk5.default.yellow("!")} No config found. Run ${import_chalk5.default.cyan("viberails init")} first.`);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
clack6.intro("viberails config");
|
|
1988
|
+
const config = await (0, import_config6.loadConfig)(configPath);
|
|
1989
|
+
let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
|
|
1990
|
+
clack6.note(formatRulesText(config).join("\n"), "Current rules");
|
|
1991
|
+
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1992
|
+
const overrides = await promptRuleMenu({
|
|
1993
|
+
maxFileLines: config.rules.maxFileLines,
|
|
1994
|
+
testCoverage: config.rules.testCoverage,
|
|
1995
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
1996
|
+
enforceNaming: config.rules.enforceNaming,
|
|
1997
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
1998
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
1999
|
+
coverageCommand: config.defaults?.coverage?.command,
|
|
2000
|
+
packageOverrides: config.packages
|
|
2001
|
+
});
|
|
2002
|
+
applyRuleOverrides(config, overrides);
|
|
2003
|
+
if (options.rescan && config.packages.length > 1) {
|
|
2004
|
+
const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
|
|
2005
|
+
if (shouldInfer) {
|
|
2006
|
+
const bs = clack6.spinner();
|
|
2007
|
+
bs.start("Building import graph...");
|
|
2008
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2009
|
+
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2010
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
2011
|
+
const inferred = inferBoundaries(graph);
|
|
2012
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
2013
|
+
if (denyCount > 0) {
|
|
2014
|
+
config.boundaries = inferred;
|
|
2015
|
+
config.rules.enforceBoundaries = true;
|
|
2016
|
+
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
2017
|
+
} else {
|
|
2018
|
+
bs.stop("No boundary rules inferred");
|
|
1944
2019
|
}
|
|
1945
2020
|
}
|
|
1946
2021
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2022
|
+
const shouldWrite = await confirm3("Save updated configuration?");
|
|
2023
|
+
if (!shouldWrite) {
|
|
2024
|
+
clack6.outro("No changes written.");
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const compacted = (0, import_config6.compactConfig)(config);
|
|
2028
|
+
fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2029
|
+
`);
|
|
2030
|
+
if (!scanResult) {
|
|
2031
|
+
const s = clack6.spinner();
|
|
2032
|
+
s.start("Scanning for context generation...");
|
|
2033
|
+
scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
2034
|
+
s.stop("Scan complete");
|
|
2035
|
+
}
|
|
2036
|
+
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2037
|
+
clack6.log.success(
|
|
2038
|
+
`Updated:
|
|
2039
|
+
${CONFIG_FILE3}
|
|
2040
|
+
.viberails/context.md
|
|
2041
|
+
.viberails/scan-result.json`
|
|
2042
|
+
);
|
|
2043
|
+
clack6.outro("Done! Run viberails check to verify.");
|
|
2044
|
+
}
|
|
2045
|
+
async function rescanAndMerge(projectRoot, config) {
|
|
2046
|
+
const s = clack6.spinner();
|
|
2047
|
+
s.start("Re-scanning project...");
|
|
2048
|
+
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
2049
|
+
const merged = (0, import_config6.mergeConfig)(config, scanResult);
|
|
2050
|
+
s.stop("Scan complete");
|
|
2051
|
+
const changes = diffConfigs(config, merged);
|
|
2052
|
+
if (changes.length > 0) {
|
|
2053
|
+
const changeLines = changes.map((c) => {
|
|
2054
|
+
const icon = c.type === "removed" ? "-" : "+";
|
|
2055
|
+
return `${icon} ${c.description}`;
|
|
2056
|
+
}).join("\n");
|
|
2057
|
+
clack6.note(changeLines, "Changes detected");
|
|
2058
|
+
} else {
|
|
2059
|
+
clack6.log.info("No new changes detected from scan.");
|
|
2060
|
+
}
|
|
2061
|
+
Object.assign(config, merged);
|
|
2062
|
+
return scanResult;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/commands/fix.ts
|
|
2066
|
+
var fs13 = __toESM(require("fs"), 1);
|
|
2067
|
+
var path13 = __toESM(require("path"), 1);
|
|
2068
|
+
var import_config7 = require("@viberails/config");
|
|
2069
|
+
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
2070
|
+
|
|
2071
|
+
// src/commands/fix-helpers.ts
|
|
2072
|
+
var import_node_child_process4 = require("child_process");
|
|
2073
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
2074
|
+
function printPlan(renames, stubs) {
|
|
2075
|
+
if (renames.length > 0) {
|
|
2076
|
+
console.log(import_chalk6.default.bold("\nFile renames:"));
|
|
2077
|
+
for (const r of renames) {
|
|
2078
|
+
console.log(` ${import_chalk6.default.red(r.oldPath)} \u2192 ${import_chalk6.default.green(r.newPath)}`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (stubs.length > 0) {
|
|
2082
|
+
console.log(import_chalk6.default.bold("\nTest stubs to create:"));
|
|
2083
|
+
for (const s of stubs) {
|
|
2084
|
+
console.log(` ${import_chalk6.default.green("+")} ${s.path}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function checkGitDirty(projectRoot) {
|
|
2089
|
+
try {
|
|
2090
|
+
const output = (0, import_node_child_process4.execSync)("git status --porcelain", {
|
|
2091
|
+
cwd: projectRoot,
|
|
2092
|
+
encoding: "utf-8",
|
|
2093
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2094
|
+
});
|
|
2095
|
+
return output.trim().length > 0;
|
|
2096
|
+
} catch {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function getConventionValue(convention) {
|
|
2101
|
+
if (typeof convention === "string") return convention;
|
|
2102
|
+
return void 0;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/commands/fix-imports.ts
|
|
2106
|
+
var path10 = __toESM(require("path"), 1);
|
|
2107
|
+
function stripExtension(filePath) {
|
|
2108
|
+
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
2109
|
+
}
|
|
2110
|
+
function computeNewSpecifier(oldSpecifier, newBare) {
|
|
2111
|
+
const hasJsExt = oldSpecifier.endsWith(".js");
|
|
2112
|
+
const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
|
|
2113
|
+
const dir = base.lastIndexOf("/");
|
|
2114
|
+
const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
|
|
2115
|
+
const newSpec = prefix + newBare;
|
|
2116
|
+
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
2117
|
+
}
|
|
2118
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
2119
|
+
if (renames.length === 0) return [];
|
|
2120
|
+
const { Project, SyntaxKind } = await import("ts-morph");
|
|
2121
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
2122
|
+
for (const r of renames) {
|
|
2123
|
+
const oldStripped = stripExtension(r.oldAbsPath);
|
|
2124
|
+
const newFilename = path10.basename(r.newPath);
|
|
2125
|
+
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
2126
|
+
renameMap.set(oldStripped, { newBare: newName });
|
|
2127
|
+
}
|
|
2128
|
+
const project = new Project({
|
|
2129
|
+
tsConfigFilePath: void 0,
|
|
2130
|
+
skipAddingFilesFromTsConfig: true
|
|
2131
|
+
});
|
|
2132
|
+
project.addSourceFilesAtPaths(path10.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
2133
|
+
const updates = [];
|
|
2134
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
2135
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
2136
|
+
const filePath = sourceFile.getFilePath();
|
|
2137
|
+
const segments = filePath.split(path10.sep);
|
|
2138
|
+
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
2139
|
+
const fileDir = path10.dirname(filePath);
|
|
2140
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
2141
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
2142
|
+
if (!specifier.startsWith(".")) continue;
|
|
2143
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
2144
|
+
if (!match) continue;
|
|
2145
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
2146
|
+
updates.push({
|
|
2147
|
+
file: filePath,
|
|
2148
|
+
oldSpecifier: specifier,
|
|
2149
|
+
newSpecifier: newSpec,
|
|
2150
|
+
line: decl.getStartLineNumber()
|
|
2151
|
+
});
|
|
2152
|
+
decl.setModuleSpecifier(newSpec);
|
|
2153
|
+
}
|
|
2154
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
2155
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
2156
|
+
if (!specifier || !specifier.startsWith(".")) continue;
|
|
2157
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
2158
|
+
if (!match) continue;
|
|
2159
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
2160
|
+
updates.push({
|
|
2161
|
+
file: filePath,
|
|
2162
|
+
oldSpecifier: specifier,
|
|
2163
|
+
newSpecifier: newSpec,
|
|
2164
|
+
line: decl.getStartLineNumber()
|
|
2165
|
+
});
|
|
2166
|
+
decl.setModuleSpecifier(newSpec);
|
|
2167
|
+
}
|
|
2168
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2169
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
2170
|
+
const args = call.getArguments();
|
|
2171
|
+
if (args.length === 0) continue;
|
|
2172
|
+
const arg = args[0];
|
|
2173
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
2174
|
+
const specifier = arg.getText().slice(1, -1);
|
|
2175
|
+
if (!specifier.startsWith(".")) continue;
|
|
2176
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
2177
|
+
if (!match) continue;
|
|
2178
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
2179
|
+
updates.push({
|
|
2180
|
+
file: filePath,
|
|
2181
|
+
oldSpecifier: specifier,
|
|
2182
|
+
newSpecifier: newSpec,
|
|
2183
|
+
line: call.getStartLineNumber()
|
|
2184
|
+
});
|
|
2185
|
+
const quote = arg.getText()[0];
|
|
2186
|
+
arg.replaceWithText(`${quote}${newSpec}${quote}`);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (updates.length > 0) {
|
|
2190
|
+
await project.save();
|
|
1954
2191
|
}
|
|
1955
|
-
return
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// src/display.ts
|
|
1959
|
-
function formatItem(item, nameMap) {
|
|
1960
|
-
const name = nameMap?.[item.name] ?? item.name;
|
|
1961
|
-
return item.version ? `${name} ${item.version}` : name;
|
|
2192
|
+
return updates;
|
|
1962
2193
|
}
|
|
1963
|
-
function
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
2194
|
+
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
2195
|
+
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
2196
|
+
const resolved = path10.resolve(fromDir, cleanSpec);
|
|
2197
|
+
for (const ext of extensions) {
|
|
2198
|
+
const candidate = resolved + ext;
|
|
2199
|
+
const stripped = stripExtension(candidate);
|
|
2200
|
+
const match = renameMap.get(stripped);
|
|
2201
|
+
if (match) return match;
|
|
1967
2202
|
}
|
|
1968
|
-
return
|
|
2203
|
+
return void 0;
|
|
1969
2204
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2205
|
+
|
|
2206
|
+
// src/commands/fix-naming.ts
|
|
2207
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
2208
|
+
var path11 = __toESM(require("path"), 1);
|
|
2209
|
+
|
|
2210
|
+
// src/commands/convert-name.ts
|
|
2211
|
+
function splitIntoWords(name) {
|
|
2212
|
+
const parts = name.split(/[-_]/);
|
|
2213
|
+
const words = [];
|
|
2214
|
+
for (const part of parts) {
|
|
2215
|
+
if (part === "") continue;
|
|
2216
|
+
let current = "";
|
|
2217
|
+
for (let i = 0; i < part.length; i++) {
|
|
2218
|
+
const ch = part[i];
|
|
2219
|
+
const isUpper = ch >= "A" && ch <= "Z";
|
|
2220
|
+
if (isUpper && current.length > 0) {
|
|
2221
|
+
const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
|
|
2222
|
+
const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
|
|
2223
|
+
if (!prevIsUpper || nextIsLower) {
|
|
2224
|
+
words.push(current.toLowerCase());
|
|
2225
|
+
current = "";
|
|
1990
2226
|
}
|
|
1991
2227
|
}
|
|
1992
|
-
|
|
1993
|
-
const ind = convention.confidence === "high" ? import_chalk6.default.green("\u2713") : import_chalk6.default.yellow("~");
|
|
1994
|
-
const detail = import_chalk6.default.dim(`(${confidenceLabel(convention)})`);
|
|
1995
|
-
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
2228
|
+
current += ch;
|
|
1996
2229
|
}
|
|
2230
|
+
if (current) words.push(current.toLowerCase());
|
|
1997
2231
|
}
|
|
2232
|
+
return words;
|
|
1998
2233
|
}
|
|
1999
|
-
function
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2234
|
+
function convertName(bare, target) {
|
|
2235
|
+
const words = splitIntoWords(bare);
|
|
2236
|
+
if (words.length === 0) return bare;
|
|
2237
|
+
switch (target) {
|
|
2238
|
+
case "kebab-case":
|
|
2239
|
+
return words.join("-");
|
|
2240
|
+
case "camelCase":
|
|
2241
|
+
return words[0] + words.slice(1).map(capitalize).join("");
|
|
2242
|
+
case "PascalCase":
|
|
2243
|
+
return words.map(capitalize).join("");
|
|
2244
|
+
case "snake_case":
|
|
2245
|
+
return words.join("_");
|
|
2246
|
+
default:
|
|
2247
|
+
return bare;
|
|
2007
2248
|
}
|
|
2008
2249
|
}
|
|
2009
|
-
function
|
|
2010
|
-
if (
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2250
|
+
function capitalize(word) {
|
|
2251
|
+
if (word.length === 0) return word;
|
|
2252
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/commands/fix-naming.ts
|
|
2256
|
+
function computeRename(relPath, targetConvention, projectRoot) {
|
|
2257
|
+
const filename = path11.basename(relPath);
|
|
2258
|
+
const dir = path11.dirname(relPath);
|
|
2259
|
+
const dotIndex = filename.indexOf(".");
|
|
2260
|
+
if (dotIndex === -1) return null;
|
|
2261
|
+
const bare = filename.slice(0, dotIndex);
|
|
2262
|
+
const suffix = filename.slice(dotIndex);
|
|
2263
|
+
const newBare = convertName(bare, targetConvention);
|
|
2264
|
+
if (newBare === bare) return null;
|
|
2265
|
+
const newFilename = newBare + suffix;
|
|
2266
|
+
const newRelPath = path11.join(dir, newFilename);
|
|
2267
|
+
const oldAbsPath = path11.join(projectRoot, relPath);
|
|
2268
|
+
const newAbsPath = path11.join(projectRoot, newRelPath);
|
|
2269
|
+
if (fs11.existsSync(newAbsPath)) return null;
|
|
2270
|
+
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
2271
|
+
}
|
|
2272
|
+
function executeRename(rename) {
|
|
2273
|
+
if (fs11.existsSync(rename.newAbsPath)) return false;
|
|
2274
|
+
fs11.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
function deduplicateRenames(renames) {
|
|
2278
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2279
|
+
const result = [];
|
|
2280
|
+
for (const r of renames) {
|
|
2281
|
+
if (seen.has(r.newAbsPath)) continue;
|
|
2282
|
+
seen.add(r.newAbsPath);
|
|
2283
|
+
result.push(r);
|
|
2019
2284
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2285
|
+
return result;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/commands/fix-tests.ts
|
|
2289
|
+
var fs12 = __toESM(require("fs"), 1);
|
|
2290
|
+
var path12 = __toESM(require("path"), 1);
|
|
2291
|
+
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
2292
|
+
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2293
|
+
const testPattern = pkg?.structure?.testPattern;
|
|
2294
|
+
if (!testPattern) return null;
|
|
2295
|
+
const basename8 = path12.basename(sourceRelPath);
|
|
2296
|
+
const ext = path12.extname(basename8);
|
|
2297
|
+
if (!ext) return null;
|
|
2298
|
+
const stem = basename8.slice(0, -ext.length);
|
|
2299
|
+
const testSuffix = testPattern.replace("*", "");
|
|
2300
|
+
const testFilename = `${stem}${testSuffix}`;
|
|
2301
|
+
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
2302
|
+
const testAbsPath = path12.join(dir, testFilename);
|
|
2303
|
+
if (fs12.existsSync(testAbsPath)) return null;
|
|
2304
|
+
return {
|
|
2305
|
+
path: path12.relative(projectRoot, testAbsPath),
|
|
2306
|
+
absPath: testAbsPath,
|
|
2307
|
+
moduleName: stem
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
function writeTestStub(stub, config) {
|
|
2311
|
+
const pkg = resolvePackageForFile(stub.path, config);
|
|
2312
|
+
const testRunner = pkg?.stack?.testRunner ?? "";
|
|
2313
|
+
const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
|
|
2314
|
+
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
2315
|
+
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
2316
|
+
it.todo('add tests');
|
|
2317
|
+
});
|
|
2318
|
+
`;
|
|
2319
|
+
fs12.mkdirSync(path12.dirname(stub.absPath), { recursive: true });
|
|
2320
|
+
fs12.writeFileSync(stub.absPath, content);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// src/commands/fix.ts
|
|
2324
|
+
var CONFIG_FILE4 = "viberails.config.json";
|
|
2325
|
+
async function fixCommand(options, cwd) {
|
|
2326
|
+
const startDir = cwd ?? process.cwd();
|
|
2327
|
+
const projectRoot = findProjectRoot(startDir);
|
|
2328
|
+
if (!projectRoot) {
|
|
2329
|
+
console.error(`${import_chalk7.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
2330
|
+
return 1;
|
|
2023
2331
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2332
|
+
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
2333
|
+
if (!fs13.existsSync(configPath)) {
|
|
2334
|
+
console.error(
|
|
2335
|
+
`${import_chalk7.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
2336
|
+
);
|
|
2337
|
+
return 1;
|
|
2026
2338
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2339
|
+
const config = await (0, import_config7.loadConfig)(configPath);
|
|
2340
|
+
if (!options.dryRun) {
|
|
2341
|
+
const isDirty = checkGitDirty(projectRoot);
|
|
2342
|
+
if (isDirty) {
|
|
2343
|
+
console.log(
|
|
2344
|
+
import_chalk7.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2029
2347
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2348
|
+
const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
|
|
2349
|
+
const shouldFixTests = !options.rule || options.rule.includes("missing-test");
|
|
2350
|
+
const allFiles = getAllSourceFiles(projectRoot, config);
|
|
2351
|
+
const renames = [];
|
|
2352
|
+
if (shouldFixNaming) {
|
|
2353
|
+
for (const file of allFiles) {
|
|
2354
|
+
const resolved = resolveConfigForFile(file, config);
|
|
2355
|
+
if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
|
|
2356
|
+
const violation = checkNaming(file, resolved.conventions);
|
|
2357
|
+
if (!violation) continue;
|
|
2358
|
+
const convention = getConventionValue(resolved.conventions.fileNaming);
|
|
2359
|
+
if (!convention) continue;
|
|
2360
|
+
const rename = computeRename(file, convention, projectRoot);
|
|
2361
|
+
if (rename) renames.push(rename);
|
|
2035
2362
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2363
|
+
}
|
|
2364
|
+
const dedupedRenames = deduplicateRenames(renames);
|
|
2365
|
+
const testStubs = [];
|
|
2366
|
+
if (shouldFixTests) {
|
|
2367
|
+
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
2368
|
+
for (const v of testViolations) {
|
|
2369
|
+
const stub = generateTestStub(v.file, config, projectRoot);
|
|
2370
|
+
if (stub) testStubs.push(stub);
|
|
2038
2371
|
}
|
|
2039
2372
|
}
|
|
2040
|
-
if (
|
|
2041
|
-
console.log(
|
|
2373
|
+
if (dedupedRenames.length === 0 && testStubs.length === 0) {
|
|
2374
|
+
console.log(`${import_chalk7.default.green("\u2713")} No fixable violations found.`);
|
|
2375
|
+
return 0;
|
|
2042
2376
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2377
|
+
printPlan(dedupedRenames, testStubs);
|
|
2378
|
+
if (options.dryRun) {
|
|
2379
|
+
console.log(import_chalk7.default.dim("\nDry run \u2014 no changes applied."));
|
|
2380
|
+
return 0;
|
|
2045
2381
|
}
|
|
2046
|
-
if (
|
|
2047
|
-
|
|
2048
|
-
|
|
2382
|
+
if (!options.yes) {
|
|
2383
|
+
const confirmed = await confirmDangerous("Apply these fixes?");
|
|
2384
|
+
if (!confirmed) {
|
|
2385
|
+
console.log("Aborted.");
|
|
2386
|
+
return 0;
|
|
2049
2387
|
}
|
|
2050
2388
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
for (const group of groups) {
|
|
2056
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
2389
|
+
let renameCount = 0;
|
|
2390
|
+
for (const rename of dedupedRenames) {
|
|
2391
|
+
if (executeRename(rename)) {
|
|
2392
|
+
renameCount++;
|
|
2057
2393
|
}
|
|
2058
2394
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2395
|
+
let importUpdateCount = 0;
|
|
2396
|
+
if (renameCount > 0) {
|
|
2397
|
+
const appliedRenames = dedupedRenames.filter((r) => fs13.existsSync(r.newAbsPath));
|
|
2398
|
+
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
2399
|
+
importUpdateCount = updates.length;
|
|
2400
|
+
}
|
|
2401
|
+
let stubCount = 0;
|
|
2402
|
+
for (const stub of testStubs) {
|
|
2403
|
+
if (!fs13.existsSync(stub.absPath)) {
|
|
2404
|
+
writeTestStub(stub, config);
|
|
2405
|
+
stubCount++;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
console.log("");
|
|
2409
|
+
if (renameCount > 0) {
|
|
2410
|
+
console.log(`${import_chalk7.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
2411
|
+
}
|
|
2412
|
+
if (importUpdateCount > 0) {
|
|
2070
2413
|
console.log(
|
|
2071
|
-
|
|
2414
|
+
`${import_chalk7.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
2072
2415
|
);
|
|
2073
|
-
} else if (config.rules.testCoverage > 0) {
|
|
2074
|
-
console.log(` ${import_chalk6.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
|
|
2075
|
-
} else {
|
|
2076
|
-
console.log(` ${import_chalk6.default.dim("\u2022")} Test coverage target: disabled`);
|
|
2077
2416
|
}
|
|
2078
|
-
if (
|
|
2079
|
-
console.log(
|
|
2080
|
-
} else {
|
|
2081
|
-
console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
|
|
2417
|
+
if (stubCount > 0) {
|
|
2418
|
+
console.log(`${import_chalk7.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
2082
2419
|
}
|
|
2083
|
-
|
|
2084
|
-
` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
2085
|
-
);
|
|
2086
|
-
console.log("");
|
|
2420
|
+
return 0;
|
|
2087
2421
|
}
|
|
2088
2422
|
|
|
2423
|
+
// src/commands/init.ts
|
|
2424
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
2425
|
+
var path18 = __toESM(require("path"), 1);
|
|
2426
|
+
var clack8 = __toESM(require("@clack/prompts"), 1);
|
|
2427
|
+
var import_config8 = require("@viberails/config");
|
|
2428
|
+
var import_scanner2 = require("@viberails/scanner");
|
|
2429
|
+
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
2430
|
+
|
|
2089
2431
|
// src/utils/check-prerequisites.ts
|
|
2090
|
-
var
|
|
2091
|
-
var
|
|
2092
|
-
var
|
|
2093
|
-
var
|
|
2094
|
-
var
|
|
2432
|
+
var import_node_child_process5 = require("child_process");
|
|
2433
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
2434
|
+
var path14 = __toESM(require("path"), 1);
|
|
2435
|
+
var clack7 = __toESM(require("@clack/prompts"), 1);
|
|
2436
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
2095
2437
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2096
2438
|
const testRunner = scanResult.stack.testRunner;
|
|
2097
2439
|
if (!testRunner) return [];
|
|
@@ -2116,9 +2458,9 @@ function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
|
2116
2458
|
function displayMissingPrereqs(prereqs) {
|
|
2117
2459
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2118
2460
|
for (const m of missing) {
|
|
2119
|
-
console.log(` ${
|
|
2461
|
+
console.log(` ${import_chalk8.default.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
|
|
2120
2462
|
if (m.installCommand) {
|
|
2121
|
-
console.log(` Install: ${
|
|
2463
|
+
console.log(` Install: ${import_chalk8.default.cyan(m.installCommand)}`);
|
|
2122
2464
|
}
|
|
2123
2465
|
}
|
|
2124
2466
|
}
|
|
@@ -2128,11 +2470,11 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2128
2470
|
const prereqLines = prereqs.map(
|
|
2129
2471
|
(p) => `${p.installed ? "\u2713" : "\u2717"} ${p.label}${p.installed ? "" : ` \u2014 ${p.reason}`}`
|
|
2130
2472
|
).join("\n");
|
|
2131
|
-
|
|
2473
|
+
clack7.note(prereqLines, "Coverage prerequisites");
|
|
2132
2474
|
let disableCoverage = false;
|
|
2133
2475
|
for (const m of missing) {
|
|
2134
2476
|
if (!m.installCommand) continue;
|
|
2135
|
-
const choice = await
|
|
2477
|
+
const choice = await clack7.select({
|
|
2136
2478
|
message: `${m.label} is not installed. It is required for coverage percentage checks.`,
|
|
2137
2479
|
options: [
|
|
2138
2480
|
{
|
|
@@ -2154,9 +2496,9 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2154
2496
|
});
|
|
2155
2497
|
assertNotCancelled(choice);
|
|
2156
2498
|
if (choice === "install") {
|
|
2157
|
-
const is =
|
|
2499
|
+
const is = clack7.spinner();
|
|
2158
2500
|
is.start(`Installing ${m.label}...`);
|
|
2159
|
-
const result = (0,
|
|
2501
|
+
const result = (0, import_node_child_process5.spawnSync)(m.installCommand, {
|
|
2160
2502
|
cwd: projectRoot,
|
|
2161
2503
|
shell: true,
|
|
2162
2504
|
encoding: "utf-8",
|
|
@@ -2166,16 +2508,16 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2166
2508
|
is.stop(`Installed ${m.label}`);
|
|
2167
2509
|
} else {
|
|
2168
2510
|
is.stop(`Failed to install ${m.label}`);
|
|
2169
|
-
|
|
2511
|
+
clack7.log.warn(
|
|
2170
2512
|
`Install manually: ${m.installCommand}
|
|
2171
2513
|
Coverage percentage checks will not work until the dependency is installed.`
|
|
2172
2514
|
);
|
|
2173
2515
|
}
|
|
2174
2516
|
} else if (choice === "disable") {
|
|
2175
2517
|
disableCoverage = true;
|
|
2176
|
-
|
|
2518
|
+
clack7.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
|
|
2177
2519
|
} else {
|
|
2178
|
-
|
|
2520
|
+
clack7.log.info(
|
|
2179
2521
|
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2180
2522
|
Install later: ${m.installCommand}`
|
|
2181
2523
|
);
|
|
@@ -2185,8 +2527,8 @@ Install later: ${m.installCommand}`
|
|
|
2185
2527
|
}
|
|
2186
2528
|
function hasDependency(projectRoot, name) {
|
|
2187
2529
|
try {
|
|
2188
|
-
const pkgPath =
|
|
2189
|
-
const pkg = JSON.parse(
|
|
2530
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
2531
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
2190
2532
|
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2191
2533
|
} catch {
|
|
2192
2534
|
return false;
|
|
@@ -2208,84 +2550,58 @@ function filterHighConfidence(conventions, meta) {
|
|
|
2208
2550
|
}
|
|
2209
2551
|
|
|
2210
2552
|
// src/utils/update-gitignore.ts
|
|
2211
|
-
var
|
|
2212
|
-
var
|
|
2553
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
2554
|
+
var path15 = __toESM(require("path"), 1);
|
|
2213
2555
|
function updateGitignore(projectRoot) {
|
|
2214
|
-
const gitignorePath =
|
|
2556
|
+
const gitignorePath = path15.join(projectRoot, ".gitignore");
|
|
2215
2557
|
let content = "";
|
|
2216
|
-
if (
|
|
2217
|
-
content =
|
|
2558
|
+
if (fs15.existsSync(gitignorePath)) {
|
|
2559
|
+
content = fs15.readFileSync(gitignorePath, "utf-8");
|
|
2218
2560
|
}
|
|
2219
2561
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
2220
2562
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
2221
2563
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
2222
2564
|
`;
|
|
2223
|
-
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
// src/utils/write-generated-files.ts
|
|
2228
|
-
var fs14 = __toESM(require("fs"), 1);
|
|
2229
|
-
var path14 = __toESM(require("path"), 1);
|
|
2230
|
-
var import_context = require("@viberails/context");
|
|
2231
|
-
var CONTEXT_DIR = ".viberails";
|
|
2232
|
-
var CONTEXT_FILE = "context.md";
|
|
2233
|
-
var SCAN_RESULT_FILE = "scan-result.json";
|
|
2234
|
-
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
2235
|
-
const contextDir = path14.join(projectRoot, CONTEXT_DIR);
|
|
2236
|
-
try {
|
|
2237
|
-
if (!fs14.existsSync(contextDir)) {
|
|
2238
|
-
fs14.mkdirSync(contextDir, { recursive: true });
|
|
2239
|
-
}
|
|
2240
|
-
const context = (0, import_context.generateContext)(config);
|
|
2241
|
-
fs14.writeFileSync(path14.join(contextDir, CONTEXT_FILE), context);
|
|
2242
|
-
fs14.writeFileSync(
|
|
2243
|
-
path14.join(contextDir, SCAN_RESULT_FILE),
|
|
2244
|
-
`${JSON.stringify(scanResult, null, 2)}
|
|
2245
|
-
`
|
|
2246
|
-
);
|
|
2247
|
-
} catch (err) {
|
|
2248
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2249
|
-
throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
|
|
2565
|
+
fs15.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
2250
2566
|
}
|
|
2251
2567
|
}
|
|
2252
2568
|
|
|
2253
2569
|
// src/commands/init-hooks.ts
|
|
2254
|
-
var
|
|
2255
|
-
var
|
|
2256
|
-
var
|
|
2570
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
2571
|
+
var path16 = __toESM(require("path"), 1);
|
|
2572
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
2257
2573
|
var import_yaml = require("yaml");
|
|
2258
2574
|
function setupPreCommitHook(projectRoot) {
|
|
2259
|
-
const lefthookPath =
|
|
2260
|
-
if (
|
|
2575
|
+
const lefthookPath = path16.join(projectRoot, "lefthook.yml");
|
|
2576
|
+
if (fs16.existsSync(lefthookPath)) {
|
|
2261
2577
|
addLefthookPreCommit(lefthookPath);
|
|
2262
|
-
console.log(` ${
|
|
2578
|
+
console.log(` ${import_chalk9.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2263
2579
|
return "lefthook.yml";
|
|
2264
2580
|
}
|
|
2265
|
-
const huskyDir =
|
|
2266
|
-
if (
|
|
2581
|
+
const huskyDir = path16.join(projectRoot, ".husky");
|
|
2582
|
+
if (fs16.existsSync(huskyDir)) {
|
|
2267
2583
|
writeHuskyPreCommit(huskyDir);
|
|
2268
|
-
console.log(` ${
|
|
2584
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2269
2585
|
return ".husky/pre-commit";
|
|
2270
2586
|
}
|
|
2271
|
-
const gitDir =
|
|
2272
|
-
if (
|
|
2273
|
-
const hooksDir =
|
|
2274
|
-
if (!
|
|
2275
|
-
|
|
2587
|
+
const gitDir = path16.join(projectRoot, ".git");
|
|
2588
|
+
if (fs16.existsSync(gitDir)) {
|
|
2589
|
+
const hooksDir = path16.join(gitDir, "hooks");
|
|
2590
|
+
if (!fs16.existsSync(hooksDir)) {
|
|
2591
|
+
fs16.mkdirSync(hooksDir, { recursive: true });
|
|
2276
2592
|
}
|
|
2277
2593
|
writeGitHookPreCommit(hooksDir);
|
|
2278
|
-
console.log(` ${
|
|
2594
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
2279
2595
|
return ".git/hooks/pre-commit";
|
|
2280
2596
|
}
|
|
2281
2597
|
return void 0;
|
|
2282
2598
|
}
|
|
2283
2599
|
function writeGitHookPreCommit(hooksDir) {
|
|
2284
|
-
const hookPath =
|
|
2285
|
-
if (
|
|
2286
|
-
const existing =
|
|
2600
|
+
const hookPath = path16.join(hooksDir, "pre-commit");
|
|
2601
|
+
if (fs16.existsSync(hookPath)) {
|
|
2602
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
2287
2603
|
if (existing.includes("viberails")) return;
|
|
2288
|
-
|
|
2604
|
+
fs16.writeFileSync(
|
|
2289
2605
|
hookPath,
|
|
2290
2606
|
`${existing.trimEnd()}
|
|
2291
2607
|
|
|
@@ -2302,10 +2618,10 @@ npx viberails check --staged
|
|
|
2302
2618
|
"npx viberails check --staged",
|
|
2303
2619
|
""
|
|
2304
2620
|
].join("\n");
|
|
2305
|
-
|
|
2621
|
+
fs16.writeFileSync(hookPath, script, { mode: 493 });
|
|
2306
2622
|
}
|
|
2307
2623
|
function addLefthookPreCommit(lefthookPath) {
|
|
2308
|
-
const content =
|
|
2624
|
+
const content = fs16.readFileSync(lefthookPath, "utf-8");
|
|
2309
2625
|
if (content.includes("viberails")) return;
|
|
2310
2626
|
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
2311
2627
|
if (!doc["pre-commit"]) {
|
|
@@ -2317,29 +2633,29 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
2317
2633
|
doc["pre-commit"].commands.viberails = {
|
|
2318
2634
|
run: "npx viberails check --staged"
|
|
2319
2635
|
};
|
|
2320
|
-
|
|
2636
|
+
fs16.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
2321
2637
|
}
|
|
2322
2638
|
function detectHookManager(projectRoot) {
|
|
2323
|
-
if (
|
|
2324
|
-
if (
|
|
2325
|
-
if (
|
|
2639
|
+
if (fs16.existsSync(path16.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
2640
|
+
if (fs16.existsSync(path16.join(projectRoot, ".husky"))) return "Husky";
|
|
2641
|
+
if (fs16.existsSync(path16.join(projectRoot, ".git"))) return "git hook";
|
|
2326
2642
|
return void 0;
|
|
2327
2643
|
}
|
|
2328
2644
|
function setupClaudeCodeHook(projectRoot) {
|
|
2329
|
-
const claudeDir =
|
|
2330
|
-
if (!
|
|
2331
|
-
|
|
2645
|
+
const claudeDir = path16.join(projectRoot, ".claude");
|
|
2646
|
+
if (!fs16.existsSync(claudeDir)) {
|
|
2647
|
+
fs16.mkdirSync(claudeDir, { recursive: true });
|
|
2332
2648
|
}
|
|
2333
|
-
const settingsPath =
|
|
2649
|
+
const settingsPath = path16.join(claudeDir, "settings.json");
|
|
2334
2650
|
let settings = {};
|
|
2335
|
-
if (
|
|
2651
|
+
if (fs16.existsSync(settingsPath)) {
|
|
2336
2652
|
try {
|
|
2337
|
-
settings = JSON.parse(
|
|
2653
|
+
settings = JSON.parse(fs16.readFileSync(settingsPath, "utf-8"));
|
|
2338
2654
|
} catch {
|
|
2339
2655
|
console.warn(
|
|
2340
|
-
` ${
|
|
2656
|
+
` ${import_chalk9.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
2341
2657
|
);
|
|
2342
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2658
|
+
console.warn(` Fix the JSON manually, then re-run ${import_chalk9.default.cyan("viberails init --force")}`);
|
|
2343
2659
|
return;
|
|
2344
2660
|
}
|
|
2345
2661
|
}
|
|
@@ -2360,30 +2676,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2360
2676
|
}
|
|
2361
2677
|
];
|
|
2362
2678
|
settings.hooks = hooks;
|
|
2363
|
-
|
|
2679
|
+
fs16.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2364
2680
|
`);
|
|
2365
|
-
console.log(` ${
|
|
2681
|
+
console.log(` ${import_chalk9.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
2366
2682
|
}
|
|
2367
2683
|
function setupClaudeMdReference(projectRoot) {
|
|
2368
|
-
const claudeMdPath =
|
|
2684
|
+
const claudeMdPath = path16.join(projectRoot, "CLAUDE.md");
|
|
2369
2685
|
let content = "";
|
|
2370
|
-
if (
|
|
2371
|
-
content =
|
|
2686
|
+
if (fs16.existsSync(claudeMdPath)) {
|
|
2687
|
+
content = fs16.readFileSync(claudeMdPath, "utf-8");
|
|
2372
2688
|
}
|
|
2373
2689
|
if (content.includes("@.viberails/context.md")) return;
|
|
2374
2690
|
const ref = "\n@.viberails/context.md\n";
|
|
2375
2691
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
2376
|
-
|
|
2377
|
-
console.log(` ${
|
|
2692
|
+
fs16.writeFileSync(claudeMdPath, prefix + ref);
|
|
2693
|
+
console.log(` ${import_chalk9.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2378
2694
|
}
|
|
2379
2695
|
function setupGithubAction(projectRoot, packageManager) {
|
|
2380
|
-
const workflowDir =
|
|
2381
|
-
const workflowPath =
|
|
2382
|
-
if (
|
|
2383
|
-
const existing =
|
|
2696
|
+
const workflowDir = path16.join(projectRoot, ".github", "workflows");
|
|
2697
|
+
const workflowPath = path16.join(workflowDir, "viberails.yml");
|
|
2698
|
+
if (fs16.existsSync(workflowPath)) {
|
|
2699
|
+
const existing = fs16.readFileSync(workflowPath, "utf-8");
|
|
2384
2700
|
if (existing.includes("viberails")) return void 0;
|
|
2385
2701
|
}
|
|
2386
|
-
|
|
2702
|
+
fs16.mkdirSync(workflowDir, { recursive: true });
|
|
2387
2703
|
const pm = packageManager || "npm";
|
|
2388
2704
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
2389
2705
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -2417,71 +2733,71 @@ function setupGithubAction(projectRoot, packageManager) {
|
|
|
2417
2733
|
""
|
|
2418
2734
|
);
|
|
2419
2735
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
2420
|
-
|
|
2736
|
+
fs16.writeFileSync(workflowPath, content);
|
|
2421
2737
|
return ".github/workflows/viberails.yml";
|
|
2422
2738
|
}
|
|
2423
2739
|
function writeHuskyPreCommit(huskyDir) {
|
|
2424
|
-
const hookPath =
|
|
2425
|
-
if (
|
|
2426
|
-
const existing =
|
|
2740
|
+
const hookPath = path16.join(huskyDir, "pre-commit");
|
|
2741
|
+
if (fs16.existsSync(hookPath)) {
|
|
2742
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
2427
2743
|
if (!existing.includes("viberails")) {
|
|
2428
|
-
|
|
2744
|
+
fs16.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2429
2745
|
npx viberails check --staged
|
|
2430
2746
|
`);
|
|
2431
2747
|
}
|
|
2432
2748
|
return;
|
|
2433
2749
|
}
|
|
2434
|
-
|
|
2750
|
+
fs16.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
2435
2751
|
}
|
|
2436
2752
|
|
|
2437
2753
|
// src/commands/init-hooks-extra.ts
|
|
2438
|
-
var
|
|
2439
|
-
var
|
|
2440
|
-
var
|
|
2754
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
2755
|
+
var path17 = __toESM(require("path"), 1);
|
|
2756
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
2441
2757
|
var import_yaml2 = require("yaml");
|
|
2442
2758
|
function addPreCommitStep(projectRoot, name, command, marker) {
|
|
2443
|
-
const lefthookPath =
|
|
2444
|
-
if (
|
|
2445
|
-
const content =
|
|
2759
|
+
const lefthookPath = path17.join(projectRoot, "lefthook.yml");
|
|
2760
|
+
if (fs17.existsSync(lefthookPath)) {
|
|
2761
|
+
const content = fs17.readFileSync(lefthookPath, "utf-8");
|
|
2446
2762
|
if (content.includes(marker)) return void 0;
|
|
2447
2763
|
const doc = (0, import_yaml2.parse)(content) ?? {};
|
|
2448
2764
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
2449
2765
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
2450
2766
|
doc["pre-commit"].commands[name] = { run: command };
|
|
2451
|
-
|
|
2767
|
+
fs17.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
|
|
2452
2768
|
return "lefthook.yml";
|
|
2453
2769
|
}
|
|
2454
|
-
const huskyDir =
|
|
2455
|
-
if (
|
|
2456
|
-
const hookPath =
|
|
2457
|
-
if (
|
|
2458
|
-
const existing =
|
|
2770
|
+
const huskyDir = path17.join(projectRoot, ".husky");
|
|
2771
|
+
if (fs17.existsSync(huskyDir)) {
|
|
2772
|
+
const hookPath = path17.join(huskyDir, "pre-commit");
|
|
2773
|
+
if (fs17.existsSync(hookPath)) {
|
|
2774
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2459
2775
|
if (existing.includes(marker)) return void 0;
|
|
2460
|
-
|
|
2776
|
+
fs17.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2461
2777
|
${command}
|
|
2462
2778
|
`);
|
|
2463
2779
|
} else {
|
|
2464
|
-
|
|
2780
|
+
fs17.writeFileSync(hookPath, `#!/bin/sh
|
|
2465
2781
|
${command}
|
|
2466
2782
|
`, { mode: 493 });
|
|
2467
2783
|
}
|
|
2468
2784
|
return ".husky/pre-commit";
|
|
2469
2785
|
}
|
|
2470
|
-
const gitDir =
|
|
2471
|
-
if (
|
|
2472
|
-
const hooksDir =
|
|
2473
|
-
if (!
|
|
2474
|
-
const hookPath =
|
|
2475
|
-
if (
|
|
2476
|
-
const existing =
|
|
2786
|
+
const gitDir = path17.join(projectRoot, ".git");
|
|
2787
|
+
if (fs17.existsSync(gitDir)) {
|
|
2788
|
+
const hooksDir = path17.join(gitDir, "hooks");
|
|
2789
|
+
if (!fs17.existsSync(hooksDir)) fs17.mkdirSync(hooksDir, { recursive: true });
|
|
2790
|
+
const hookPath = path17.join(hooksDir, "pre-commit");
|
|
2791
|
+
if (fs17.existsSync(hookPath)) {
|
|
2792
|
+
const existing = fs17.readFileSync(hookPath, "utf-8");
|
|
2477
2793
|
if (existing.includes(marker)) return void 0;
|
|
2478
|
-
|
|
2794
|
+
fs17.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2479
2795
|
|
|
2480
2796
|
# ${name}
|
|
2481
2797
|
${command}
|
|
2482
2798
|
`);
|
|
2483
2799
|
} else {
|
|
2484
|
-
|
|
2800
|
+
fs17.writeFileSync(hookPath, `#!/bin/sh
|
|
2485
2801
|
# Generated by viberails
|
|
2486
2802
|
|
|
2487
2803
|
# ${name}
|
|
@@ -2497,7 +2813,7 @@ ${command}
|
|
|
2497
2813
|
function setupTypecheckHook(projectRoot) {
|
|
2498
2814
|
const target = addPreCommitStep(projectRoot, "typecheck", "npx tsc --noEmit", "tsc");
|
|
2499
2815
|
if (target) {
|
|
2500
|
-
console.log(` ${
|
|
2816
|
+
console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
|
|
2501
2817
|
}
|
|
2502
2818
|
return target;
|
|
2503
2819
|
}
|
|
@@ -2506,7 +2822,7 @@ function setupLintHook(projectRoot, linter) {
|
|
|
2506
2822
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
2507
2823
|
const target = addPreCommitStep(projectRoot, "lint", command, linter);
|
|
2508
2824
|
if (target) {
|
|
2509
|
-
console.log(` ${
|
|
2825
|
+
console.log(` ${import_chalk10.default.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
2510
2826
|
}
|
|
2511
2827
|
return target;
|
|
2512
2828
|
}
|
|
@@ -2540,7 +2856,7 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
2540
2856
|
}
|
|
2541
2857
|
|
|
2542
2858
|
// src/commands/init.ts
|
|
2543
|
-
var
|
|
2859
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
2544
2860
|
function getExemptedPackages(config) {
|
|
2545
2861
|
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
2546
2862
|
}
|
|
@@ -2551,11 +2867,11 @@ async function initCommand(options, cwd) {
|
|
|
2551
2867
|
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
2552
2868
|
);
|
|
2553
2869
|
}
|
|
2554
|
-
const configPath =
|
|
2555
|
-
if (
|
|
2870
|
+
const configPath = path18.join(projectRoot, CONFIG_FILE5);
|
|
2871
|
+
if (fs18.existsSync(configPath) && !options.force) {
|
|
2556
2872
|
console.log(
|
|
2557
|
-
`${
|
|
2558
|
-
Run ${
|
|
2873
|
+
`${import_chalk11.default.yellow("!")} viberails is already initialized.
|
|
2874
|
+
Run ${import_chalk11.default.cyan("viberails config")} to edit rules, ${import_chalk11.default.cyan("viberails sync")} to update, or ${import_chalk11.default.cyan("viberails init --force")} to start fresh.`
|
|
2559
2875
|
);
|
|
2560
2876
|
return;
|
|
2561
2877
|
}
|
|
@@ -2563,9 +2879,9 @@ async function initCommand(options, cwd) {
|
|
|
2563
2879
|
await initInteractive(projectRoot, configPath, options);
|
|
2564
2880
|
}
|
|
2565
2881
|
async function initNonInteractive(projectRoot, configPath) {
|
|
2566
|
-
console.log(
|
|
2567
|
-
const scanResult = await (0,
|
|
2568
|
-
const config = (0,
|
|
2882
|
+
console.log(import_chalk11.default.dim("Scanning project..."));
|
|
2883
|
+
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
2884
|
+
const config = (0, import_config8.generateConfig)(scanResult);
|
|
2569
2885
|
for (const pkg of config.packages) {
|
|
2570
2886
|
const pkgMeta = config._meta?.packages?.[pkg.path]?.conventions;
|
|
2571
2887
|
pkg.conventions = filterHighConfidence(pkg.conventions ?? {}, pkgMeta);
|
|
@@ -2576,11 +2892,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2576
2892
|
const exempted = getExemptedPackages(config);
|
|
2577
2893
|
if (exempted.length > 0) {
|
|
2578
2894
|
console.log(
|
|
2579
|
-
` ${
|
|
2895
|
+
` ${import_chalk11.default.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${import_chalk11.default.dim("(types-only)")}`
|
|
2580
2896
|
);
|
|
2581
2897
|
}
|
|
2582
2898
|
if (config.packages.length > 1) {
|
|
2583
|
-
console.log(
|
|
2899
|
+
console.log(import_chalk11.default.dim("Building import graph..."));
|
|
2584
2900
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2585
2901
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2586
2902
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -2592,51 +2908,51 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2592
2908
|
console.log(` Inferred ${denyCount} boundary rules`);
|
|
2593
2909
|
}
|
|
2594
2910
|
}
|
|
2595
|
-
const compacted = (0,
|
|
2596
|
-
|
|
2911
|
+
const compacted = (0, import_config8.compactConfig)(config);
|
|
2912
|
+
fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2597
2913
|
`);
|
|
2598
2914
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2599
2915
|
updateGitignore(projectRoot);
|
|
2600
2916
|
setupClaudeCodeHook(projectRoot);
|
|
2601
2917
|
setupClaudeMdReference(projectRoot);
|
|
2602
|
-
const preCommitTarget = setupPreCommitHook(projectRoot);
|
|
2603
2918
|
const rootPkg = config.packages[0];
|
|
2604
2919
|
const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
|
|
2605
2920
|
const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
|
|
2606
|
-
const
|
|
2921
|
+
const hookManager = detectHookManager(projectRoot);
|
|
2922
|
+
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
2923
|
+
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
2607
2924
|
const linter = rootPkg?.stack?.linter?.split("@")[0];
|
|
2608
|
-
const
|
|
2609
|
-
const ok = import_chalk10.default.green("\u2713");
|
|
2925
|
+
const ok = import_chalk11.default.green("\u2713");
|
|
2610
2926
|
const created = [
|
|
2611
|
-
`${ok} ${
|
|
2927
|
+
`${ok} ${path18.basename(configPath)}`,
|
|
2612
2928
|
`${ok} .viberails/context.md`,
|
|
2613
2929
|
`${ok} .viberails/scan-result.json`,
|
|
2614
2930
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
2615
2931
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
2616
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
2617
|
-
typecheckTarget ? `${ok} ${typecheckTarget} \u2014 added typecheck` : "",
|
|
2618
|
-
lintTarget ? `${ok} ${lintTarget} \u2014 added lint check` : "",
|
|
2932
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${import_chalk11.default.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
2619
2933
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
2620
2934
|
].filter(Boolean);
|
|
2935
|
+
if (hasHookManager && rootPkg?.stack?.language === "typescript") setupTypecheckHook(projectRoot);
|
|
2936
|
+
if (hasHookManager && linter) setupLintHook(projectRoot, linter);
|
|
2621
2937
|
console.log(`
|
|
2622
2938
|
Created:
|
|
2623
2939
|
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
2624
2940
|
}
|
|
2625
2941
|
async function initInteractive(projectRoot, configPath, options) {
|
|
2626
|
-
|
|
2627
|
-
if (
|
|
2942
|
+
clack8.intro("viberails");
|
|
2943
|
+
if (fs18.existsSync(configPath) && options.force) {
|
|
2628
2944
|
const replace = await confirmDangerous(
|
|
2629
|
-
`${
|
|
2945
|
+
`${path18.basename(configPath)} already exists and will be replaced. Continue?`
|
|
2630
2946
|
);
|
|
2631
2947
|
if (!replace) {
|
|
2632
|
-
|
|
2948
|
+
clack8.outro("Aborted. No files were written.");
|
|
2633
2949
|
return;
|
|
2634
2950
|
}
|
|
2635
2951
|
}
|
|
2636
|
-
const s =
|
|
2952
|
+
const s = clack8.spinner();
|
|
2637
2953
|
s.start("Scanning project...");
|
|
2638
|
-
const scanResult = await (0,
|
|
2639
|
-
const config = (0,
|
|
2954
|
+
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
2955
|
+
const config = (0, import_config8.generateConfig)(scanResult);
|
|
2640
2956
|
s.stop("Scan complete");
|
|
2641
2957
|
const prereqResult = await promptMissingPrereqs(
|
|
2642
2958
|
projectRoot,
|
|
@@ -2646,16 +2962,16 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2646
2962
|
config.rules.testCoverage = 0;
|
|
2647
2963
|
}
|
|
2648
2964
|
if (scanResult.statistics.totalFiles === 0) {
|
|
2649
|
-
|
|
2965
|
+
clack8.log.warn(
|
|
2650
2966
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
2651
2967
|
);
|
|
2652
2968
|
}
|
|
2653
|
-
|
|
2969
|
+
clack8.note(formatScanResultsText(scanResult), "Scan results");
|
|
2654
2970
|
const rulesLines = formatRulesText(config);
|
|
2655
2971
|
const exemptedPkgs = getExemptedPackages(config);
|
|
2656
2972
|
if (exemptedPkgs.length > 0)
|
|
2657
2973
|
rulesLines.push(`Auto-exempted from coverage: ${exemptedPkgs.join(", ")} (types-only)`);
|
|
2658
|
-
|
|
2974
|
+
clack8.note(rulesLines.join("\n"), "Rules");
|
|
2659
2975
|
const decision = await promptInitDecision();
|
|
2660
2976
|
if (decision === "customize") {
|
|
2661
2977
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
@@ -2669,41 +2985,16 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2669
2985
|
coverageCommand: config.defaults?.coverage?.command,
|
|
2670
2986
|
packageOverrides: config.packages
|
|
2671
2987
|
});
|
|
2672
|
-
|
|
2673
|
-
config.rules.maxFileLines = overrides.maxFileLines;
|
|
2674
|
-
config.rules.testCoverage = overrides.testCoverage;
|
|
2675
|
-
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
2676
|
-
config.rules.enforceNaming = overrides.enforceNaming;
|
|
2677
|
-
for (const pkg of config.packages) {
|
|
2678
|
-
pkg.coverage = pkg.coverage ?? {};
|
|
2679
|
-
if (pkg.coverage.summaryPath === void 0) {
|
|
2680
|
-
pkg.coverage.summaryPath = overrides.coverageSummaryPath;
|
|
2681
|
-
}
|
|
2682
|
-
if (pkg.coverage.command === void 0 && overrides.coverageCommand) {
|
|
2683
|
-
pkg.coverage.command = overrides.coverageCommand;
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
if (overrides.fileNamingValue) {
|
|
2687
|
-
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
2688
|
-
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2689
|
-
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
2690
|
-
if (oldNaming && oldNaming !== overrides.fileNamingValue) {
|
|
2691
|
-
for (const pkg of config.packages) {
|
|
2692
|
-
if (pkg.conventions?.fileNaming === oldNaming) {
|
|
2693
|
-
pkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2988
|
+
applyRuleOverrides(config, overrides);
|
|
2698
2989
|
}
|
|
2699
2990
|
if (config.packages.length > 1) {
|
|
2700
|
-
|
|
2991
|
+
clack8.note(
|
|
2701
2992
|
"Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
|
|
2702
2993
|
"Boundaries"
|
|
2703
2994
|
);
|
|
2704
2995
|
const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
|
|
2705
2996
|
if (shouldInfer) {
|
|
2706
|
-
const bs =
|
|
2997
|
+
const bs = clack8.spinner();
|
|
2707
2998
|
bs.start("Building import graph...");
|
|
2708
2999
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2709
3000
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -2715,7 +3006,7 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2715
3006
|
config.rules.enforceBoundaries = true;
|
|
2716
3007
|
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
2717
3008
|
const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
|
|
2718
|
-
|
|
3009
|
+
clack8.note(boundaryLines, "Boundary rules");
|
|
2719
3010
|
} else {
|
|
2720
3011
|
bs.stop("No boundary rules inferred");
|
|
2721
3012
|
}
|
|
@@ -2723,22 +3014,23 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2723
3014
|
}
|
|
2724
3015
|
const hookManager = detectHookManager(projectRoot);
|
|
2725
3016
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
2726
|
-
const integrations = await promptIntegrations(hookManager, {
|
|
3017
|
+
const integrations = await promptIntegrations(projectRoot, hookManager, {
|
|
2727
3018
|
isTypeScript: rootPkgStack?.language === "typescript",
|
|
2728
|
-
linter: rootPkgStack?.linter?.split("@")[0]
|
|
3019
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3020
|
+
packageManager: rootPkgStack?.packageManager
|
|
2729
3021
|
});
|
|
2730
3022
|
const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
|
|
2731
3023
|
if (!shouldWrite) {
|
|
2732
|
-
|
|
3024
|
+
clack8.outro("Aborted. No files were written.");
|
|
2733
3025
|
return;
|
|
2734
3026
|
}
|
|
2735
|
-
const compacted = (0,
|
|
2736
|
-
|
|
3027
|
+
const compacted = (0, import_config8.compactConfig)(config);
|
|
3028
|
+
fs18.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2737
3029
|
`);
|
|
2738
3030
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2739
3031
|
updateGitignore(projectRoot);
|
|
2740
3032
|
const createdFiles = [
|
|
2741
|
-
|
|
3033
|
+
path18.basename(configPath),
|
|
2742
3034
|
".viberails/context.md",
|
|
2743
3035
|
".viberails/scan-result.json",
|
|
2744
3036
|
...setupSelectedIntegrations(projectRoot, integrations, {
|
|
@@ -2746,155 +3038,27 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2746
3038
|
packageManager: rootPkgStack?.packageManager
|
|
2747
3039
|
})
|
|
2748
3040
|
];
|
|
2749
|
-
|
|
3041
|
+
clack8.log.success(`Created:
|
|
2750
3042
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
2751
|
-
|
|
3043
|
+
clack8.outro(
|
|
2752
3044
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
2753
|
-
${
|
|
3045
|
+
${import_chalk11.default.dim("Tip: use")} ${import_chalk11.default.cyan("viberails check --enforce")} ${import_chalk11.default.dim("in CI to block PRs on violations.")}`
|
|
2754
3046
|
);
|
|
2755
3047
|
}
|
|
2756
3048
|
|
|
2757
3049
|
// src/commands/sync.ts
|
|
2758
|
-
var
|
|
2759
|
-
var
|
|
2760
|
-
var
|
|
2761
|
-
var
|
|
2762
|
-
var
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
var import_types5 = require("@viberails/types");
|
|
2766
|
-
function parseStackString(s) {
|
|
2767
|
-
const atIdx = s.indexOf("@");
|
|
2768
|
-
if (atIdx > 0) {
|
|
2769
|
-
return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
|
|
2770
|
-
}
|
|
2771
|
-
return { name: s };
|
|
2772
|
-
}
|
|
2773
|
-
function displayStackName(s) {
|
|
2774
|
-
const { name, version } = parseStackString(s);
|
|
2775
|
-
const allMaps = {
|
|
2776
|
-
...import_types5.FRAMEWORK_NAMES,
|
|
2777
|
-
...import_types5.STYLING_NAMES,
|
|
2778
|
-
...import_types5.ORM_NAMES
|
|
2779
|
-
};
|
|
2780
|
-
const display = allMaps[name] ?? name;
|
|
2781
|
-
return version ? `${display} ${version}` : display;
|
|
2782
|
-
}
|
|
2783
|
-
function isNewlyDetected(config, pkgPath, key) {
|
|
2784
|
-
return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
|
|
2785
|
-
}
|
|
2786
|
-
var STACK_FIELDS = [
|
|
2787
|
-
"framework",
|
|
2788
|
-
"styling",
|
|
2789
|
-
"backend",
|
|
2790
|
-
"orm",
|
|
2791
|
-
"linter",
|
|
2792
|
-
"formatter",
|
|
2793
|
-
"testRunner"
|
|
2794
|
-
];
|
|
2795
|
-
var CONVENTION_KEYS = [
|
|
2796
|
-
"fileNaming",
|
|
2797
|
-
"componentNaming",
|
|
2798
|
-
"hookNaming",
|
|
2799
|
-
"importAlias"
|
|
2800
|
-
];
|
|
2801
|
-
var STRUCTURE_FIELDS = [
|
|
2802
|
-
{ key: "srcDir", label: "source directory" },
|
|
2803
|
-
{ key: "pages", label: "pages directory" },
|
|
2804
|
-
{ key: "components", label: "components directory" },
|
|
2805
|
-
{ key: "hooks", label: "hooks directory" },
|
|
2806
|
-
{ key: "utils", label: "utilities directory" },
|
|
2807
|
-
{ key: "types", label: "types directory" },
|
|
2808
|
-
{ key: "tests", label: "tests directory" },
|
|
2809
|
-
{ key: "testPattern", label: "test pattern" }
|
|
2810
|
-
];
|
|
2811
|
-
function diffPackage(existing, merged, mergedConfig) {
|
|
2812
|
-
const changes = [];
|
|
2813
|
-
const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
|
|
2814
|
-
for (const field of STACK_FIELDS) {
|
|
2815
|
-
const oldVal = existing.stack?.[field];
|
|
2816
|
-
const newVal = merged.stack?.[field];
|
|
2817
|
-
if (!oldVal && newVal) {
|
|
2818
|
-
changes.push({
|
|
2819
|
-
type: "added",
|
|
2820
|
-
description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
|
|
2821
|
-
});
|
|
2822
|
-
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
2823
|
-
changes.push({
|
|
2824
|
-
type: "changed",
|
|
2825
|
-
description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
2826
|
-
});
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
for (const key of CONVENTION_KEYS) {
|
|
2830
|
-
const oldVal = existing.conventions?.[key];
|
|
2831
|
-
const newVal = merged.conventions?.[key];
|
|
2832
|
-
const label = import_types5.CONVENTION_LABELS[key] ?? key;
|
|
2833
|
-
if (!oldVal && newVal) {
|
|
2834
|
-
changes.push({
|
|
2835
|
-
type: "added",
|
|
2836
|
-
description: `${pkgPrefix}New convention: ${label} (${newVal})`
|
|
2837
|
-
});
|
|
2838
|
-
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
2839
|
-
const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
|
|
2840
|
-
changes.push({
|
|
2841
|
-
type: "changed",
|
|
2842
|
-
description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
|
|
2843
|
-
});
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
2847
|
-
const oldVal = existing.structure?.[key];
|
|
2848
|
-
const newVal = merged.structure?.[key];
|
|
2849
|
-
if (!oldVal && newVal) {
|
|
2850
|
-
changes.push({
|
|
2851
|
-
type: "added",
|
|
2852
|
-
description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
|
|
2853
|
-
});
|
|
2854
|
-
}
|
|
2855
|
-
}
|
|
2856
|
-
return changes;
|
|
2857
|
-
}
|
|
2858
|
-
function diffConfigs(existing, merged) {
|
|
2859
|
-
const changes = [];
|
|
2860
|
-
const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
|
|
2861
|
-
const mergedByPath = new Map(merged.packages.map((p) => [p.path, p]));
|
|
2862
|
-
for (const existingPkg of existing.packages) {
|
|
2863
|
-
const mergedPkg = mergedByPath.get(existingPkg.path);
|
|
2864
|
-
if (mergedPkg) {
|
|
2865
|
-
changes.push(...diffPackage(existingPkg, mergedPkg, merged));
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
for (const mergedPkg of merged.packages) {
|
|
2869
|
-
if (!existingByPath.has(mergedPkg.path)) {
|
|
2870
|
-
changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
return changes;
|
|
2874
|
-
}
|
|
2875
|
-
function formatStatsDelta(oldStats, newStats) {
|
|
2876
|
-
const fileDelta = newStats.totalFiles - oldStats.totalFiles;
|
|
2877
|
-
const lineDelta = newStats.totalLines - oldStats.totalLines;
|
|
2878
|
-
if (fileDelta === 0 && lineDelta === 0) return void 0;
|
|
2879
|
-
const parts = [];
|
|
2880
|
-
if (fileDelta !== 0) {
|
|
2881
|
-
const sign = fileDelta > 0 ? "+" : "";
|
|
2882
|
-
parts.push(`${sign}${fileDelta.toLocaleString()} files`);
|
|
2883
|
-
}
|
|
2884
|
-
if (lineDelta !== 0) {
|
|
2885
|
-
const sign = lineDelta > 0 ? "+" : "";
|
|
2886
|
-
parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
|
|
2887
|
-
}
|
|
2888
|
-
return `${parts.join(", ")} since last sync`;
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
// src/commands/sync.ts
|
|
2892
|
-
var CONFIG_FILE5 = "viberails.config.json";
|
|
3050
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
3051
|
+
var path19 = __toESM(require("path"), 1);
|
|
3052
|
+
var clack9 = __toESM(require("@clack/prompts"), 1);
|
|
3053
|
+
var import_config9 = require("@viberails/config");
|
|
3054
|
+
var import_scanner3 = require("@viberails/scanner");
|
|
3055
|
+
var import_chalk12 = __toESM(require("chalk"), 1);
|
|
3056
|
+
var CONFIG_FILE6 = "viberails.config.json";
|
|
2893
3057
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
2894
3058
|
function loadPreviousStats(projectRoot) {
|
|
2895
|
-
const scanResultPath =
|
|
3059
|
+
const scanResultPath = path19.join(projectRoot, SCAN_RESULT_FILE2);
|
|
2896
3060
|
try {
|
|
2897
|
-
const raw =
|
|
3061
|
+
const raw = fs19.readFileSync(scanResultPath, "utf-8");
|
|
2898
3062
|
const parsed = JSON.parse(raw);
|
|
2899
3063
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
2900
3064
|
return parsed.statistics;
|
|
@@ -2903,7 +3067,7 @@ function loadPreviousStats(projectRoot) {
|
|
|
2903
3067
|
}
|
|
2904
3068
|
return void 0;
|
|
2905
3069
|
}
|
|
2906
|
-
async function syncCommand(cwd) {
|
|
3070
|
+
async function syncCommand(options, cwd) {
|
|
2907
3071
|
const startDir = cwd ?? process.cwd();
|
|
2908
3072
|
const projectRoot = findProjectRoot(startDir);
|
|
2909
3073
|
if (!projectRoot) {
|
|
@@ -2911,15 +3075,15 @@ async function syncCommand(cwd) {
|
|
|
2911
3075
|
"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"
|
|
2912
3076
|
);
|
|
2913
3077
|
}
|
|
2914
|
-
const configPath =
|
|
2915
|
-
const existing = await (0,
|
|
3078
|
+
const configPath = path19.join(projectRoot, CONFIG_FILE6);
|
|
3079
|
+
const existing = await (0, import_config9.loadConfig)(configPath);
|
|
2916
3080
|
const previousStats = loadPreviousStats(projectRoot);
|
|
2917
|
-
console.log(
|
|
2918
|
-
const scanResult = await (0,
|
|
2919
|
-
const merged = (0,
|
|
2920
|
-
const compacted = (0,
|
|
3081
|
+
console.log(import_chalk12.default.dim("Scanning project..."));
|
|
3082
|
+
const scanResult = await (0, import_scanner3.scan)(projectRoot);
|
|
3083
|
+
const merged = (0, import_config9.mergeConfig)(existing, scanResult);
|
|
3084
|
+
const compacted = (0, import_config9.compactConfig)(merged);
|
|
2921
3085
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
2922
|
-
const rawDisk =
|
|
3086
|
+
const rawDisk = fs19.readFileSync(configPath, "utf-8").trim();
|
|
2923
3087
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
2924
3088
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
2925
3089
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -2927,31 +3091,69 @@ async function syncCommand(cwd) {
|
|
|
2927
3091
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
2928
3092
|
if (changes.length > 0 || statsDelta) {
|
|
2929
3093
|
console.log(`
|
|
2930
|
-
${
|
|
3094
|
+
${import_chalk12.default.bold("Changes:")}`);
|
|
2931
3095
|
for (const change of changes) {
|
|
2932
|
-
const icon = change.type === "removed" ?
|
|
3096
|
+
const icon = change.type === "removed" ? import_chalk12.default.red("-") : import_chalk12.default.green("+");
|
|
2933
3097
|
console.log(` ${icon} ${change.description}`);
|
|
2934
3098
|
}
|
|
2935
3099
|
if (statsDelta) {
|
|
2936
|
-
console.log(` ${
|
|
3100
|
+
console.log(` ${import_chalk12.default.dim(statsDelta)}`);
|
|
2937
3101
|
}
|
|
2938
3102
|
}
|
|
2939
|
-
|
|
3103
|
+
if (options?.interactive) {
|
|
3104
|
+
clack9.intro("viberails sync (interactive)");
|
|
3105
|
+
clack9.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
3106
|
+
const decision = await clack9.select({
|
|
3107
|
+
message: "How would you like to proceed?",
|
|
3108
|
+
options: [
|
|
3109
|
+
{ value: "accept", label: "Accept changes" },
|
|
3110
|
+
{ value: "customize", label: "Customize rules" },
|
|
3111
|
+
{ value: "cancel", label: "Cancel (no changes written)" }
|
|
3112
|
+
]
|
|
3113
|
+
});
|
|
3114
|
+
assertNotCancelled(decision);
|
|
3115
|
+
if (decision === "cancel") {
|
|
3116
|
+
clack9.outro("Sync cancelled. No files were written.");
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
if (decision === "customize") {
|
|
3120
|
+
const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
|
|
3121
|
+
const overrides = await promptRuleMenu({
|
|
3122
|
+
maxFileLines: merged.rules.maxFileLines,
|
|
3123
|
+
testCoverage: merged.rules.testCoverage,
|
|
3124
|
+
enforceMissingTests: merged.rules.enforceMissingTests,
|
|
3125
|
+
enforceNaming: merged.rules.enforceNaming,
|
|
3126
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3127
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3128
|
+
coverageCommand: merged.defaults?.coverage?.command,
|
|
3129
|
+
packageOverrides: merged.packages
|
|
3130
|
+
});
|
|
3131
|
+
applyRuleOverrides(merged, overrides);
|
|
3132
|
+
const recompacted = (0, import_config9.compactConfig)(merged);
|
|
3133
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3134
|
+
`);
|
|
3135
|
+
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3136
|
+
clack9.log.success("Updated config with your customizations.");
|
|
3137
|
+
clack9.outro("Done! Run viberails check to verify.");
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
fs19.writeFileSync(configPath, `${compactedJson}
|
|
2940
3142
|
`);
|
|
2941
3143
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
2942
3144
|
console.log(`
|
|
2943
|
-
${
|
|
3145
|
+
${import_chalk12.default.bold("Synced:")}`);
|
|
2944
3146
|
if (configChanged) {
|
|
2945
|
-
console.log(` ${
|
|
3147
|
+
console.log(` ${import_chalk12.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
2946
3148
|
} else {
|
|
2947
|
-
console.log(` ${
|
|
3149
|
+
console.log(` ${import_chalk12.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
2948
3150
|
}
|
|
2949
|
-
console.log(` ${
|
|
2950
|
-
console.log(` ${
|
|
3151
|
+
console.log(` ${import_chalk12.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
3152
|
+
console.log(` ${import_chalk12.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
2951
3153
|
}
|
|
2952
3154
|
|
|
2953
3155
|
// src/index.ts
|
|
2954
|
-
var VERSION = "0.5.
|
|
3156
|
+
var VERSION = "0.5.1";
|
|
2955
3157
|
var program = new import_commander.Command();
|
|
2956
3158
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
2957
3159
|
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) => {
|
|
@@ -2959,16 +3161,25 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
2959
3161
|
await initCommand(options);
|
|
2960
3162
|
} catch (err) {
|
|
2961
3163
|
const message = err instanceof Error ? err.message : String(err);
|
|
2962
|
-
console.error(`${
|
|
3164
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
3165
|
+
process.exit(1);
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
program.command("sync").description("Re-scan and update generated files").option("-i, --interactive", "Review changes before writing").action(async (options) => {
|
|
3169
|
+
try {
|
|
3170
|
+
await syncCommand(options);
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3173
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
2963
3174
|
process.exit(1);
|
|
2964
3175
|
}
|
|
2965
3176
|
});
|
|
2966
|
-
program.command("
|
|
3177
|
+
program.command("config").description("Interactively edit existing config rules").option("--rescan", "Re-scan project first (picks up new packages, stack changes)").action(async (options) => {
|
|
2967
3178
|
try {
|
|
2968
|
-
await
|
|
3179
|
+
await configCommand(options);
|
|
2969
3180
|
} catch (err) {
|
|
2970
3181
|
const message = err instanceof Error ? err.message : String(err);
|
|
2971
|
-
console.error(`${
|
|
3182
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
2972
3183
|
process.exit(1);
|
|
2973
3184
|
}
|
|
2974
3185
|
});
|
|
@@ -2989,7 +3200,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
2989
3200
|
process.exit(exitCode);
|
|
2990
3201
|
} catch (err) {
|
|
2991
3202
|
const message = err instanceof Error ? err.message : String(err);
|
|
2992
|
-
console.error(`${
|
|
3203
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
2993
3204
|
process.exit(1);
|
|
2994
3205
|
}
|
|
2995
3206
|
}
|
|
@@ -3000,7 +3211,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3000
3211
|
process.exit(exitCode);
|
|
3001
3212
|
} catch (err) {
|
|
3002
3213
|
const message = err instanceof Error ? err.message : String(err);
|
|
3003
|
-
console.error(`${
|
|
3214
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
3004
3215
|
process.exit(1);
|
|
3005
3216
|
}
|
|
3006
3217
|
});
|
|
@@ -3009,7 +3220,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3009
3220
|
await boundariesCommand(options);
|
|
3010
3221
|
} catch (err) {
|
|
3011
3222
|
const message = err instanceof Error ? err.message : String(err);
|
|
3012
|
-
console.error(`${
|
|
3223
|
+
console.error(`${import_chalk13.default.red("Error:")} ${message}`);
|
|
3013
3224
|
process.exit(1);
|
|
3014
3225
|
}
|
|
3015
3226
|
});
|