viberails 0.3.2 → 0.3.3
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 +238 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +249 -56
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -116,15 +116,21 @@ async function promptIntegrations(hookManager) {
|
|
|
116
116
|
value: "claude",
|
|
117
117
|
label: "Claude Code hook",
|
|
118
118
|
hint: "checks files when Claude edits them"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
value: "claudeMd",
|
|
122
|
+
label: "CLAUDE.md reference",
|
|
123
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
119
124
|
}
|
|
120
125
|
],
|
|
121
|
-
initialValues: ["preCommit", "claude"],
|
|
126
|
+
initialValues: ["preCommit", "claude", "claudeMd"],
|
|
122
127
|
required: false
|
|
123
128
|
});
|
|
124
129
|
assertNotCancelled(result);
|
|
125
130
|
return {
|
|
126
131
|
preCommitHook: result.includes("preCommit"),
|
|
127
|
-
claudeCodeHook: result.includes("claude")
|
|
132
|
+
claudeCodeHook: result.includes("claude"),
|
|
133
|
+
claudeMdRef: result.includes("claudeMd")
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -182,22 +188,21 @@ async function boundariesCommand(options, cwd) {
|
|
|
182
188
|
displayRules(config);
|
|
183
189
|
}
|
|
184
190
|
function displayRules(config) {
|
|
185
|
-
if (!config.boundaries || config.boundaries.length === 0) {
|
|
191
|
+
if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
|
|
186
192
|
console.log(chalk.yellow("No boundary rules configured."));
|
|
187
193
|
console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
188
194
|
return;
|
|
189
195
|
}
|
|
190
|
-
const
|
|
191
|
-
const
|
|
196
|
+
const { deny } = config.boundaries;
|
|
197
|
+
const sources = Object.keys(deny).filter((k) => deny[k].length > 0);
|
|
198
|
+
const totalRules = sources.reduce((sum, k) => sum + deny[k].length, 0);
|
|
192
199
|
console.log(`
|
|
193
|
-
${chalk.bold(`Boundary rules (${
|
|
200
|
+
${chalk.bold(`Boundary rules (${totalRules} deny rules):`)}
|
|
194
201
|
`);
|
|
195
|
-
for (const
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
200
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
202
|
+
for (const source of sources) {
|
|
203
|
+
for (const target of deny[source]) {
|
|
204
|
+
console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
|
|
205
|
+
}
|
|
201
206
|
}
|
|
202
207
|
console.log(
|
|
203
208
|
`
|
|
@@ -214,24 +219,22 @@ async function inferAndDisplay(projectRoot, config, configPath) {
|
|
|
214
219
|
});
|
|
215
220
|
console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
216
221
|
const inferred = inferBoundaries(graph);
|
|
217
|
-
|
|
222
|
+
const sources = Object.keys(inferred.deny).filter((k) => inferred.deny[k].length > 0);
|
|
223
|
+
const totalRules = sources.reduce((sum, k) => sum + inferred.deny[k].length, 0);
|
|
224
|
+
if (totalRules === 0) {
|
|
218
225
|
console.log(chalk.yellow("No boundary rules could be inferred."));
|
|
219
226
|
return;
|
|
220
227
|
}
|
|
221
|
-
const allow = inferred.filter((r) => r.allow);
|
|
222
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
223
228
|
console.log(`
|
|
224
229
|
${chalk.bold("Inferred boundary rules:")}
|
|
225
230
|
`);
|
|
226
|
-
for (const
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const reason = r.reason ? chalk.dim(` (${r.reason})`) : "";
|
|
231
|
-
console.log(` ${chalk.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
|
|
231
|
+
for (const source of sources) {
|
|
232
|
+
for (const target of inferred.deny[source]) {
|
|
233
|
+
console.log(` ${chalk.red("\u2717")} ${source} \u2192 ${target}`);
|
|
234
|
+
}
|
|
232
235
|
}
|
|
233
236
|
console.log(`
|
|
234
|
-
${
|
|
237
|
+
${totalRules} denied`);
|
|
235
238
|
console.log("");
|
|
236
239
|
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
237
240
|
if (shouldSave) {
|
|
@@ -239,7 +242,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
239
242
|
config.rules.enforceBoundaries = true;
|
|
240
243
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
241
244
|
`);
|
|
242
|
-
console.log(`${chalk.green("\u2713")} Saved ${
|
|
245
|
+
console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
async function showGraph(projectRoot, config) {
|
|
@@ -598,7 +601,7 @@ async function checkCommand(options, cwd) {
|
|
|
598
601
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
599
602
|
violations.push(...testViolations);
|
|
600
603
|
}
|
|
601
|
-
if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
|
|
604
|
+
if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
|
|
602
605
|
const startTime = Date.now();
|
|
603
606
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
604
607
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -614,7 +617,7 @@ async function checkCommand(options, cwd) {
|
|
|
614
617
|
violations.push({
|
|
615
618
|
file: relFile,
|
|
616
619
|
rule: "boundary-violation",
|
|
617
|
-
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}
|
|
620
|
+
message: `Imports "${bv.specifier}" violating boundary: ${bv.rule.from} \u2192 ${bv.rule.to}`,
|
|
618
621
|
severity
|
|
619
622
|
});
|
|
620
623
|
}
|
|
@@ -1009,9 +1012,14 @@ import { generateConfig } from "@viberails/config";
|
|
|
1009
1012
|
import { scan } from "@viberails/scanner";
|
|
1010
1013
|
import chalk8 from "chalk";
|
|
1011
1014
|
|
|
1012
|
-
// src/display.ts
|
|
1013
|
-
import {
|
|
1014
|
-
|
|
1015
|
+
// src/display-text.ts
|
|
1016
|
+
import {
|
|
1017
|
+
CONVENTION_LABELS as CONVENTION_LABELS2,
|
|
1018
|
+
FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
|
|
1019
|
+
LIBRARY_NAMES as LIBRARY_NAMES2,
|
|
1020
|
+
ORM_NAMES as ORM_NAMES2,
|
|
1021
|
+
STYLING_NAMES as STYLING_NAMES3
|
|
1022
|
+
} from "@viberails/types";
|
|
1015
1023
|
|
|
1016
1024
|
// src/display-helpers.ts
|
|
1017
1025
|
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
@@ -1062,6 +1070,16 @@ function formatRoleGroup(group) {
|
|
|
1062
1070
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1063
1071
|
}
|
|
1064
1072
|
|
|
1073
|
+
// src/display.ts
|
|
1074
|
+
import {
|
|
1075
|
+
CONVENTION_LABELS,
|
|
1076
|
+
FRAMEWORK_NAMES as FRAMEWORK_NAMES2,
|
|
1077
|
+
LIBRARY_NAMES,
|
|
1078
|
+
ORM_NAMES,
|
|
1079
|
+
STYLING_NAMES as STYLING_NAMES2
|
|
1080
|
+
} from "@viberails/types";
|
|
1081
|
+
import chalk6 from "chalk";
|
|
1082
|
+
|
|
1065
1083
|
// src/display-monorepo.ts
|
|
1066
1084
|
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1067
1085
|
import chalk5 from "chalk";
|
|
@@ -1171,12 +1189,6 @@ function formatMonorepoResultsText(scanResult, config) {
|
|
|
1171
1189
|
}
|
|
1172
1190
|
|
|
1173
1191
|
// src/display.ts
|
|
1174
|
-
var CONVENTION_LABELS = {
|
|
1175
|
-
fileNaming: "File naming",
|
|
1176
|
-
componentNaming: "Component naming",
|
|
1177
|
-
hookNaming: "Hook naming",
|
|
1178
|
-
importAlias: "Import alias"
|
|
1179
|
-
};
|
|
1180
1192
|
function formatItem(item, nameMap) {
|
|
1181
1193
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1182
1194
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1312,6 +1324,11 @@ function displayRulesPreview(config) {
|
|
|
1312
1324
|
}
|
|
1313
1325
|
console.log("");
|
|
1314
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
// src/display-text.ts
|
|
1329
|
+
function getConventionStr2(cv) {
|
|
1330
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1331
|
+
}
|
|
1315
1332
|
function plainConfidenceLabel(convention) {
|
|
1316
1333
|
const pct = Math.round(convention.consistency);
|
|
1317
1334
|
if (convention.confidence === "high") {
|
|
@@ -1327,7 +1344,7 @@ function formatConventionsText(scanResult) {
|
|
|
1327
1344
|
lines.push("Conventions:");
|
|
1328
1345
|
for (const [key, convention] of conventionEntries) {
|
|
1329
1346
|
if (convention.confidence === "low") continue;
|
|
1330
|
-
const label =
|
|
1347
|
+
const label = CONVENTION_LABELS2[key] ?? key;
|
|
1331
1348
|
if (scanResult.packages.length > 1) {
|
|
1332
1349
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1333
1350
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1361,7 +1378,7 @@ function formatRulesText(config) {
|
|
|
1361
1378
|
lines.push(" \u2022 Require test files: no");
|
|
1362
1379
|
}
|
|
1363
1380
|
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1364
|
-
lines.push(` \u2022 Enforce file naming: ${
|
|
1381
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
|
|
1365
1382
|
} else {
|
|
1366
1383
|
lines.push(" \u2022 Enforce file naming: no");
|
|
1367
1384
|
}
|
|
@@ -1376,17 +1393,17 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1376
1393
|
const { stack } = scanResult;
|
|
1377
1394
|
lines.push("Detected:");
|
|
1378
1395
|
if (stack.framework) {
|
|
1379
|
-
lines.push(` \u2713 ${formatItem(stack.framework,
|
|
1396
|
+
lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
|
|
1380
1397
|
}
|
|
1381
1398
|
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1382
1399
|
if (stack.styling) {
|
|
1383
|
-
lines.push(` \u2713 ${formatItem(stack.styling,
|
|
1400
|
+
lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
|
|
1384
1401
|
}
|
|
1385
1402
|
if (stack.backend) {
|
|
1386
|
-
lines.push(` \u2713 ${formatItem(stack.backend,
|
|
1403
|
+
lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
|
|
1387
1404
|
}
|
|
1388
1405
|
if (stack.orm) {
|
|
1389
|
-
lines.push(` \u2713 ${formatItem(stack.orm,
|
|
1406
|
+
lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
|
|
1390
1407
|
}
|
|
1391
1408
|
const secondaryParts = [];
|
|
1392
1409
|
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
@@ -1398,7 +1415,7 @@ function formatScanResultsText(scanResult, config) {
|
|
|
1398
1415
|
}
|
|
1399
1416
|
if (stack.libraries.length > 0) {
|
|
1400
1417
|
for (const lib of stack.libraries) {
|
|
1401
|
-
lines.push(` \u2713 ${formatItem(lib,
|
|
1418
|
+
lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
|
|
1402
1419
|
}
|
|
1403
1420
|
}
|
|
1404
1421
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1550,7 +1567,17 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1550
1567
|
const existing = hooks.PostToolUse ?? [];
|
|
1551
1568
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1552
1569
|
const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
|
|
1553
|
-
const
|
|
1570
|
+
const checkAndReport = [
|
|
1571
|
+
`FILE=$(${extractFile})`,
|
|
1572
|
+
'if [ -z "$FILE" ]; then exit 0; fi',
|
|
1573
|
+
'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
|
|
1574
|
+
`if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
|
|
1575
|
+
' echo "$OUTPUT" >&2',
|
|
1576
|
+
" exit 2",
|
|
1577
|
+
"fi",
|
|
1578
|
+
"exit 0"
|
|
1579
|
+
].join("\n");
|
|
1580
|
+
const hookCommand = checkAndReport;
|
|
1554
1581
|
hooks.PostToolUse = [
|
|
1555
1582
|
...existing,
|
|
1556
1583
|
{
|
|
@@ -1568,6 +1595,18 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1568
1595
|
`);
|
|
1569
1596
|
console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1570
1597
|
}
|
|
1598
|
+
function setupClaudeMdReference(projectRoot) {
|
|
1599
|
+
const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
|
|
1600
|
+
let content = "";
|
|
1601
|
+
if (fs11.existsSync(claudeMdPath)) {
|
|
1602
|
+
content = fs11.readFileSync(claudeMdPath, "utf-8");
|
|
1603
|
+
}
|
|
1604
|
+
if (content.includes("@.viberails/context.md")) return;
|
|
1605
|
+
const ref = "\n@.viberails/context.md\n";
|
|
1606
|
+
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1607
|
+
fs11.writeFileSync(claudeMdPath, prefix + ref);
|
|
1608
|
+
console.log(` ${chalk7.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
1609
|
+
}
|
|
1571
1610
|
function writeHuskyPreCommit(huskyDir) {
|
|
1572
1611
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1573
1612
|
if (fs11.existsSync(hookPath)) {
|
|
@@ -1596,7 +1635,7 @@ function filterHighConfidence(conventions) {
|
|
|
1596
1635
|
}
|
|
1597
1636
|
return filtered;
|
|
1598
1637
|
}
|
|
1599
|
-
function
|
|
1638
|
+
function getConventionStr3(cv) {
|
|
1600
1639
|
if (!cv) return void 0;
|
|
1601
1640
|
return typeof cv === "string" ? cv : cv.value;
|
|
1602
1641
|
}
|
|
@@ -1613,10 +1652,10 @@ async function initCommand(options, cwd) {
|
|
|
1613
1652
|
);
|
|
1614
1653
|
}
|
|
1615
1654
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1616
|
-
if (fs12.existsSync(configPath)) {
|
|
1655
|
+
if (fs12.existsSync(configPath) && !options.force) {
|
|
1617
1656
|
console.log(
|
|
1618
1657
|
`${chalk8.yellow("!")} viberails is already initialized.
|
|
1619
|
-
Run ${chalk8.cyan("viberails sync")} to update, or
|
|
1658
|
+
Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
|
|
1620
1659
|
);
|
|
1621
1660
|
return;
|
|
1622
1661
|
}
|
|
@@ -1636,16 +1675,18 @@ async function initCommand(options, cwd) {
|
|
|
1636
1675
|
ignore: config2.ignore
|
|
1637
1676
|
});
|
|
1638
1677
|
const inferred = inferBoundaries(graph);
|
|
1639
|
-
|
|
1678
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1679
|
+
if (denyCount > 0) {
|
|
1640
1680
|
config2.boundaries = inferred;
|
|
1641
1681
|
config2.rules.enforceBoundaries = true;
|
|
1642
|
-
console.log(` Inferred ${
|
|
1682
|
+
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1643
1683
|
}
|
|
1644
1684
|
}
|
|
1645
1685
|
fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1646
1686
|
`);
|
|
1647
1687
|
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1648
1688
|
updateGitignore(projectRoot);
|
|
1689
|
+
setupClaudeMdReference(projectRoot);
|
|
1649
1690
|
console.log(`
|
|
1650
1691
|
Created:`);
|
|
1651
1692
|
console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
|
|
@@ -1677,7 +1718,7 @@ Created:`);
|
|
|
1677
1718
|
requireTests: config.rules.requireTests,
|
|
1678
1719
|
enforceNaming: config.rules.enforceNaming,
|
|
1679
1720
|
enforcement: config.enforcement,
|
|
1680
|
-
fileNamingValue:
|
|
1721
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming)
|
|
1681
1722
|
});
|
|
1682
1723
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1683
1724
|
config.rules.requireTests = overrides.requireTests;
|
|
@@ -1706,10 +1747,11 @@ Created:`);
|
|
|
1706
1747
|
ignore: config.ignore
|
|
1707
1748
|
});
|
|
1708
1749
|
const inferred = inferBoundaries(graph);
|
|
1709
|
-
|
|
1750
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1751
|
+
if (denyCount > 0) {
|
|
1710
1752
|
config.boundaries = inferred;
|
|
1711
1753
|
config.rules.enforceBoundaries = true;
|
|
1712
|
-
bs.stop(`Inferred ${
|
|
1754
|
+
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
1713
1755
|
} else {
|
|
1714
1756
|
bs.stop("No boundary rules inferred");
|
|
1715
1757
|
}
|
|
@@ -1743,6 +1785,10 @@ Created:`);
|
|
|
1743
1785
|
setupClaudeCodeHook(projectRoot);
|
|
1744
1786
|
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1745
1787
|
}
|
|
1788
|
+
if (integrations.claudeMdRef) {
|
|
1789
|
+
setupClaudeMdReference(projectRoot);
|
|
1790
|
+
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1791
|
+
}
|
|
1746
1792
|
clack2.log.success(`Created:
|
|
1747
1793
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1748
1794
|
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
@@ -1767,7 +1813,145 @@ import * as path14 from "path";
|
|
|
1767
1813
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1768
1814
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1769
1815
|
import chalk9 from "chalk";
|
|
1816
|
+
|
|
1817
|
+
// src/utils/diff-configs.ts
|
|
1818
|
+
import { CONVENTION_LABELS as CONVENTION_LABELS3, FRAMEWORK_NAMES as FRAMEWORK_NAMES4, ORM_NAMES as ORM_NAMES3, STYLING_NAMES as STYLING_NAMES4 } from "@viberails/types";
|
|
1819
|
+
function parseStackString(s) {
|
|
1820
|
+
const atIdx = s.indexOf("@");
|
|
1821
|
+
if (atIdx > 0) {
|
|
1822
|
+
return { name: s.slice(0, atIdx), version: s.slice(atIdx + 1) };
|
|
1823
|
+
}
|
|
1824
|
+
return { name: s };
|
|
1825
|
+
}
|
|
1826
|
+
function displayStackName(s) {
|
|
1827
|
+
const { name, version } = parseStackString(s);
|
|
1828
|
+
const allMaps = {
|
|
1829
|
+
...FRAMEWORK_NAMES4,
|
|
1830
|
+
...STYLING_NAMES4,
|
|
1831
|
+
...ORM_NAMES3
|
|
1832
|
+
};
|
|
1833
|
+
const display = allMaps[name] ?? name;
|
|
1834
|
+
return version ? `${display} ${version}` : display;
|
|
1835
|
+
}
|
|
1836
|
+
function conventionStr(cv) {
|
|
1837
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1838
|
+
}
|
|
1839
|
+
function isDetected(cv) {
|
|
1840
|
+
return typeof cv !== "string" && cv._detected === true;
|
|
1841
|
+
}
|
|
1842
|
+
var STACK_FIELDS = [
|
|
1843
|
+
"framework",
|
|
1844
|
+
"styling",
|
|
1845
|
+
"backend",
|
|
1846
|
+
"orm",
|
|
1847
|
+
"linter",
|
|
1848
|
+
"formatter",
|
|
1849
|
+
"testRunner"
|
|
1850
|
+
];
|
|
1851
|
+
var CONVENTION_KEYS = [
|
|
1852
|
+
"fileNaming",
|
|
1853
|
+
"componentNaming",
|
|
1854
|
+
"hookNaming",
|
|
1855
|
+
"importAlias"
|
|
1856
|
+
];
|
|
1857
|
+
var STRUCTURE_FIELDS = [
|
|
1858
|
+
{ key: "srcDir", label: "source directory" },
|
|
1859
|
+
{ key: "pages", label: "pages directory" },
|
|
1860
|
+
{ key: "components", label: "components directory" },
|
|
1861
|
+
{ key: "hooks", label: "hooks directory" },
|
|
1862
|
+
{ key: "utils", label: "utilities directory" },
|
|
1863
|
+
{ key: "types", label: "types directory" },
|
|
1864
|
+
{ key: "tests", label: "tests directory" },
|
|
1865
|
+
{ key: "testPattern", label: "test pattern" }
|
|
1866
|
+
];
|
|
1867
|
+
function diffConfigs(existing, merged) {
|
|
1868
|
+
const changes = [];
|
|
1869
|
+
for (const field of STACK_FIELDS) {
|
|
1870
|
+
const oldVal = existing.stack[field];
|
|
1871
|
+
const newVal = merged.stack[field];
|
|
1872
|
+
if (!oldVal && newVal) {
|
|
1873
|
+
changes.push({ type: "added", description: `Stack: added ${displayStackName(newVal)}` });
|
|
1874
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1875
|
+
changes.push({
|
|
1876
|
+
type: "changed",
|
|
1877
|
+
description: `Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
for (const key of CONVENTION_KEYS) {
|
|
1882
|
+
const oldVal = existing.conventions[key];
|
|
1883
|
+
const newVal = merged.conventions[key];
|
|
1884
|
+
const label = CONVENTION_LABELS3[key] ?? key;
|
|
1885
|
+
if (!oldVal && newVal) {
|
|
1886
|
+
changes.push({
|
|
1887
|
+
type: "added",
|
|
1888
|
+
description: `New convention: ${label} (${conventionStr(newVal)})`
|
|
1889
|
+
});
|
|
1890
|
+
} else if (oldVal && newVal && isDetected(newVal)) {
|
|
1891
|
+
changes.push({
|
|
1892
|
+
type: "changed",
|
|
1893
|
+
description: `Convention updated: ${label} (${conventionStr(newVal)})`
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1898
|
+
const oldVal = existing.structure[key];
|
|
1899
|
+
const newVal = merged.structure[key];
|
|
1900
|
+
if (!oldVal && newVal) {
|
|
1901
|
+
changes.push({ type: "added", description: `Structure: detected ${label} (${newVal})` });
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const existingPaths = new Set((existing.packages ?? []).map((p) => p.path));
|
|
1905
|
+
for (const pkg of merged.packages ?? []) {
|
|
1906
|
+
if (!existingPaths.has(pkg.path)) {
|
|
1907
|
+
changes.push({ type: "added", description: `New package: ${pkg.path}` });
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const existingWsPkgs = new Set(existing.workspace?.packages ?? []);
|
|
1911
|
+
const mergedWsPkgs = new Set(merged.workspace?.packages ?? []);
|
|
1912
|
+
for (const pkg of mergedWsPkgs) {
|
|
1913
|
+
if (!existingWsPkgs.has(pkg)) {
|
|
1914
|
+
changes.push({ type: "added", description: `Workspace: added ${pkg}` });
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
for (const pkg of existingWsPkgs) {
|
|
1918
|
+
if (!mergedWsPkgs.has(pkg)) {
|
|
1919
|
+
changes.push({ type: "removed", description: `Workspace: removed ${pkg}` });
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return changes;
|
|
1923
|
+
}
|
|
1924
|
+
function formatStatsDelta(oldStats, newStats) {
|
|
1925
|
+
const fileDelta = newStats.totalFiles - oldStats.totalFiles;
|
|
1926
|
+
const lineDelta = newStats.totalLines - oldStats.totalLines;
|
|
1927
|
+
if (fileDelta === 0 && lineDelta === 0) return void 0;
|
|
1928
|
+
const parts = [];
|
|
1929
|
+
if (fileDelta !== 0) {
|
|
1930
|
+
const sign = fileDelta > 0 ? "+" : "";
|
|
1931
|
+
parts.push(`${sign}${fileDelta.toLocaleString()} files`);
|
|
1932
|
+
}
|
|
1933
|
+
if (lineDelta !== 0) {
|
|
1934
|
+
const sign = lineDelta > 0 ? "+" : "";
|
|
1935
|
+
parts.push(`${sign}${lineDelta.toLocaleString()} lines`);
|
|
1936
|
+
}
|
|
1937
|
+
return `${parts.join(", ")} since last sync`;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/commands/sync.ts
|
|
1770
1941
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1942
|
+
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
1943
|
+
function loadPreviousStats(projectRoot) {
|
|
1944
|
+
const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
|
|
1945
|
+
try {
|
|
1946
|
+
const raw = fs13.readFileSync(scanResultPath, "utf-8");
|
|
1947
|
+
const parsed = JSON.parse(raw);
|
|
1948
|
+
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
1949
|
+
return parsed.statistics;
|
|
1950
|
+
}
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1771
1955
|
async function syncCommand(cwd) {
|
|
1772
1956
|
const startDir = cwd ?? process.cwd();
|
|
1773
1957
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1778,16 +1962,25 @@ async function syncCommand(cwd) {
|
|
|
1778
1962
|
}
|
|
1779
1963
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1780
1964
|
const existing = await loadConfig4(configPath);
|
|
1965
|
+
const previousStats = loadPreviousStats(projectRoot);
|
|
1781
1966
|
console.log(chalk9.dim("Scanning project..."));
|
|
1782
1967
|
const scanResult = await scan2(projectRoot);
|
|
1783
1968
|
const merged = mergeConfig(existing, scanResult);
|
|
1784
1969
|
const existingJson = JSON.stringify(existing, null, 2);
|
|
1785
1970
|
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1786
1971
|
const configChanged = existingJson !== mergedJson;
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1972
|
+
const changes = configChanged ? diffConfigs(existing, merged) : [];
|
|
1973
|
+
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
1974
|
+
if (changes.length > 0 || statsDelta) {
|
|
1975
|
+
console.log(`
|
|
1976
|
+
${chalk9.bold("Changes:")}`);
|
|
1977
|
+
for (const change of changes) {
|
|
1978
|
+
const icon = change.type === "removed" ? chalk9.red("-") : chalk9.green("+");
|
|
1979
|
+
console.log(` ${icon} ${change.description}`);
|
|
1980
|
+
}
|
|
1981
|
+
if (statsDelta) {
|
|
1982
|
+
console.log(` ${chalk9.dim(statsDelta)}`);
|
|
1983
|
+
}
|
|
1791
1984
|
}
|
|
1792
1985
|
fs13.writeFileSync(configPath, `${mergedJson}
|
|
1793
1986
|
`);
|
|
@@ -1804,10 +1997,10 @@ ${chalk9.bold("Synced:")}`);
|
|
|
1804
1997
|
}
|
|
1805
1998
|
|
|
1806
1999
|
// src/index.ts
|
|
1807
|
-
var VERSION = "0.3.
|
|
2000
|
+
var VERSION = "0.3.3";
|
|
1808
2001
|
var program = new Command();
|
|
1809
2002
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1810
|
-
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)").action(async (options) => {
|
|
2003
|
+
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) => {
|
|
1811
2004
|
try {
|
|
1812
2005
|
await initCommand(options);
|
|
1813
2006
|
} catch (err) {
|