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