viberails 0.3.1 → 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 +682 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +693 -224
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk10 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
8
8
|
import * as fs3 from "fs";
|
|
9
9
|
import * as path3 from "path";
|
|
10
10
|
import { loadConfig } from "@viberails/config";
|
|
11
|
-
import
|
|
11
|
+
import chalk from "chalk";
|
|
12
12
|
|
|
13
13
|
// src/utils/find-project-root.ts
|
|
14
14
|
import * as fs from "fs";
|
|
@@ -28,43 +28,110 @@ function findProjectRoot(startDir) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// src/utils/prompt.ts
|
|
31
|
-
import * as
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
import * as clack from "@clack/prompts";
|
|
32
|
+
function assertNotCancelled(value) {
|
|
33
|
+
if (clack.isCancel(value)) {
|
|
34
|
+
clack.cancel("Setup cancelled.");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function confirm2(message) {
|
|
39
|
+
const result = await clack.confirm({ message, initialValue: true });
|
|
40
|
+
assertNotCancelled(result);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
async function confirmDangerous(message) {
|
|
44
|
+
const result = await clack.confirm({ message, initialValue: false });
|
|
45
|
+
assertNotCancelled(result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
async function promptInitDecision() {
|
|
49
|
+
const result = await clack.select({
|
|
50
|
+
message: "Accept these settings?",
|
|
51
|
+
options: [
|
|
52
|
+
{ value: "accept", label: "Yes, looks good", hint: "recommended" },
|
|
53
|
+
{ value: "customize", label: "Let me customize" }
|
|
54
|
+
]
|
|
44
55
|
});
|
|
56
|
+
assertNotCancelled(result);
|
|
57
|
+
return result;
|
|
45
58
|
}
|
|
46
|
-
async function
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
async function promptRuleCustomization(defaults) {
|
|
60
|
+
const maxFileLinesResult = await clack.text({
|
|
61
|
+
message: "Maximum lines per source file?",
|
|
62
|
+
placeholder: String(defaults.maxFileLines),
|
|
63
|
+
initialValue: String(defaults.maxFileLines),
|
|
64
|
+
validate: (v) => {
|
|
65
|
+
const n = Number.parseInt(v, 10);
|
|
66
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
67
|
+
}
|
|
53
68
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (trimmed === "") resolve4(defaultYes);
|
|
59
|
-
else resolve4(trimmed === "y" || trimmed === "yes");
|
|
60
|
-
});
|
|
69
|
+
assertNotCancelled(maxFileLinesResult);
|
|
70
|
+
const requireTestsResult = await clack.confirm({
|
|
71
|
+
message: "Require matching test files for source files?",
|
|
72
|
+
initialValue: defaults.requireTests
|
|
61
73
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
assertNotCancelled(requireTestsResult);
|
|
75
|
+
const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
|
|
76
|
+
const enforceNamingResult = await clack.confirm({
|
|
77
|
+
message: namingLabel,
|
|
78
|
+
initialValue: defaults.enforceNaming
|
|
79
|
+
});
|
|
80
|
+
assertNotCancelled(enforceNamingResult);
|
|
81
|
+
const enforcementResult = await clack.select({
|
|
82
|
+
message: "Enforcement mode",
|
|
83
|
+
options: [
|
|
84
|
+
{
|
|
85
|
+
value: "warn",
|
|
86
|
+
label: "warn",
|
|
87
|
+
hint: "show violations but don't block commits (recommended)"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
value: "enforce",
|
|
91
|
+
label: "enforce",
|
|
92
|
+
hint: "block commits with violations"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
initialValue: defaults.enforcement
|
|
96
|
+
});
|
|
97
|
+
assertNotCancelled(enforcementResult);
|
|
98
|
+
return {
|
|
99
|
+
maxFileLines: Number.parseInt(maxFileLinesResult, 10),
|
|
100
|
+
requireTests: requireTestsResult,
|
|
101
|
+
enforceNaming: enforceNamingResult,
|
|
102
|
+
enforcement: enforcementResult
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function promptIntegrations(hookManager) {
|
|
106
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
|
|
107
|
+
const result = await clack.multiselect({
|
|
108
|
+
message: "Set up integrations?",
|
|
109
|
+
options: [
|
|
110
|
+
{
|
|
111
|
+
value: "preCommit",
|
|
112
|
+
label: hookLabel,
|
|
113
|
+
hint: "runs checks when you commit"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: "claude",
|
|
117
|
+
label: "Claude Code hook",
|
|
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"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
initialValues: ["preCommit", "claude", "claudeMd"],
|
|
127
|
+
required: false
|
|
128
|
+
});
|
|
129
|
+
assertNotCancelled(result);
|
|
130
|
+
return {
|
|
131
|
+
preCommitHook: result.includes("preCommit"),
|
|
132
|
+
claudeCodeHook: result.includes("claude"),
|
|
133
|
+
claudeMdRef: result.includes("claudeMd")
|
|
134
|
+
};
|
|
68
135
|
}
|
|
69
136
|
|
|
70
137
|
// src/utils/resolve-workspace-packages.ts
|
|
@@ -121,67 +188,65 @@ async function boundariesCommand(options, cwd) {
|
|
|
121
188
|
displayRules(config);
|
|
122
189
|
}
|
|
123
190
|
function displayRules(config) {
|
|
124
|
-
if (!config.boundaries || config.boundaries.length === 0) {
|
|
125
|
-
console.log(
|
|
126
|
-
console.log(`Run ${
|
|
191
|
+
if (!config.boundaries || Object.keys(config.boundaries.deny).length === 0) {
|
|
192
|
+
console.log(chalk.yellow("No boundary rules configured."));
|
|
193
|
+
console.log(`Run ${chalk.cyan("viberails boundaries --infer")} to generate rules.`);
|
|
127
194
|
return;
|
|
128
195
|
}
|
|
129
|
-
const
|
|
130
|
-
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);
|
|
131
199
|
console.log(`
|
|
132
|
-
${
|
|
200
|
+
${chalk.bold(`Boundary rules (${totalRules} deny rules):`)}
|
|
133
201
|
`);
|
|
134
|
-
for (const
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
|
|
139
|
-
console.log(` ${chalk2.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
|
+
}
|
|
140
206
|
}
|
|
141
207
|
console.log(
|
|
142
208
|
`
|
|
143
|
-
Enforcement: ${config.rules.enforceBoundaries ?
|
|
209
|
+
Enforcement: ${config.rules.enforceBoundaries ? chalk.green("on") : chalk.yellow("off")}`
|
|
144
210
|
);
|
|
145
211
|
}
|
|
146
212
|
async function inferAndDisplay(projectRoot, config, configPath) {
|
|
147
|
-
console.log(
|
|
213
|
+
console.log(chalk.dim("Analyzing imports..."));
|
|
148
214
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
149
215
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
150
216
|
const graph = await buildImportGraph(projectRoot, {
|
|
151
217
|
packages,
|
|
152
218
|
ignore: config.ignore
|
|
153
219
|
});
|
|
154
|
-
console.log(
|
|
220
|
+
console.log(chalk.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
|
|
155
221
|
const inferred = inferBoundaries(graph);
|
|
156
|
-
|
|
157
|
-
|
|
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) {
|
|
225
|
+
console.log(chalk.yellow("No boundary rules could be inferred."));
|
|
158
226
|
return;
|
|
159
227
|
}
|
|
160
|
-
const allow = inferred.filter((r) => r.allow);
|
|
161
|
-
const deny = inferred.filter((r) => !r.allow);
|
|
162
228
|
console.log(`
|
|
163
|
-
${
|
|
229
|
+
${chalk.bold("Inferred boundary rules:")}
|
|
164
230
|
`);
|
|
165
|
-
for (const
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const reason = r.reason ? chalk2.dim(` (${r.reason})`) : "";
|
|
170
|
-
console.log(` ${chalk2.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
|
+
}
|
|
171
235
|
}
|
|
172
236
|
console.log(`
|
|
173
|
-
${
|
|
174
|
-
|
|
237
|
+
${totalRules} denied`);
|
|
238
|
+
console.log("");
|
|
239
|
+
const shouldSave = await confirm2("Save to viberails.config.json?");
|
|
175
240
|
if (shouldSave) {
|
|
176
241
|
config.boundaries = inferred;
|
|
177
242
|
config.rules.enforceBoundaries = true;
|
|
178
243
|
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
179
244
|
`);
|
|
180
|
-
console.log(`${
|
|
245
|
+
console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
|
|
181
246
|
}
|
|
182
247
|
}
|
|
183
248
|
async function showGraph(projectRoot, config) {
|
|
184
|
-
console.log(
|
|
249
|
+
console.log(chalk.dim("Building import graph..."));
|
|
185
250
|
const { buildImportGraph } = await import("@viberails/graph");
|
|
186
251
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
187
252
|
const graph = await buildImportGraph(projectRoot, {
|
|
@@ -189,20 +254,20 @@ async function showGraph(projectRoot, config) {
|
|
|
189
254
|
ignore: config.ignore
|
|
190
255
|
});
|
|
191
256
|
console.log(`
|
|
192
|
-
${
|
|
257
|
+
${chalk.bold("Import dependency graph:")}
|
|
193
258
|
`);
|
|
194
259
|
console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
|
|
195
260
|
`);
|
|
196
261
|
if (graph.packages.length > 0) {
|
|
197
262
|
for (const pkg of graph.packages) {
|
|
198
263
|
const deps = pkg.internalDeps.length > 0 ? `
|
|
199
|
-
${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` :
|
|
264
|
+
${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : chalk.dim(" (no internal deps)");
|
|
200
265
|
console.log(` ${pkg.name}${deps}`);
|
|
201
266
|
}
|
|
202
267
|
}
|
|
203
268
|
if (graph.cycles.length > 0) {
|
|
204
269
|
console.log(`
|
|
205
|
-
${
|
|
270
|
+
${chalk.yellow("Cycles detected:")}`);
|
|
206
271
|
for (const cycle of graph.cycles) {
|
|
207
272
|
const paths = cycle.map((f) => path3.relative(projectRoot, f));
|
|
208
273
|
console.log(` ${paths.join(" \u2192 ")}`);
|
|
@@ -214,7 +279,7 @@ ${chalk2.yellow("Cycles detected:")}`);
|
|
|
214
279
|
import * as fs6 from "fs";
|
|
215
280
|
import * as path6 from "path";
|
|
216
281
|
import { loadConfig as loadConfig2 } from "@viberails/config";
|
|
217
|
-
import
|
|
282
|
+
import chalk2 from "chalk";
|
|
218
283
|
|
|
219
284
|
// src/commands/check-config.ts
|
|
220
285
|
function resolveConfigForFile(relPath, config) {
|
|
@@ -452,12 +517,12 @@ function printGroupedViolations(violations, limit) {
|
|
|
452
517
|
const toShow = group.slice(0, remaining);
|
|
453
518
|
const hidden = group.length - toShow.length;
|
|
454
519
|
for (const v of toShow) {
|
|
455
|
-
const icon = v.severity === "error" ?
|
|
456
|
-
console.log(`${icon} ${
|
|
520
|
+
const icon = v.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
521
|
+
console.log(`${icon} ${chalk2.dim(v.rule)} ${v.file}: ${v.message}`);
|
|
457
522
|
}
|
|
458
523
|
totalShown += toShow.length;
|
|
459
524
|
if (hidden > 0) {
|
|
460
|
-
console.log(
|
|
525
|
+
console.log(chalk2.dim(` ... and ${hidden} more ${rule} violations`));
|
|
461
526
|
}
|
|
462
527
|
}
|
|
463
528
|
}
|
|
@@ -475,13 +540,13 @@ async function checkCommand(options, cwd) {
|
|
|
475
540
|
const startDir = cwd ?? process.cwd();
|
|
476
541
|
const projectRoot = findProjectRoot(startDir);
|
|
477
542
|
if (!projectRoot) {
|
|
478
|
-
console.error(`${
|
|
543
|
+
console.error(`${chalk2.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
479
544
|
return 1;
|
|
480
545
|
}
|
|
481
546
|
const configPath = path6.join(projectRoot, CONFIG_FILE2);
|
|
482
547
|
if (!fs6.existsSync(configPath)) {
|
|
483
548
|
console.error(
|
|
484
|
-
`${
|
|
549
|
+
`${chalk2.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
485
550
|
);
|
|
486
551
|
return 1;
|
|
487
552
|
}
|
|
@@ -495,7 +560,7 @@ async function checkCommand(options, cwd) {
|
|
|
495
560
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
496
561
|
}
|
|
497
562
|
if (filesToCheck.length === 0) {
|
|
498
|
-
console.log(`${
|
|
563
|
+
console.log(`${chalk2.green("\u2713")} No files to check.`);
|
|
499
564
|
return 0;
|
|
500
565
|
}
|
|
501
566
|
const violations = [];
|
|
@@ -536,7 +601,7 @@ async function checkCommand(options, cwd) {
|
|
|
536
601
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
537
602
|
violations.push(...testViolations);
|
|
538
603
|
}
|
|
539
|
-
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) {
|
|
540
605
|
const startTime = Date.now();
|
|
541
606
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
542
607
|
const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
|
|
@@ -552,12 +617,12 @@ async function checkCommand(options, cwd) {
|
|
|
552
617
|
violations.push({
|
|
553
618
|
file: relFile,
|
|
554
619
|
rule: "boundary-violation",
|
|
555
|
-
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}`,
|
|
556
621
|
severity
|
|
557
622
|
});
|
|
558
623
|
}
|
|
559
624
|
const elapsed = Date.now() - startTime;
|
|
560
|
-
console.log(
|
|
625
|
+
console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
561
626
|
}
|
|
562
627
|
if (options.format === "json") {
|
|
563
628
|
console.log(
|
|
@@ -570,7 +635,7 @@ async function checkCommand(options, cwd) {
|
|
|
570
635
|
return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
|
|
571
636
|
}
|
|
572
637
|
if (violations.length === 0) {
|
|
573
|
-
console.log(`${
|
|
638
|
+
console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
574
639
|
return 0;
|
|
575
640
|
}
|
|
576
641
|
if (!options.quiet) {
|
|
@@ -578,7 +643,7 @@ async function checkCommand(options, cwd) {
|
|
|
578
643
|
}
|
|
579
644
|
printSummary(violations);
|
|
580
645
|
if (config.enforcement === "enforce") {
|
|
581
|
-
console.log(
|
|
646
|
+
console.log(chalk2.red("Fix violations before committing."));
|
|
582
647
|
return 1;
|
|
583
648
|
}
|
|
584
649
|
return 0;
|
|
@@ -588,23 +653,22 @@ async function checkCommand(options, cwd) {
|
|
|
588
653
|
import * as fs9 from "fs";
|
|
589
654
|
import * as path10 from "path";
|
|
590
655
|
import { loadConfig as loadConfig3 } from "@viberails/config";
|
|
591
|
-
import
|
|
656
|
+
import chalk4 from "chalk";
|
|
592
657
|
|
|
593
658
|
// src/commands/fix-helpers.ts
|
|
594
659
|
import { execSync as execSync2 } from "child_process";
|
|
595
|
-
import
|
|
596
|
-
import chalk4 from "chalk";
|
|
660
|
+
import chalk3 from "chalk";
|
|
597
661
|
function printPlan(renames, stubs) {
|
|
598
662
|
if (renames.length > 0) {
|
|
599
|
-
console.log(
|
|
663
|
+
console.log(chalk3.bold("\nFile renames:"));
|
|
600
664
|
for (const r of renames) {
|
|
601
|
-
console.log(` ${
|
|
665
|
+
console.log(` ${chalk3.red(r.oldPath)} \u2192 ${chalk3.green(r.newPath)}`);
|
|
602
666
|
}
|
|
603
667
|
}
|
|
604
668
|
if (stubs.length > 0) {
|
|
605
|
-
console.log(
|
|
669
|
+
console.log(chalk3.bold("\nTest stubs to create:"));
|
|
606
670
|
for (const s of stubs) {
|
|
607
|
-
console.log(` ${
|
|
671
|
+
console.log(` ${chalk3.green("+")} ${s.path}`);
|
|
608
672
|
}
|
|
609
673
|
}
|
|
610
674
|
}
|
|
@@ -626,15 +690,6 @@ function getConventionValue(convention) {
|
|
|
626
690
|
}
|
|
627
691
|
return void 0;
|
|
628
692
|
}
|
|
629
|
-
function promptConfirm(question) {
|
|
630
|
-
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
631
|
-
return new Promise((resolve4) => {
|
|
632
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
633
|
-
rl.close();
|
|
634
|
-
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
693
|
|
|
639
694
|
// src/commands/fix-imports.ts
|
|
640
695
|
import * as path7 from "path";
|
|
@@ -855,13 +910,13 @@ async function fixCommand(options, cwd) {
|
|
|
855
910
|
const startDir = cwd ?? process.cwd();
|
|
856
911
|
const projectRoot = findProjectRoot(startDir);
|
|
857
912
|
if (!projectRoot) {
|
|
858
|
-
console.error(`${
|
|
913
|
+
console.error(`${chalk4.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
859
914
|
return 1;
|
|
860
915
|
}
|
|
861
916
|
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
862
917
|
if (!fs9.existsSync(configPath)) {
|
|
863
918
|
console.error(
|
|
864
|
-
`${
|
|
919
|
+
`${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
865
920
|
);
|
|
866
921
|
return 1;
|
|
867
922
|
}
|
|
@@ -870,7 +925,7 @@ async function fixCommand(options, cwd) {
|
|
|
870
925
|
const isDirty = checkGitDirty(projectRoot);
|
|
871
926
|
if (isDirty) {
|
|
872
927
|
console.log(
|
|
873
|
-
|
|
928
|
+
chalk4.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
874
929
|
);
|
|
875
930
|
}
|
|
876
931
|
}
|
|
@@ -900,16 +955,16 @@ async function fixCommand(options, cwd) {
|
|
|
900
955
|
}
|
|
901
956
|
}
|
|
902
957
|
if (dedupedRenames.length === 0 && testStubs.length === 0) {
|
|
903
|
-
console.log(`${
|
|
958
|
+
console.log(`${chalk4.green("\u2713")} No fixable violations found.`);
|
|
904
959
|
return 0;
|
|
905
960
|
}
|
|
906
961
|
printPlan(dedupedRenames, testStubs);
|
|
907
962
|
if (options.dryRun) {
|
|
908
|
-
console.log(
|
|
963
|
+
console.log(chalk4.dim("\nDry run \u2014 no changes applied."));
|
|
909
964
|
return 0;
|
|
910
965
|
}
|
|
911
966
|
if (!options.yes) {
|
|
912
|
-
const confirmed = await
|
|
967
|
+
const confirmed = await confirmDangerous("Apply these fixes?");
|
|
913
968
|
if (!confirmed) {
|
|
914
969
|
console.log("Aborted.");
|
|
915
970
|
return 0;
|
|
@@ -936,15 +991,15 @@ async function fixCommand(options, cwd) {
|
|
|
936
991
|
}
|
|
937
992
|
console.log("");
|
|
938
993
|
if (renameCount > 0) {
|
|
939
|
-
console.log(`${
|
|
994
|
+
console.log(`${chalk4.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
940
995
|
}
|
|
941
996
|
if (importUpdateCount > 0) {
|
|
942
997
|
console.log(
|
|
943
|
-
`${
|
|
998
|
+
`${chalk4.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
944
999
|
);
|
|
945
1000
|
}
|
|
946
1001
|
if (stubCount > 0) {
|
|
947
|
-
console.log(`${
|
|
1002
|
+
console.log(`${chalk4.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
948
1003
|
}
|
|
949
1004
|
return 0;
|
|
950
1005
|
}
|
|
@@ -952,13 +1007,19 @@ async function fixCommand(options, cwd) {
|
|
|
952
1007
|
// src/commands/init.ts
|
|
953
1008
|
import * as fs12 from "fs";
|
|
954
1009
|
import * as path13 from "path";
|
|
1010
|
+
import * as clack2 from "@clack/prompts";
|
|
955
1011
|
import { generateConfig } from "@viberails/config";
|
|
956
1012
|
import { scan } from "@viberails/scanner";
|
|
957
|
-
import
|
|
1013
|
+
import chalk8 from "chalk";
|
|
958
1014
|
|
|
959
|
-
// src/display.ts
|
|
960
|
-
import {
|
|
961
|
-
|
|
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";
|
|
962
1023
|
|
|
963
1024
|
// src/display-helpers.ts
|
|
964
1025
|
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
@@ -1009,9 +1070,19 @@ function formatRoleGroup(group) {
|
|
|
1009
1070
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1010
1071
|
}
|
|
1011
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
|
+
|
|
1012
1083
|
// src/display-monorepo.ts
|
|
1013
1084
|
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1014
|
-
import
|
|
1085
|
+
import chalk5 from "chalk";
|
|
1015
1086
|
function formatPackageSummary(pkg) {
|
|
1016
1087
|
const parts = [];
|
|
1017
1088
|
if (pkg.stack.framework) {
|
|
@@ -1027,19 +1098,19 @@ function formatPackageSummary(pkg) {
|
|
|
1027
1098
|
function displayMonorepoResults(scanResult) {
|
|
1028
1099
|
const { stack, packages } = scanResult;
|
|
1029
1100
|
console.log(`
|
|
1030
|
-
${
|
|
1031
|
-
console.log(` ${
|
|
1101
|
+
${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
1102
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1032
1103
|
if (stack.packageManager) {
|
|
1033
|
-
console.log(` ${
|
|
1104
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1034
1105
|
}
|
|
1035
1106
|
if (stack.linter) {
|
|
1036
|
-
console.log(` ${
|
|
1107
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1037
1108
|
}
|
|
1038
1109
|
if (stack.formatter) {
|
|
1039
|
-
console.log(` ${
|
|
1110
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1040
1111
|
}
|
|
1041
1112
|
if (stack.testRunner) {
|
|
1042
|
-
console.log(` ${
|
|
1113
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1043
1114
|
}
|
|
1044
1115
|
console.log("");
|
|
1045
1116
|
for (const pkg of packages) {
|
|
@@ -1050,13 +1121,13 @@ ${chalk6.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
|
1050
1121
|
);
|
|
1051
1122
|
if (packagesWithDirs.length > 0) {
|
|
1052
1123
|
console.log(`
|
|
1053
|
-
${
|
|
1124
|
+
${chalk5.bold("Structure:")}`);
|
|
1054
1125
|
for (const pkg of packagesWithDirs) {
|
|
1055
1126
|
const groups = groupByRole(pkg.structure.directories);
|
|
1056
1127
|
if (groups.length === 0) continue;
|
|
1057
1128
|
console.log(` ${pkg.relativePath}:`);
|
|
1058
1129
|
for (const group of groups) {
|
|
1059
|
-
console.log(` ${
|
|
1130
|
+
console.log(` ${chalk5.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1060
1131
|
}
|
|
1061
1132
|
}
|
|
1062
1133
|
}
|
|
@@ -1064,14 +1135,60 @@ ${chalk6.bold("Structure:")}`);
|
|
|
1064
1135
|
displaySummarySection(scanResult);
|
|
1065
1136
|
console.log("");
|
|
1066
1137
|
}
|
|
1138
|
+
function formatPackageSummaryPlain(pkg) {
|
|
1139
|
+
const parts = [];
|
|
1140
|
+
if (pkg.stack.framework) {
|
|
1141
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
|
|
1142
|
+
}
|
|
1143
|
+
if (pkg.stack.styling) {
|
|
1144
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
1145
|
+
}
|
|
1146
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
1147
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1148
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1149
|
+
}
|
|
1150
|
+
function formatMonorepoResultsText(scanResult, config) {
|
|
1151
|
+
const lines = [];
|
|
1152
|
+
const { stack, packages } = scanResult;
|
|
1153
|
+
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1154
|
+
const sharedParts = [formatItem(stack.language)];
|
|
1155
|
+
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1156
|
+
if (stack.linter) sharedParts.push(formatItem(stack.linter));
|
|
1157
|
+
if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
|
|
1158
|
+
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1159
|
+
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1160
|
+
lines.push("");
|
|
1161
|
+
for (const pkg of packages) {
|
|
1162
|
+
lines.push(formatPackageSummaryPlain(pkg));
|
|
1163
|
+
}
|
|
1164
|
+
const packagesWithDirs = packages.filter(
|
|
1165
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
1166
|
+
);
|
|
1167
|
+
if (packagesWithDirs.length > 0) {
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
lines.push("Structure:");
|
|
1170
|
+
for (const pkg of packagesWithDirs) {
|
|
1171
|
+
const groups = groupByRole(pkg.structure.directories);
|
|
1172
|
+
if (groups.length === 0) continue;
|
|
1173
|
+
lines.push(` ${pkg.relativePath}:`);
|
|
1174
|
+
for (const group of groups) {
|
|
1175
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1180
|
+
const pkgCount = packages.length > 1 ? packages.length : void 0;
|
|
1181
|
+
lines.push("");
|
|
1182
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1183
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1184
|
+
if (ext) {
|
|
1185
|
+
lines.push(ext);
|
|
1186
|
+
}
|
|
1187
|
+
lines.push(...formatRulesText(config));
|
|
1188
|
+
return lines.join("\n");
|
|
1189
|
+
}
|
|
1067
1190
|
|
|
1068
1191
|
// src/display.ts
|
|
1069
|
-
var CONVENTION_LABELS = {
|
|
1070
|
-
fileNaming: "File naming",
|
|
1071
|
-
componentNaming: "Component naming",
|
|
1072
|
-
hookNaming: "Hook naming",
|
|
1073
|
-
importAlias: "Import alias"
|
|
1074
|
-
};
|
|
1075
1192
|
function formatItem(item, nameMap) {
|
|
1076
1193
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1077
1194
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1087,7 +1204,7 @@ function displayConventions(scanResult) {
|
|
|
1087
1204
|
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1088
1205
|
if (conventionEntries.length === 0) return;
|
|
1089
1206
|
console.log(`
|
|
1090
|
-
${
|
|
1207
|
+
${chalk6.bold("Conventions:")}`);
|
|
1091
1208
|
for (const [key, convention] of conventionEntries) {
|
|
1092
1209
|
if (convention.confidence === "low") continue;
|
|
1093
1210
|
const label = CONVENTION_LABELS[key] ?? key;
|
|
@@ -1095,19 +1212,19 @@ ${chalk7.bold("Conventions:")}`);
|
|
|
1095
1212
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1096
1213
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1097
1214
|
if (allSame || pkgValues.length <= 1) {
|
|
1098
|
-
const ind = convention.confidence === "high" ?
|
|
1099
|
-
const detail =
|
|
1215
|
+
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1216
|
+
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1100
1217
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1101
1218
|
} else {
|
|
1102
|
-
console.log(` ${
|
|
1219
|
+
console.log(` ${chalk6.yellow("~")} ${label}: varies by package`);
|
|
1103
1220
|
for (const pv of pkgValues) {
|
|
1104
1221
|
const pct = Math.round(pv.convention.consistency);
|
|
1105
1222
|
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1106
1223
|
}
|
|
1107
1224
|
}
|
|
1108
1225
|
} else {
|
|
1109
|
-
const ind = convention.confidence === "high" ?
|
|
1110
|
-
const detail =
|
|
1226
|
+
const ind = convention.confidence === "high" ? chalk6.green("\u2713") : chalk6.yellow("~");
|
|
1227
|
+
const detail = chalk6.dim(`(${confidenceLabel(convention)})`);
|
|
1111
1228
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
1112
1229
|
}
|
|
1113
1230
|
}
|
|
@@ -1115,7 +1232,7 @@ ${chalk7.bold("Conventions:")}`);
|
|
|
1115
1232
|
function displaySummarySection(scanResult) {
|
|
1116
1233
|
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1117
1234
|
console.log(`
|
|
1118
|
-
${
|
|
1235
|
+
${chalk6.bold("Summary:")}`);
|
|
1119
1236
|
console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
|
|
1120
1237
|
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1121
1238
|
if (ext) {
|
|
@@ -1129,43 +1246,43 @@ function displayScanResults(scanResult) {
|
|
|
1129
1246
|
}
|
|
1130
1247
|
const { stack } = scanResult;
|
|
1131
1248
|
console.log(`
|
|
1132
|
-
${
|
|
1249
|
+
${chalk6.bold("Detected:")}`);
|
|
1133
1250
|
if (stack.framework) {
|
|
1134
|
-
console.log(` ${
|
|
1251
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES2)}`);
|
|
1135
1252
|
}
|
|
1136
|
-
console.log(` ${
|
|
1253
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1137
1254
|
if (stack.styling) {
|
|
1138
|
-
console.log(` ${
|
|
1255
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES2)}`);
|
|
1139
1256
|
}
|
|
1140
1257
|
if (stack.backend) {
|
|
1141
|
-
console.log(` ${
|
|
1258
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES2)}`);
|
|
1142
1259
|
}
|
|
1143
1260
|
if (stack.orm) {
|
|
1144
|
-
console.log(` ${
|
|
1261
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES)}`);
|
|
1145
1262
|
}
|
|
1146
1263
|
if (stack.linter) {
|
|
1147
|
-
console.log(` ${
|
|
1264
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1148
1265
|
}
|
|
1149
1266
|
if (stack.formatter) {
|
|
1150
|
-
console.log(` ${
|
|
1267
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1151
1268
|
}
|
|
1152
1269
|
if (stack.testRunner) {
|
|
1153
|
-
console.log(` ${
|
|
1270
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
1154
1271
|
}
|
|
1155
1272
|
if (stack.packageManager) {
|
|
1156
|
-
console.log(` ${
|
|
1273
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1157
1274
|
}
|
|
1158
1275
|
if (stack.libraries.length > 0) {
|
|
1159
1276
|
for (const lib of stack.libraries) {
|
|
1160
|
-
console.log(` ${
|
|
1277
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1161
1278
|
}
|
|
1162
1279
|
}
|
|
1163
1280
|
const groups = groupByRole(scanResult.structure.directories);
|
|
1164
1281
|
if (groups.length > 0) {
|
|
1165
1282
|
console.log(`
|
|
1166
|
-
${
|
|
1283
|
+
${chalk6.bold("Structure:")}`);
|
|
1167
1284
|
for (const group of groups) {
|
|
1168
|
-
console.log(` ${
|
|
1285
|
+
console.log(` ${chalk6.green("\u2713")} ${formatRoleGroup(group)}`);
|
|
1169
1286
|
}
|
|
1170
1287
|
}
|
|
1171
1288
|
displayConventions(scanResult);
|
|
@@ -1176,38 +1293,151 @@ function getConventionStr(cv) {
|
|
|
1176
1293
|
return typeof cv === "string" ? cv : cv.value;
|
|
1177
1294
|
}
|
|
1178
1295
|
function displayRulesPreview(config) {
|
|
1179
|
-
console.log(`${
|
|
1180
|
-
console.log(` ${
|
|
1296
|
+
console.log(`${chalk6.bold("Rules:")}`);
|
|
1297
|
+
console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1181
1298
|
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1182
1299
|
console.log(
|
|
1183
|
-
` ${
|
|
1300
|
+
` ${chalk6.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
|
|
1184
1301
|
);
|
|
1185
1302
|
} else if (config.rules.requireTests) {
|
|
1186
|
-
console.log(` ${
|
|
1303
|
+
console.log(` ${chalk6.dim("\u2022")} Require test files: yes`);
|
|
1187
1304
|
} else {
|
|
1188
|
-
console.log(` ${
|
|
1305
|
+
console.log(` ${chalk6.dim("\u2022")} Require test files: no`);
|
|
1189
1306
|
}
|
|
1190
1307
|
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1191
1308
|
console.log(
|
|
1192
|
-
` ${
|
|
1309
|
+
` ${chalk6.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
|
|
1193
1310
|
);
|
|
1194
1311
|
} else {
|
|
1195
|
-
console.log(` ${
|
|
1312
|
+
console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
|
|
1196
1313
|
}
|
|
1197
1314
|
console.log(
|
|
1198
|
-
` ${
|
|
1315
|
+
` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1199
1316
|
);
|
|
1200
1317
|
console.log("");
|
|
1201
1318
|
if (config.enforcement === "enforce") {
|
|
1202
|
-
console.log(`${
|
|
1319
|
+
console.log(`${chalk6.bold("Enforcement mode:")} enforce (violations will block commits)`);
|
|
1203
1320
|
} else {
|
|
1204
1321
|
console.log(
|
|
1205
|
-
`${
|
|
1322
|
+
`${chalk6.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
|
|
1206
1323
|
);
|
|
1207
1324
|
}
|
|
1208
1325
|
console.log("");
|
|
1209
1326
|
}
|
|
1210
1327
|
|
|
1328
|
+
// src/display-text.ts
|
|
1329
|
+
function getConventionStr2(cv) {
|
|
1330
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1331
|
+
}
|
|
1332
|
+
function plainConfidenceLabel(convention) {
|
|
1333
|
+
const pct = Math.round(convention.consistency);
|
|
1334
|
+
if (convention.confidence === "high") {
|
|
1335
|
+
return `${pct}%`;
|
|
1336
|
+
}
|
|
1337
|
+
return `${pct}%, suggested only`;
|
|
1338
|
+
}
|
|
1339
|
+
function formatConventionsText(scanResult) {
|
|
1340
|
+
const lines = [];
|
|
1341
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1342
|
+
if (conventionEntries.length === 0) return lines;
|
|
1343
|
+
lines.push("");
|
|
1344
|
+
lines.push("Conventions:");
|
|
1345
|
+
for (const [key, convention] of conventionEntries) {
|
|
1346
|
+
if (convention.confidence === "low") continue;
|
|
1347
|
+
const label = CONVENTION_LABELS2[key] ?? key;
|
|
1348
|
+
if (scanResult.packages.length > 1) {
|
|
1349
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1350
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1351
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1352
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1353
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1354
|
+
} else {
|
|
1355
|
+
lines.push(` ~ ${label}: varies by package`);
|
|
1356
|
+
for (const pv of pkgValues) {
|
|
1357
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1358
|
+
lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1363
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return lines;
|
|
1367
|
+
}
|
|
1368
|
+
function formatRulesText(config) {
|
|
1369
|
+
const lines = [];
|
|
1370
|
+
lines.push("");
|
|
1371
|
+
lines.push("Rules:");
|
|
1372
|
+
lines.push(` \u2022 Max file size: ${config.rules.maxFileLines} lines`);
|
|
1373
|
+
if (config.rules.requireTests && config.structure.testPattern) {
|
|
1374
|
+
lines.push(` \u2022 Require test files: yes (${config.structure.testPattern})`);
|
|
1375
|
+
} else if (config.rules.requireTests) {
|
|
1376
|
+
lines.push(" \u2022 Require test files: yes");
|
|
1377
|
+
} else {
|
|
1378
|
+
lines.push(" \u2022 Require test files: no");
|
|
1379
|
+
}
|
|
1380
|
+
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1381
|
+
lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
|
|
1382
|
+
} else {
|
|
1383
|
+
lines.push(" \u2022 Enforce file naming: no");
|
|
1384
|
+
}
|
|
1385
|
+
lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
|
|
1386
|
+
return lines;
|
|
1387
|
+
}
|
|
1388
|
+
function formatScanResultsText(scanResult, config) {
|
|
1389
|
+
if (scanResult.packages.length > 1) {
|
|
1390
|
+
return formatMonorepoResultsText(scanResult, config);
|
|
1391
|
+
}
|
|
1392
|
+
const lines = [];
|
|
1393
|
+
const { stack } = scanResult;
|
|
1394
|
+
lines.push("Detected:");
|
|
1395
|
+
if (stack.framework) {
|
|
1396
|
+
lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
|
|
1397
|
+
}
|
|
1398
|
+
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1399
|
+
if (stack.styling) {
|
|
1400
|
+
lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES3)}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (stack.backend) {
|
|
1403
|
+
lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
|
|
1404
|
+
}
|
|
1405
|
+
if (stack.orm) {
|
|
1406
|
+
lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES2)}`);
|
|
1407
|
+
}
|
|
1408
|
+
const secondaryParts = [];
|
|
1409
|
+
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
1410
|
+
if (stack.linter) secondaryParts.push(formatItem(stack.linter));
|
|
1411
|
+
if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
|
|
1412
|
+
if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
|
|
1413
|
+
if (secondaryParts.length > 0) {
|
|
1414
|
+
lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
|
|
1415
|
+
}
|
|
1416
|
+
if (stack.libraries.length > 0) {
|
|
1417
|
+
for (const lib of stack.libraries) {
|
|
1418
|
+
lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES2)}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1422
|
+
if (groups.length > 0) {
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
lines.push("Structure:");
|
|
1425
|
+
for (const group of groups) {
|
|
1426
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1430
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1431
|
+
lines.push("");
|
|
1432
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1433
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1434
|
+
if (ext) {
|
|
1435
|
+
lines.push(ext);
|
|
1436
|
+
}
|
|
1437
|
+
lines.push(...formatRulesText(config));
|
|
1438
|
+
return lines.join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1211
1441
|
// src/utils/write-generated-files.ts
|
|
1212
1442
|
import * as fs10 from "fs";
|
|
1213
1443
|
import * as path11 from "path";
|
|
@@ -1237,18 +1467,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1237
1467
|
// src/commands/init-hooks.ts
|
|
1238
1468
|
import * as fs11 from "fs";
|
|
1239
1469
|
import * as path12 from "path";
|
|
1240
|
-
import
|
|
1470
|
+
import chalk7 from "chalk";
|
|
1241
1471
|
function setupPreCommitHook(projectRoot) {
|
|
1242
1472
|
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1243
1473
|
if (fs11.existsSync(lefthookPath)) {
|
|
1244
1474
|
addLefthookPreCommit(lefthookPath);
|
|
1245
|
-
console.log(` ${
|
|
1475
|
+
console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
1246
1476
|
return;
|
|
1247
1477
|
}
|
|
1248
1478
|
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1249
1479
|
if (fs11.existsSync(huskyDir)) {
|
|
1250
1480
|
writeHuskyPreCommit(huskyDir);
|
|
1251
|
-
console.log(` ${
|
|
1481
|
+
console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
1252
1482
|
return;
|
|
1253
1483
|
}
|
|
1254
1484
|
const gitDir = path12.join(projectRoot, ".git");
|
|
@@ -1258,7 +1488,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
1258
1488
|
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
1259
1489
|
}
|
|
1260
1490
|
writeGitHookPreCommit(hooksDir);
|
|
1261
|
-
console.log(` ${
|
|
1491
|
+
console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
|
|
1262
1492
|
}
|
|
1263
1493
|
}
|
|
1264
1494
|
function writeGitHookPreCommit(hooksDir) {
|
|
@@ -1328,7 +1558,7 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1328
1558
|
settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
|
|
1329
1559
|
} catch {
|
|
1330
1560
|
console.warn(
|
|
1331
|
-
` ${
|
|
1561
|
+
` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
|
|
1332
1562
|
);
|
|
1333
1563
|
settings = {};
|
|
1334
1564
|
}
|
|
@@ -1337,7 +1567,17 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1337
1567
|
const existing = hooks.PostToolUse ?? [];
|
|
1338
1568
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1339
1569
|
const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
|
|
1340
|
-
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;
|
|
1341
1581
|
hooks.PostToolUse = [
|
|
1342
1582
|
...existing,
|
|
1343
1583
|
{
|
|
@@ -1353,7 +1593,19 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1353
1593
|
settings.hooks = hooks;
|
|
1354
1594
|
fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1355
1595
|
`);
|
|
1356
|
-
console.log(` ${
|
|
1596
|
+
console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
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`);
|
|
1357
1609
|
}
|
|
1358
1610
|
function writeHuskyPreCommit(huskyDir) {
|
|
1359
1611
|
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
@@ -1383,6 +1635,14 @@ function filterHighConfidence(conventions) {
|
|
|
1383
1635
|
}
|
|
1384
1636
|
return filtered;
|
|
1385
1637
|
}
|
|
1638
|
+
function getConventionStr3(cv) {
|
|
1639
|
+
if (!cv) return void 0;
|
|
1640
|
+
return typeof cv === "string" ? cv : cv.value;
|
|
1641
|
+
}
|
|
1642
|
+
function hasConventionOverrides(config) {
|
|
1643
|
+
if (!config.packages || config.packages.length === 0) return false;
|
|
1644
|
+
return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
|
|
1645
|
+
}
|
|
1386
1646
|
async function initCommand(options, cwd) {
|
|
1387
1647
|
const startDir = cwd ?? process.cwd();
|
|
1388
1648
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1392,84 +1652,146 @@ async function initCommand(options, cwd) {
|
|
|
1392
1652
|
);
|
|
1393
1653
|
}
|
|
1394
1654
|
const configPath = path13.join(projectRoot, CONFIG_FILE4);
|
|
1395
|
-
if (fs12.existsSync(configPath)) {
|
|
1655
|
+
if (fs12.existsSync(configPath) && !options.force) {
|
|
1396
1656
|
console.log(
|
|
1397
|
-
|
|
1657
|
+
`${chalk8.yellow("!")} viberails is already initialized.
|
|
1658
|
+
Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
|
|
1398
1659
|
);
|
|
1399
1660
|
return;
|
|
1400
1661
|
}
|
|
1401
|
-
console.log(chalk9.dim("Scanning project..."));
|
|
1402
|
-
const scanResult = await scan(projectRoot);
|
|
1403
|
-
const config = generateConfig(scanResult);
|
|
1404
1662
|
if (options.yes) {
|
|
1405
|
-
|
|
1663
|
+
console.log(chalk8.dim("Scanning project..."));
|
|
1664
|
+
const scanResult2 = await scan(projectRoot);
|
|
1665
|
+
const config2 = generateConfig(scanResult2);
|
|
1666
|
+
config2.conventions = filterHighConfidence(config2.conventions);
|
|
1667
|
+
displayScanResults(scanResult2);
|
|
1668
|
+
displayRulesPreview(config2);
|
|
1669
|
+
if (config2.workspace?.packages && config2.workspace.packages.length > 0) {
|
|
1670
|
+
console.log(chalk8.dim("Building import graph..."));
|
|
1671
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1672
|
+
const packages = resolveWorkspacePackages(projectRoot, config2.workspace);
|
|
1673
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1674
|
+
packages,
|
|
1675
|
+
ignore: config2.ignore
|
|
1676
|
+
});
|
|
1677
|
+
const inferred = inferBoundaries(graph);
|
|
1678
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1679
|
+
if (denyCount > 0) {
|
|
1680
|
+
config2.boundaries = inferred;
|
|
1681
|
+
config2.rules.enforceBoundaries = true;
|
|
1682
|
+
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
1686
|
+
`);
|
|
1687
|
+
writeGeneratedFiles(projectRoot, config2, scanResult2);
|
|
1688
|
+
updateGitignore(projectRoot);
|
|
1689
|
+
setupClaudeMdReference(projectRoot);
|
|
1690
|
+
console.log(`
|
|
1691
|
+
Created:`);
|
|
1692
|
+
console.log(` ${chalk8.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1693
|
+
console.log(` ${chalk8.green("\u2713")} .viberails/context.md`);
|
|
1694
|
+
console.log(` ${chalk8.green("\u2713")} .viberails/scan-result.json`);
|
|
1695
|
+
return;
|
|
1406
1696
|
}
|
|
1407
|
-
|
|
1697
|
+
clack2.intro("viberails");
|
|
1698
|
+
const s = clack2.spinner();
|
|
1699
|
+
s.start("Scanning project...");
|
|
1700
|
+
const scanResult = await scan(projectRoot);
|
|
1701
|
+
const config = generateConfig(scanResult);
|
|
1702
|
+
s.stop("Scan complete");
|
|
1408
1703
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1704
|
+
clack2.log.warn(
|
|
1705
|
+
"No source files detected. viberails will generate context\nwith minimal content. Run viberails sync after adding files."
|
|
1411
1706
|
);
|
|
1412
1707
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1708
|
+
const resultsText = formatScanResultsText(scanResult, config);
|
|
1709
|
+
clack2.note(resultsText, "Scan results");
|
|
1710
|
+
const decision = await promptInitDecision();
|
|
1711
|
+
if (decision === "customize") {
|
|
1712
|
+
clack2.note(
|
|
1713
|
+
"Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
|
|
1714
|
+
"Rules"
|
|
1715
|
+
);
|
|
1716
|
+
const overrides = await promptRuleCustomization({
|
|
1717
|
+
maxFileLines: config.rules.maxFileLines,
|
|
1718
|
+
requireTests: config.rules.requireTests,
|
|
1719
|
+
enforceNaming: config.rules.enforceNaming,
|
|
1720
|
+
enforcement: config.enforcement,
|
|
1721
|
+
fileNamingValue: getConventionStr3(config.conventions.fileNaming)
|
|
1722
|
+
});
|
|
1723
|
+
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1724
|
+
config.rules.requireTests = overrides.requireTests;
|
|
1725
|
+
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1726
|
+
config.enforcement = overrides.enforcement;
|
|
1727
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1728
|
+
clack2.note(
|
|
1729
|
+
'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
|
|
1730
|
+
"Per-package overrides"
|
|
1731
|
+
);
|
|
1419
1732
|
}
|
|
1420
1733
|
}
|
|
1421
|
-
if (config.workspace && config.workspace.packages.length > 0) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1734
|
+
if (config.workspace?.packages && config.workspace.packages.length > 0) {
|
|
1735
|
+
clack2.note(
|
|
1736
|
+
"Boundary rules prevent packages from importing where they\nshouldn't. viberails scans your existing imports and creates\nrules based on what's already working.",
|
|
1737
|
+
"Boundaries"
|
|
1738
|
+
);
|
|
1739
|
+
const shouldInfer = await confirm2("Infer boundary rules from import patterns?");
|
|
1427
1740
|
if (shouldInfer) {
|
|
1428
|
-
|
|
1741
|
+
const bs = clack2.spinner();
|
|
1742
|
+
bs.start("Building import graph...");
|
|
1429
1743
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1430
1744
|
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
1431
|
-
const graph = await buildImportGraph(projectRoot, {
|
|
1745
|
+
const graph = await buildImportGraph(projectRoot, {
|
|
1746
|
+
packages,
|
|
1747
|
+
ignore: config.ignore
|
|
1748
|
+
});
|
|
1432
1749
|
const inferred = inferBoundaries(graph);
|
|
1433
|
-
|
|
1750
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1751
|
+
if (denyCount > 0) {
|
|
1434
1752
|
config.boundaries = inferred;
|
|
1435
1753
|
config.rules.enforceBoundaries = true;
|
|
1436
|
-
|
|
1754
|
+
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
1755
|
+
} else {
|
|
1756
|
+
bs.stop("No boundary rules inferred");
|
|
1437
1757
|
}
|
|
1438
1758
|
}
|
|
1439
1759
|
}
|
|
1440
1760
|
const hookManager = detectHookManager(projectRoot);
|
|
1441
|
-
|
|
1442
|
-
if (
|
|
1443
|
-
|
|
1444
|
-
|
|
1761
|
+
const integrations = await promptIntegrations(hookManager);
|
|
1762
|
+
if (hasConventionOverrides(config)) {
|
|
1763
|
+
clack2.note(
|
|
1764
|
+
"Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
|
|
1765
|
+
"Per-package conventions"
|
|
1766
|
+
);
|
|
1445
1767
|
}
|
|
1446
1768
|
fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
1447
1769
|
`);
|
|
1448
1770
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1449
1771
|
updateGitignore(projectRoot);
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1772
|
+
const createdFiles = [
|
|
1773
|
+
CONFIG_FILE4,
|
|
1774
|
+
".viberails/context.md",
|
|
1775
|
+
".viberails/scan-result.json"
|
|
1776
|
+
];
|
|
1455
1777
|
if (integrations.preCommitHook) {
|
|
1456
1778
|
setupPreCommitHook(projectRoot);
|
|
1779
|
+
const hookMgr = detectHookManager(projectRoot);
|
|
1780
|
+
if (hookMgr) {
|
|
1781
|
+
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1782
|
+
}
|
|
1457
1783
|
}
|
|
1458
1784
|
if (integrations.claudeCodeHook) {
|
|
1459
1785
|
setupClaudeCodeHook(projectRoot);
|
|
1786
|
+
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1460
1787
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
];
|
|
1465
|
-
if (integrations.claudeCodeHook) {
|
|
1466
|
-
filesToCommit.push(chalk9.cyan(".claude/settings.json"));
|
|
1788
|
+
if (integrations.claudeMdRef) {
|
|
1789
|
+
setupClaudeMdReference(projectRoot);
|
|
1790
|
+
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1467
1791
|
}
|
|
1468
|
-
|
|
1469
|
-
${
|
|
1470
|
-
|
|
1471
|
-
console.log(` 2. Commit ${filesToCommit.join(", ")}`);
|
|
1472
|
-
console.log(` 3. Run ${chalk9.cyan("viberails check")} to verify your project passes`);
|
|
1792
|
+
clack2.log.success(`Created:
|
|
1793
|
+
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1794
|
+
clack2.outro("Done! Next: review viberails.config.json, then run viberails check");
|
|
1473
1795
|
}
|
|
1474
1796
|
function updateGitignore(projectRoot) {
|
|
1475
1797
|
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
@@ -1490,8 +1812,146 @@ import * as fs13 from "fs";
|
|
|
1490
1812
|
import * as path14 from "path";
|
|
1491
1813
|
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1492
1814
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1493
|
-
import
|
|
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
|
|
1494
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
|
+
}
|
|
1495
1955
|
async function syncCommand(cwd) {
|
|
1496
1956
|
const startDir = cwd ?? process.cwd();
|
|
1497
1957
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -1502,41 +1962,50 @@ async function syncCommand(cwd) {
|
|
|
1502
1962
|
}
|
|
1503
1963
|
const configPath = path14.join(projectRoot, CONFIG_FILE5);
|
|
1504
1964
|
const existing = await loadConfig4(configPath);
|
|
1505
|
-
|
|
1965
|
+
const previousStats = loadPreviousStats(projectRoot);
|
|
1966
|
+
console.log(chalk9.dim("Scanning project..."));
|
|
1506
1967
|
const scanResult = await scan2(projectRoot);
|
|
1507
1968
|
const merged = mergeConfig(existing, scanResult);
|
|
1508
1969
|
const existingJson = JSON.stringify(existing, null, 2);
|
|
1509
1970
|
const mergedJson = JSON.stringify(merged, null, 2);
|
|
1510
1971
|
const configChanged = existingJson !== mergedJson;
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
+
}
|
|
1515
1984
|
}
|
|
1516
1985
|
fs13.writeFileSync(configPath, `${mergedJson}
|
|
1517
1986
|
`);
|
|
1518
1987
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1519
1988
|
console.log(`
|
|
1520
|
-
${
|
|
1989
|
+
${chalk9.bold("Synced:")}`);
|
|
1521
1990
|
if (configChanged) {
|
|
1522
|
-
console.log(` ${
|
|
1991
|
+
console.log(` ${chalk9.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
|
|
1523
1992
|
} else {
|
|
1524
|
-
console.log(` ${
|
|
1993
|
+
console.log(` ${chalk9.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
|
|
1525
1994
|
}
|
|
1526
|
-
console.log(` ${
|
|
1527
|
-
console.log(` ${
|
|
1995
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1996
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1528
1997
|
}
|
|
1529
1998
|
|
|
1530
1999
|
// src/index.ts
|
|
1531
|
-
var VERSION = "0.3.
|
|
2000
|
+
var VERSION = "0.3.3";
|
|
1532
2001
|
var program = new Command();
|
|
1533
2002
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
1534
|
-
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) => {
|
|
1535
2004
|
try {
|
|
1536
2005
|
await initCommand(options);
|
|
1537
2006
|
} catch (err) {
|
|
1538
2007
|
const message = err instanceof Error ? err.message : String(err);
|
|
1539
|
-
console.error(`${
|
|
2008
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1540
2009
|
process.exit(1);
|
|
1541
2010
|
}
|
|
1542
2011
|
});
|
|
@@ -1545,7 +2014,7 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
1545
2014
|
await syncCommand();
|
|
1546
2015
|
} catch (err) {
|
|
1547
2016
|
const message = err instanceof Error ? err.message : String(err);
|
|
1548
|
-
console.error(`${
|
|
2017
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1549
2018
|
process.exit(1);
|
|
1550
2019
|
}
|
|
1551
2020
|
});
|
|
@@ -1560,7 +2029,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
1560
2029
|
process.exit(exitCode);
|
|
1561
2030
|
} catch (err) {
|
|
1562
2031
|
const message = err instanceof Error ? err.message : String(err);
|
|
1563
|
-
console.error(`${
|
|
2032
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1564
2033
|
process.exit(1);
|
|
1565
2034
|
}
|
|
1566
2035
|
}
|
|
@@ -1571,7 +2040,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
1571
2040
|
process.exit(exitCode);
|
|
1572
2041
|
} catch (err) {
|
|
1573
2042
|
const message = err instanceof Error ? err.message : String(err);
|
|
1574
|
-
console.error(`${
|
|
2043
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1575
2044
|
process.exit(1);
|
|
1576
2045
|
}
|
|
1577
2046
|
});
|
|
@@ -1580,7 +2049,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
1580
2049
|
await boundariesCommand(options);
|
|
1581
2050
|
} catch (err) {
|
|
1582
2051
|
const message = err instanceof Error ? err.message : String(err);
|
|
1583
|
-
console.error(`${
|
|
2052
|
+
console.error(`${chalk10.red("Error:")} ${message}`);
|
|
1584
2053
|
process.exit(1);
|
|
1585
2054
|
}
|
|
1586
2055
|
});
|