viberails 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1584 -645
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1577 -638
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk12 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
|
-
import { loadConfig } from "@viberails/config";
|
|
10
|
+
import { compactConfig, loadConfig } from "@viberails/config";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
|
|
13
13
|
// src/utils/find-project-root.ts
|
|
@@ -28,118 +28,485 @@ function findProjectRoot(startDir) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// src/utils/prompt.ts
|
|
31
|
+
import * as clack5 from "@clack/prompts";
|
|
32
|
+
|
|
33
|
+
// src/utils/prompt-integrations.ts
|
|
31
34
|
import * as clack from "@clack/prompts";
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
async function promptIntegrations(hookManager, tools) {
|
|
36
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook (git hook)";
|
|
37
|
+
const options = [
|
|
38
|
+
{
|
|
39
|
+
value: "preCommit",
|
|
40
|
+
label: hookLabel,
|
|
41
|
+
hint: "runs viberails checks when you commit"
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
if (tools?.isTypeScript) {
|
|
45
|
+
options.push({
|
|
46
|
+
value: "typecheck",
|
|
47
|
+
label: "Typecheck (tsc --noEmit)",
|
|
48
|
+
hint: "catches type errors before commit"
|
|
49
|
+
});
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
if (tools?.linter) {
|
|
52
|
+
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
53
|
+
options.push({
|
|
54
|
+
value: "lint",
|
|
55
|
+
label: `Lint check (${linterName})`,
|
|
56
|
+
hint: "runs linter on staged files before commit"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
options.push(
|
|
60
|
+
{
|
|
61
|
+
value: "claude",
|
|
62
|
+
label: "Claude Code hook",
|
|
63
|
+
hint: "checks files when Claude edits them"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: "claudeMd",
|
|
67
|
+
label: "CLAUDE.md reference",
|
|
68
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
value: "githubAction",
|
|
72
|
+
label: "GitHub Actions workflow",
|
|
73
|
+
hint: "blocks PRs that fail viberails check"
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
const initialValues = options.map((o) => o.value);
|
|
77
|
+
const result = await clack.multiselect({
|
|
78
|
+
message: "Set up integrations?",
|
|
79
|
+
options,
|
|
80
|
+
initialValues,
|
|
81
|
+
required: false
|
|
82
|
+
});
|
|
40
83
|
assertNotCancelled(result);
|
|
41
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
preCommitHook: result.includes("preCommit"),
|
|
86
|
+
claudeCodeHook: result.includes("claude"),
|
|
87
|
+
claudeMdRef: result.includes("claudeMd"),
|
|
88
|
+
githubAction: result.includes("githubAction"),
|
|
89
|
+
typecheckHook: result.includes("typecheck"),
|
|
90
|
+
lintHook: result.includes("lint")
|
|
91
|
+
};
|
|
42
92
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
93
|
+
|
|
94
|
+
// src/utils/prompt-rules.ts
|
|
95
|
+
import * as clack4 from "@clack/prompts";
|
|
96
|
+
|
|
97
|
+
// src/utils/prompt-menu-handlers.ts
|
|
98
|
+
import * as clack3 from "@clack/prompts";
|
|
99
|
+
|
|
100
|
+
// src/utils/prompt-package-overrides.ts
|
|
101
|
+
import * as clack2 from "@clack/prompts";
|
|
102
|
+
function normalizePackageOverrides(packages) {
|
|
103
|
+
for (const pkg of packages) {
|
|
104
|
+
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
105
|
+
delete pkg.rules;
|
|
106
|
+
}
|
|
107
|
+
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
108
|
+
delete pkg.coverage;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return packages;
|
|
47
112
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
113
|
+
function packageCoverageHint(pkg, defaults) {
|
|
114
|
+
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
115
|
+
const isExempt = coverage === 0;
|
|
116
|
+
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
117
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
118
|
+
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
119
|
+
const tags = [];
|
|
120
|
+
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
121
|
+
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
122
|
+
tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
|
|
123
|
+
if (hasSummaryOverride) tags.push("summary override");
|
|
124
|
+
if (hasCommandOverride) tags.push("command override");
|
|
125
|
+
return tags.join(", ");
|
|
126
|
+
}
|
|
127
|
+
async function promptPackageCoverageOverrides(packages, defaults) {
|
|
128
|
+
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
129
|
+
if (editablePackages.length === 0) return packages;
|
|
130
|
+
while (true) {
|
|
131
|
+
const selectedPath = await clack2.select({
|
|
132
|
+
message: "Select package to edit coverage overrides",
|
|
133
|
+
options: [
|
|
134
|
+
...editablePackages.map((pkg) => ({
|
|
135
|
+
value: pkg.path,
|
|
136
|
+
label: `${pkg.path} (${pkg.name})`,
|
|
137
|
+
hint: packageCoverageHint(pkg, defaults)
|
|
138
|
+
})),
|
|
139
|
+
{ value: "__done__", label: "Done" }
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
assertNotCancelled(selectedPath);
|
|
143
|
+
if (selectedPath === "__done__") break;
|
|
144
|
+
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
145
|
+
if (!target) continue;
|
|
146
|
+
while (true) {
|
|
147
|
+
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
148
|
+
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
149
|
+
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
|
|
150
|
+
const choice = await clack2.select({
|
|
151
|
+
message: `Edit coverage overrides for ${target.path}`,
|
|
152
|
+
options: [
|
|
153
|
+
{ value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
|
|
154
|
+
{ value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
|
|
155
|
+
{ value: "command", label: "coverage.command", hint: effectiveCommand },
|
|
156
|
+
{ value: "reset", label: "Reset this package to inherit defaults" },
|
|
157
|
+
{ value: "back", label: "Back to package list" }
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
assertNotCancelled(choice);
|
|
161
|
+
if (choice === "back") break;
|
|
162
|
+
if (choice === "testCoverage") {
|
|
163
|
+
const result = await clack2.text({
|
|
164
|
+
message: "Package testCoverage (0 to exempt package)?",
|
|
165
|
+
initialValue: String(effectiveCoverage),
|
|
166
|
+
validate: (v) => {
|
|
167
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
168
|
+
const n = Number.parseInt(v, 10);
|
|
169
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
assertNotCancelled(result);
|
|
173
|
+
const nextCoverage = Number.parseInt(result, 10);
|
|
174
|
+
if (nextCoverage === defaults.testCoverage) {
|
|
175
|
+
if (target.rules) {
|
|
176
|
+
delete target.rules.testCoverage;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (choice === "summaryPath") {
|
|
183
|
+
const result = await clack2.text({
|
|
184
|
+
message: "Package coverage.summaryPath (blank to inherit default)?",
|
|
185
|
+
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
186
|
+
placeholder: defaults.coverageSummaryPath
|
|
187
|
+
});
|
|
188
|
+
assertNotCancelled(result);
|
|
189
|
+
const value = result.trim();
|
|
190
|
+
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
191
|
+
if (target.coverage) {
|
|
192
|
+
delete target.coverage.summaryPath;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (choice === "command") {
|
|
199
|
+
const result = await clack2.text({
|
|
200
|
+
message: "Package coverage.command (blank to inherit default/auto)?",
|
|
201
|
+
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
202
|
+
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
203
|
+
});
|
|
204
|
+
assertNotCancelled(result);
|
|
205
|
+
const value = result.trim();
|
|
206
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
207
|
+
if (value.length === 0 || value === defaultCommand) {
|
|
208
|
+
if (target.coverage) {
|
|
209
|
+
delete target.coverage.command;
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (choice === "reset") {
|
|
216
|
+
if (target.rules) {
|
|
217
|
+
delete target.rules.testCoverage;
|
|
218
|
+
}
|
|
219
|
+
delete target.coverage;
|
|
220
|
+
}
|
|
221
|
+
normalizePackageOverrides(editablePackages);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return normalizePackageOverrides(packages);
|
|
58
225
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
226
|
+
|
|
227
|
+
// src/utils/prompt-menu-handlers.ts
|
|
228
|
+
function getPackageDiffs(pkg, root) {
|
|
229
|
+
const diffs = [];
|
|
230
|
+
const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
|
|
231
|
+
for (const key of convKeys) {
|
|
232
|
+
if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
|
|
233
|
+
diffs.push(`${key}: ${pkg.conventions[key]}`);
|
|
67
234
|
}
|
|
235
|
+
}
|
|
236
|
+
const stackKeys = [
|
|
237
|
+
"framework",
|
|
238
|
+
"language",
|
|
239
|
+
"styling",
|
|
240
|
+
"backend",
|
|
241
|
+
"orm",
|
|
242
|
+
"linter",
|
|
243
|
+
"formatter",
|
|
244
|
+
"testRunner",
|
|
245
|
+
"packageManager"
|
|
246
|
+
];
|
|
247
|
+
for (const key of stackKeys) {
|
|
248
|
+
if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
|
|
249
|
+
diffs.push(`${key}: ${pkg.stack[key]}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
253
|
+
diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
|
|
254
|
+
}
|
|
255
|
+
if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
|
|
256
|
+
diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
|
|
257
|
+
}
|
|
258
|
+
if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
|
|
259
|
+
diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
|
|
260
|
+
}
|
|
261
|
+
if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
|
|
262
|
+
diffs.push("coverage.command: (override)");
|
|
263
|
+
}
|
|
264
|
+
return diffs;
|
|
265
|
+
}
|
|
266
|
+
function buildMenuOptions(state, packageCount) {
|
|
267
|
+
const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
|
|
268
|
+
const options = [
|
|
269
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
270
|
+
{ value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
|
|
271
|
+
];
|
|
272
|
+
if (state.fileNamingValue) {
|
|
273
|
+
options.push({
|
|
274
|
+
value: "fileNaming",
|
|
275
|
+
label: "File naming convention",
|
|
276
|
+
hint: state.fileNamingValue
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
options.push({
|
|
280
|
+
value: "testCoverage",
|
|
281
|
+
label: "Test coverage target",
|
|
282
|
+
hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
|
|
68
283
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
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
|
|
284
|
+
options.push({
|
|
285
|
+
value: "enforceMissingTests",
|
|
286
|
+
label: "Enforce missing tests",
|
|
287
|
+
hint: state.enforceMissingTests ? "yes" : "no"
|
|
79
288
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
message: "Enforcement mode",
|
|
83
|
-
options: [
|
|
289
|
+
if (state.testCoverage > 0) {
|
|
290
|
+
options.push(
|
|
84
291
|
{
|
|
85
|
-
value: "
|
|
86
|
-
label: "
|
|
87
|
-
hint:
|
|
292
|
+
value: "coverageSummaryPath",
|
|
293
|
+
label: "Coverage summary path",
|
|
294
|
+
hint: state.coverageSummaryPath
|
|
88
295
|
},
|
|
89
296
|
{
|
|
90
|
-
value: "
|
|
91
|
-
label: "
|
|
92
|
-
hint: "
|
|
297
|
+
value: "coverageCommand",
|
|
298
|
+
label: "Coverage command",
|
|
299
|
+
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
93
300
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
301
|
+
);
|
|
302
|
+
if (packageCount > 0) {
|
|
303
|
+
options.push({
|
|
304
|
+
value: "packageOverrides",
|
|
305
|
+
label: "Per-package coverage overrides",
|
|
306
|
+
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
options.push(
|
|
311
|
+
{ value: "reset", label: "Reset all to detected defaults" },
|
|
312
|
+
{ value: "done", label: "Done" }
|
|
313
|
+
);
|
|
314
|
+
return options;
|
|
315
|
+
}
|
|
316
|
+
function clonePackages(packages) {
|
|
317
|
+
return packages?.map((pkg) => ({
|
|
318
|
+
...pkg,
|
|
319
|
+
stack: pkg.stack ? { ...pkg.stack } : void 0,
|
|
320
|
+
structure: pkg.structure ? { ...pkg.structure } : void 0,
|
|
321
|
+
conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
|
|
322
|
+
rules: pkg.rules ? { ...pkg.rules } : void 0,
|
|
323
|
+
coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
|
|
324
|
+
ignore: pkg.ignore ? [...pkg.ignore] : void 0,
|
|
325
|
+
boundaries: pkg.boundaries ? {
|
|
326
|
+
deny: [...pkg.boundaries.deny],
|
|
327
|
+
ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
|
|
328
|
+
} : void 0
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
async function handleMenuChoice(choice, state, defaults, root) {
|
|
332
|
+
if (choice === "reset") {
|
|
333
|
+
state.maxFileLines = defaults.maxFileLines;
|
|
334
|
+
state.testCoverage = defaults.testCoverage;
|
|
335
|
+
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
336
|
+
state.enforceNaming = defaults.enforceNaming;
|
|
337
|
+
state.fileNamingValue = defaults.fileNamingValue;
|
|
338
|
+
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
339
|
+
state.coverageCommand = defaults.coverageCommand;
|
|
340
|
+
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
341
|
+
clack3.log.info("Reset all rules to detected defaults.");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (choice === "packageOverrides") {
|
|
345
|
+
if (state.packageOverrides) {
|
|
346
|
+
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
347
|
+
state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
|
|
348
|
+
testCoverage: state.testCoverage,
|
|
349
|
+
coverageSummaryPath: state.coverageSummaryPath,
|
|
350
|
+
coverageCommand: state.coverageCommand
|
|
351
|
+
});
|
|
352
|
+
const lines = packageDiffs.map((entry) => `${entry.pkg.path}
|
|
353
|
+
${entry.diffs.join(", ")}`);
|
|
354
|
+
if (lines.length > 0) {
|
|
355
|
+
clack3.note(lines.join("\n\n"), "Existing package differences");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (choice === "maxFileLines") {
|
|
361
|
+
const result = await clack3.text({
|
|
362
|
+
message: "Maximum lines per source file?",
|
|
363
|
+
initialValue: String(state.maxFileLines),
|
|
364
|
+
validate: (v) => {
|
|
365
|
+
if (typeof v !== "string") return "Enter a positive number";
|
|
366
|
+
const n = Number.parseInt(v, 10);
|
|
367
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
assertNotCancelled(result);
|
|
371
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
372
|
+
}
|
|
373
|
+
if (choice === "enforceMissingTests") {
|
|
374
|
+
const result = await clack3.confirm({
|
|
375
|
+
message: "Require every source file to have a corresponding test file?",
|
|
376
|
+
initialValue: state.enforceMissingTests
|
|
377
|
+
});
|
|
378
|
+
assertNotCancelled(result);
|
|
379
|
+
state.enforceMissingTests = result;
|
|
380
|
+
}
|
|
381
|
+
if (choice === "testCoverage") {
|
|
382
|
+
const result = await clack3.text({
|
|
383
|
+
message: "Test coverage target (0 disables coverage checks)?",
|
|
384
|
+
initialValue: String(state.testCoverage),
|
|
385
|
+
validate: (v) => {
|
|
386
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
387
|
+
const n = Number.parseInt(v, 10);
|
|
388
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
assertNotCancelled(result);
|
|
392
|
+
state.testCoverage = Number.parseInt(result, 10);
|
|
393
|
+
}
|
|
394
|
+
if (choice === "coverageSummaryPath") {
|
|
395
|
+
const result = await clack3.text({
|
|
396
|
+
message: "Coverage summary path (relative to package root)?",
|
|
397
|
+
initialValue: state.coverageSummaryPath,
|
|
398
|
+
validate: (v) => {
|
|
399
|
+
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
assertNotCancelled(result);
|
|
403
|
+
state.coverageSummaryPath = result.trim();
|
|
404
|
+
}
|
|
405
|
+
if (choice === "coverageCommand") {
|
|
406
|
+
const result = await clack3.text({
|
|
407
|
+
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
408
|
+
initialValue: state.coverageCommand ?? "",
|
|
409
|
+
placeholder: "(auto-detect from package.json test runner)"
|
|
410
|
+
});
|
|
411
|
+
assertNotCancelled(result);
|
|
412
|
+
const trimmed = result.trim();
|
|
413
|
+
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
414
|
+
}
|
|
415
|
+
if (choice === "enforceNaming") {
|
|
416
|
+
const result = await clack3.confirm({
|
|
417
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
418
|
+
initialValue: state.enforceNaming
|
|
419
|
+
});
|
|
420
|
+
assertNotCancelled(result);
|
|
421
|
+
state.enforceNaming = result;
|
|
422
|
+
}
|
|
423
|
+
if (choice === "fileNaming") {
|
|
424
|
+
const selected = await clack3.select({
|
|
425
|
+
message: "Which file naming convention should be enforced?",
|
|
426
|
+
options: [
|
|
427
|
+
{ value: "kebab-case", label: "kebab-case" },
|
|
428
|
+
{ value: "camelCase", label: "camelCase" },
|
|
429
|
+
{ value: "PascalCase", label: "PascalCase" },
|
|
430
|
+
{ value: "snake_case", label: "snake_case" }
|
|
431
|
+
],
|
|
432
|
+
initialValue: state.fileNamingValue
|
|
433
|
+
});
|
|
434
|
+
assertNotCancelled(selected);
|
|
435
|
+
state.fileNamingValue = selected;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/utils/prompt-rules.ts
|
|
440
|
+
function getRootPackage(packages) {
|
|
441
|
+
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
442
|
+
}
|
|
443
|
+
async function promptRuleMenu(defaults) {
|
|
444
|
+
const state = {
|
|
445
|
+
...defaults,
|
|
446
|
+
packageOverrides: clonePackages(defaults.packageOverrides)
|
|
447
|
+
};
|
|
448
|
+
const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
|
|
449
|
+
const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
|
|
450
|
+
while (true) {
|
|
451
|
+
const options = buildMenuOptions(state, packageCount);
|
|
452
|
+
const choice = await clack4.select({ message: "Customize rules", options });
|
|
453
|
+
assertNotCancelled(choice);
|
|
454
|
+
if (choice === "done") break;
|
|
455
|
+
await handleMenuChoice(choice, state, defaults, root);
|
|
456
|
+
}
|
|
98
457
|
return {
|
|
99
|
-
maxFileLines:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
458
|
+
maxFileLines: state.maxFileLines,
|
|
459
|
+
testCoverage: state.testCoverage,
|
|
460
|
+
enforceMissingTests: state.enforceMissingTests,
|
|
461
|
+
enforceNaming: state.enforceNaming,
|
|
462
|
+
fileNamingValue: state.fileNamingValue,
|
|
463
|
+
coverageSummaryPath: state.coverageSummaryPath,
|
|
464
|
+
coverageCommand: state.coverageCommand,
|
|
465
|
+
packageOverrides: state.packageOverrides
|
|
103
466
|
};
|
|
104
467
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
468
|
+
|
|
469
|
+
// src/utils/prompt.ts
|
|
470
|
+
function assertNotCancelled(value) {
|
|
471
|
+
if (clack5.isCancel(value)) {
|
|
472
|
+
clack5.cancel("Setup cancelled.");
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function confirm3(message) {
|
|
477
|
+
const result = await clack5.confirm({ message, initialValue: true });
|
|
478
|
+
assertNotCancelled(result);
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
async function confirmDangerous(message) {
|
|
482
|
+
const result = await clack5.confirm({ message, initialValue: false });
|
|
483
|
+
assertNotCancelled(result);
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
async function promptInitDecision() {
|
|
487
|
+
const result = await clack5.select({
|
|
488
|
+
message: "Accept these rules?",
|
|
109
489
|
options: [
|
|
110
490
|
{
|
|
111
|
-
value: "
|
|
112
|
-
label:
|
|
113
|
-
hint: "
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
value: "claude",
|
|
117
|
-
label: "Claude Code hook",
|
|
118
|
-
hint: "checks files when Claude edits them"
|
|
491
|
+
value: "accept",
|
|
492
|
+
label: "Yes, looks good",
|
|
493
|
+
hint: "warns on violation; use --enforce in CI to block"
|
|
119
494
|
},
|
|
120
|
-
{
|
|
121
|
-
|
|
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
|
|
495
|
+
{ value: "customize", label: "Let me customize rules" }
|
|
496
|
+
]
|
|
128
497
|
});
|
|
129
498
|
assertNotCancelled(result);
|
|
130
|
-
return
|
|
131
|
-
preCommitHook: result.includes("preCommit"),
|
|
132
|
-
claudeCodeHook: result.includes("claude"),
|
|
133
|
-
claudeMdRef: result.includes("claudeMd")
|
|
134
|
-
};
|
|
499
|
+
return result;
|
|
135
500
|
}
|
|
136
501
|
|
|
137
502
|
// src/utils/resolve-workspace-packages.ts
|
|
138
503
|
import * as fs2 from "fs";
|
|
139
504
|
import * as path2 from "path";
|
|
140
|
-
function resolveWorkspacePackages(projectRoot,
|
|
141
|
-
const
|
|
142
|
-
for (const
|
|
505
|
+
function resolveWorkspacePackages(projectRoot, packages) {
|
|
506
|
+
const resolved = [];
|
|
507
|
+
for (const pkgConfig of packages) {
|
|
508
|
+
if (pkgConfig.path === ".") continue;
|
|
509
|
+
const relativePath = pkgConfig.path;
|
|
143
510
|
const absPath = path2.join(projectRoot, relativePath);
|
|
144
511
|
const pkgJsonPath = path2.join(absPath, "package.json");
|
|
145
512
|
if (!fs2.existsSync(pkgJsonPath)) continue;
|
|
@@ -155,13 +522,13 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
155
522
|
...Object.keys(pkg.dependencies ?? {}),
|
|
156
523
|
...Object.keys(pkg.devDependencies ?? {})
|
|
157
524
|
];
|
|
158
|
-
|
|
525
|
+
resolved.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
159
526
|
}
|
|
160
|
-
const packageNames = new Set(
|
|
161
|
-
for (const pkg of
|
|
527
|
+
const packageNames = new Set(resolved.map((p) => p.name));
|
|
528
|
+
for (const pkg of resolved) {
|
|
162
529
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
163
530
|
}
|
|
164
|
-
return
|
|
531
|
+
return resolved;
|
|
165
532
|
}
|
|
166
533
|
|
|
167
534
|
// src/commands/boundaries.ts
|
|
@@ -212,7 +579,7 @@ Enforcement: ${config.rules.enforceBoundaries ? chalk.green("on") : chalk.yellow
|
|
|
212
579
|
async function inferAndDisplay(projectRoot, config, configPath) {
|
|
213
580
|
console.log(chalk.dim("Analyzing imports..."));
|
|
214
581
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
215
|
-
const packages = config.
|
|
582
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
216
583
|
const graph = await buildImportGraph(projectRoot, {
|
|
217
584
|
packages,
|
|
218
585
|
ignore: config.ignore
|
|
@@ -236,11 +603,11 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
236
603
|
console.log(`
|
|
237
604
|
${totalRules} denied`);
|
|
238
605
|
console.log("");
|
|
239
|
-
const shouldSave = await
|
|
606
|
+
const shouldSave = await confirm3("Save to viberails.config.json?");
|
|
240
607
|
if (shouldSave) {
|
|
241
608
|
config.boundaries = inferred;
|
|
242
609
|
config.rules.enforceBoundaries = true;
|
|
243
|
-
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
610
|
+
fs3.writeFileSync(configPath, `${JSON.stringify(compactConfig(config), null, 2)}
|
|
244
611
|
`);
|
|
245
612
|
console.log(`${chalk.green("\u2713")} Saved ${totalRules} rules`);
|
|
246
613
|
}
|
|
@@ -248,7 +615,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
248
615
|
async function showGraph(projectRoot, config) {
|
|
249
616
|
console.log(chalk.dim("Building import graph..."));
|
|
250
617
|
const { buildImportGraph } = await import("@viberails/graph");
|
|
251
|
-
const packages = config.
|
|
618
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
252
619
|
const graph = await buildImportGraph(projectRoot, {
|
|
253
620
|
packages,
|
|
254
621
|
ignore: config.ignore
|
|
@@ -276,42 +643,191 @@ ${chalk.yellow("Cycles detected:")}`);
|
|
|
276
643
|
}
|
|
277
644
|
|
|
278
645
|
// src/commands/check.ts
|
|
279
|
-
import * as
|
|
280
|
-
import * as
|
|
646
|
+
import * as fs7 from "fs";
|
|
647
|
+
import * as path7 from "path";
|
|
281
648
|
import { loadConfig as loadConfig2 } from "@viberails/config";
|
|
282
649
|
import chalk2 from "chalk";
|
|
283
650
|
|
|
284
651
|
// src/commands/check-config.ts
|
|
652
|
+
import { BUILTIN_IGNORE } from "@viberails/config";
|
|
285
653
|
function resolveConfigForFile(relPath, config) {
|
|
286
|
-
if (!config.packages || config.packages.length === 0) {
|
|
287
|
-
return { rules: config.rules, conventions: config.conventions };
|
|
288
|
-
}
|
|
289
654
|
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
290
655
|
for (const pkg of sortedPackages) {
|
|
656
|
+
if (pkg.path === ".") continue;
|
|
291
657
|
if (relPath.startsWith(`${pkg.path}/`) || relPath === pkg.path) {
|
|
292
658
|
return {
|
|
293
659
|
rules: { ...config.rules, ...pkg.rules },
|
|
294
|
-
conventions:
|
|
660
|
+
conventions: pkg.conventions ?? {},
|
|
661
|
+
coverage: {
|
|
662
|
+
...config.defaults?.coverage ?? {},
|
|
663
|
+
...pkg.coverage ?? {}
|
|
664
|
+
}
|
|
295
665
|
};
|
|
296
666
|
}
|
|
297
667
|
}
|
|
298
|
-
|
|
668
|
+
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
669
|
+
return {
|
|
670
|
+
rules: { ...config.rules, ...root.rules },
|
|
671
|
+
conventions: root.conventions ?? {},
|
|
672
|
+
coverage: {
|
|
673
|
+
...config.defaults?.coverage ?? {},
|
|
674
|
+
...root.coverage ?? {}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function getEffectiveIgnore(config) {
|
|
679
|
+
return [...BUILTIN_IGNORE, ...config.ignore ?? []];
|
|
299
680
|
}
|
|
300
681
|
function resolveIgnoreForFile(relPath, config) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
682
|
+
const base = getEffectiveIgnore(config);
|
|
683
|
+
const root = config.packages.find((p) => p.path === ".");
|
|
684
|
+
const withRoot = root?.ignore ? [...base, ...root.ignore] : base;
|
|
685
|
+
const matched = [...config.packages].filter((p) => p.path !== ".").sort((a, b) => b.path.length - a.path.length).find((p) => relPath.startsWith(`${p.path}/`) || relPath === p.path);
|
|
686
|
+
if (matched?.ignore) {
|
|
687
|
+
return [...withRoot, ...matched.ignore];
|
|
688
|
+
}
|
|
689
|
+
return withRoot;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/commands/check-coverage.ts
|
|
693
|
+
import { spawnSync } from "child_process";
|
|
694
|
+
import * as fs4 from "fs";
|
|
695
|
+
import * as path4 from "path";
|
|
696
|
+
var DEFAULT_SUMMARY_PATH = "coverage/coverage-summary.json";
|
|
697
|
+
function packageRoot(projectRoot, pkg) {
|
|
698
|
+
return pkg.path === "." ? projectRoot : path4.join(projectRoot, pkg.path);
|
|
699
|
+
}
|
|
700
|
+
function resolveForPackage(config, pkg) {
|
|
701
|
+
return {
|
|
702
|
+
pkg,
|
|
703
|
+
rules: { ...config.rules, ...pkg.rules },
|
|
704
|
+
coverage: {
|
|
705
|
+
...config.defaults?.coverage ?? {},
|
|
706
|
+
...pkg.coverage ?? {}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
function resolveCoveragePackages(projectRoot, config, filesToCheck, staged) {
|
|
711
|
+
if (!staged) {
|
|
712
|
+
return config.packages.map((pkg) => resolveForPackage(config, pkg));
|
|
713
|
+
}
|
|
714
|
+
const matched = /* @__PURE__ */ new Map();
|
|
715
|
+
for (const raw of filesToCheck) {
|
|
716
|
+
const relPath = path4.isAbsolute(raw) ? path4.relative(projectRoot, raw) : raw;
|
|
717
|
+
const sorted = [...config.packages].filter((pkg2) => pkg2.path !== ".").sort((a, b) => b.path.length - a.path.length);
|
|
718
|
+
const pkg = sorted.find(
|
|
719
|
+
(candidate) => relPath.startsWith(`${candidate.path}/`) || relPath === candidate.path
|
|
720
|
+
) ?? config.packages.find((candidate) => candidate.path === ".") ?? config.packages[0];
|
|
721
|
+
matched.set(pkg.path, resolveForPackage(config, pkg));
|
|
722
|
+
}
|
|
723
|
+
return [...matched.values()];
|
|
724
|
+
}
|
|
725
|
+
function readCoveragePercentage(summaryPath) {
|
|
726
|
+
try {
|
|
727
|
+
const parsed = JSON.parse(fs4.readFileSync(summaryPath, "utf-8"));
|
|
728
|
+
const pct = parsed.total?.lines?.pct;
|
|
729
|
+
return typeof pct === "number" ? pct : void 0;
|
|
730
|
+
} catch {
|
|
731
|
+
return void 0;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function runCoverageCommand(pkgRoot, command) {
|
|
735
|
+
const result = spawnSync(command, {
|
|
736
|
+
cwd: pkgRoot,
|
|
737
|
+
shell: true,
|
|
738
|
+
encoding: "utf-8",
|
|
739
|
+
stdio: "pipe"
|
|
740
|
+
});
|
|
741
|
+
if (result.status === 0) return { ok: true };
|
|
742
|
+
const stderr = result.stderr?.trim() ?? "";
|
|
743
|
+
const stdout = result.stdout?.trim() ?? "";
|
|
744
|
+
const raw = stderr || stdout || `exit code ${result.status ?? 1}`;
|
|
745
|
+
if (raw.includes("coverage-v8") || raw.includes("coverage-istanbul") || raw.includes("MISSING DEP")) {
|
|
746
|
+
return {
|
|
747
|
+
ok: false,
|
|
748
|
+
detail: "Missing coverage provider. Install with: npm install -D @vitest/coverage-v8"
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const detail = raw.replace(/\x1B\[[0-9;]*m/g, "");
|
|
752
|
+
return { ok: false, detail };
|
|
753
|
+
}
|
|
754
|
+
function violationFilePath(projectRoot, pkgRoot, summaryPath) {
|
|
755
|
+
return path4.relative(projectRoot, path4.join(pkgRoot, summaryPath));
|
|
756
|
+
}
|
|
757
|
+
function pushViolation(violations, file, message, severity) {
|
|
758
|
+
violations.push({
|
|
759
|
+
file,
|
|
760
|
+
rule: "test-coverage",
|
|
761
|
+
message,
|
|
762
|
+
severity
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
766
|
+
const severity = options.enforce ? "error" : "warn";
|
|
767
|
+
const targets = resolveCoveragePackages(
|
|
768
|
+
projectRoot,
|
|
769
|
+
config,
|
|
770
|
+
filesToCheck,
|
|
771
|
+
options.staged === true
|
|
772
|
+
);
|
|
773
|
+
const violations = [];
|
|
774
|
+
for (const target of targets) {
|
|
775
|
+
if (target.rules.testCoverage <= 0) continue;
|
|
776
|
+
const pkgRoot = packageRoot(projectRoot, target.pkg);
|
|
777
|
+
const summaryPath = target.coverage.summaryPath ?? DEFAULT_SUMMARY_PATH;
|
|
778
|
+
const summaryAbs = path4.join(pkgRoot, summaryPath);
|
|
779
|
+
const summaryRel = violationFilePath(projectRoot, pkgRoot, summaryPath);
|
|
780
|
+
let pct = readCoveragePercentage(summaryAbs);
|
|
781
|
+
if (pct === void 0 && !options.staged) {
|
|
782
|
+
const command = target.coverage.command;
|
|
783
|
+
if (!command) {
|
|
784
|
+
const pkgLabel = target.pkg.path === "." ? "root package" : target.pkg.path;
|
|
785
|
+
pushViolation(
|
|
786
|
+
violations,
|
|
787
|
+
summaryRel,
|
|
788
|
+
`No coverage summary found for "${pkgLabel}". Run your test suite with coverage enabled, or set defaults.coverage.command in viberails.config.json.`,
|
|
789
|
+
severity
|
|
790
|
+
);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const run = runCoverageCommand(pkgRoot, command);
|
|
794
|
+
if (!run.ok) {
|
|
795
|
+
pushViolation(
|
|
796
|
+
violations,
|
|
797
|
+
summaryRel,
|
|
798
|
+
`Failed to run coverage command: ${run.detail}.`,
|
|
799
|
+
severity
|
|
800
|
+
);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
pct = readCoveragePercentage(summaryAbs);
|
|
804
|
+
}
|
|
805
|
+
if (pct === void 0) {
|
|
806
|
+
pushViolation(
|
|
807
|
+
violations,
|
|
808
|
+
summaryRel,
|
|
809
|
+
`Coverage summary not found or invalid at \`${summaryPath}\`.`,
|
|
810
|
+
severity
|
|
811
|
+
);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (pct < target.rules.testCoverage) {
|
|
815
|
+
pushViolation(
|
|
816
|
+
violations,
|
|
817
|
+
summaryRel,
|
|
818
|
+
`Line coverage ${pct.toFixed(1)}% is below required ${target.rules.testCoverage}%.`,
|
|
819
|
+
severity
|
|
820
|
+
);
|
|
306
821
|
}
|
|
307
822
|
}
|
|
308
|
-
return
|
|
823
|
+
return violations;
|
|
309
824
|
}
|
|
310
825
|
|
|
311
826
|
// src/commands/check-files.ts
|
|
312
827
|
import { execSync } from "child_process";
|
|
313
|
-
import * as
|
|
314
|
-
import * as
|
|
828
|
+
import * as fs5 from "fs";
|
|
829
|
+
import * as path5 from "path";
|
|
830
|
+
import { BUILTIN_IGNORE as BUILTIN_IGNORE2 } from "@viberails/config";
|
|
315
831
|
import picomatch from "picomatch";
|
|
316
832
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
317
833
|
"node_modules",
|
|
@@ -354,7 +870,7 @@ function isIgnored(relPath, ignorePatterns) {
|
|
|
354
870
|
}
|
|
355
871
|
function countFileLines(filePath) {
|
|
356
872
|
try {
|
|
357
|
-
const content =
|
|
873
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
358
874
|
if (content.length === 0) return 0;
|
|
359
875
|
let count = 1;
|
|
360
876
|
for (let i = 0; i < content.length; i++) {
|
|
@@ -366,14 +882,14 @@ function countFileLines(filePath) {
|
|
|
366
882
|
}
|
|
367
883
|
}
|
|
368
884
|
function checkNaming(relPath, conventions) {
|
|
369
|
-
const filename =
|
|
370
|
-
const ext =
|
|
885
|
+
const filename = path5.basename(relPath);
|
|
886
|
+
const ext = path5.extname(filename);
|
|
371
887
|
if (!SOURCE_EXTS.has(ext)) return void 0;
|
|
372
888
|
if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".") || filename.startsWith("_") || filename.startsWith("+") || filename.startsWith("$") || filename.startsWith("[")) {
|
|
373
889
|
return void 0;
|
|
374
890
|
}
|
|
375
891
|
const bare = filename.slice(0, filename.indexOf("."));
|
|
376
|
-
const convention =
|
|
892
|
+
const convention = conventions.fileNaming;
|
|
377
893
|
if (!convention) return void 0;
|
|
378
894
|
const pattern = NAMING_PATTERNS[convention];
|
|
379
895
|
if (!pattern || pattern.test(bare)) return void 0;
|
|
@@ -383,33 +899,55 @@ function getStagedFiles(projectRoot) {
|
|
|
383
899
|
try {
|
|
384
900
|
const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
385
901
|
cwd: projectRoot,
|
|
386
|
-
encoding: "utf-8"
|
|
902
|
+
encoding: "utf-8",
|
|
903
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
387
904
|
});
|
|
388
905
|
return output.trim().split("\n").filter(Boolean);
|
|
389
906
|
} catch {
|
|
390
907
|
return [];
|
|
391
908
|
}
|
|
392
909
|
}
|
|
910
|
+
function getDiffFiles(projectRoot, base) {
|
|
911
|
+
try {
|
|
912
|
+
const allOutput = execSync(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
|
|
913
|
+
cwd: projectRoot,
|
|
914
|
+
encoding: "utf-8",
|
|
915
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
916
|
+
});
|
|
917
|
+
const addedOutput = execSync(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
|
|
918
|
+
cwd: projectRoot,
|
|
919
|
+
encoding: "utf-8",
|
|
920
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
921
|
+
});
|
|
922
|
+
return {
|
|
923
|
+
all: allOutput.trim().split("\n").filter(Boolean),
|
|
924
|
+
added: addedOutput.trim().split("\n").filter(Boolean)
|
|
925
|
+
};
|
|
926
|
+
} catch {
|
|
927
|
+
return { all: [], added: [] };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
393
930
|
function getAllSourceFiles(projectRoot, config) {
|
|
931
|
+
const effectiveIgnore = [...BUILTIN_IGNORE2, ...config.ignore ?? []];
|
|
394
932
|
const files = [];
|
|
395
933
|
const walk = (dir) => {
|
|
396
934
|
let entries;
|
|
397
935
|
try {
|
|
398
|
-
entries =
|
|
936
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
399
937
|
} catch {
|
|
400
938
|
return;
|
|
401
939
|
}
|
|
402
940
|
for (const entry of entries) {
|
|
403
|
-
const rel =
|
|
941
|
+
const rel = path5.relative(projectRoot, path5.join(dir, entry.name));
|
|
404
942
|
if (entry.isDirectory()) {
|
|
405
943
|
if (ALWAYS_SKIP_DIRS.has(entry.name)) {
|
|
406
944
|
continue;
|
|
407
945
|
}
|
|
408
|
-
if (isIgnored(rel,
|
|
409
|
-
walk(
|
|
946
|
+
if (isIgnored(rel, effectiveIgnore)) continue;
|
|
947
|
+
walk(path5.join(dir, entry.name));
|
|
410
948
|
} else if (entry.isFile()) {
|
|
411
|
-
const ext =
|
|
412
|
-
if (SOURCE_EXTS.has(ext) && !isIgnored(rel,
|
|
949
|
+
const ext = path5.extname(entry.name);
|
|
950
|
+
if (SOURCE_EXTS.has(ext) && !isIgnored(rel, effectiveIgnore)) {
|
|
413
951
|
files.push(rel);
|
|
414
952
|
}
|
|
415
953
|
}
|
|
@@ -423,16 +961,16 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
423
961
|
const walk = (d) => {
|
|
424
962
|
let entries;
|
|
425
963
|
try {
|
|
426
|
-
entries =
|
|
964
|
+
entries = fs5.readdirSync(d, { withFileTypes: true });
|
|
427
965
|
} catch {
|
|
428
966
|
return;
|
|
429
967
|
}
|
|
430
968
|
for (const entry of entries) {
|
|
431
969
|
if (entry.isDirectory()) {
|
|
432
970
|
if (entry.name === "node_modules") continue;
|
|
433
|
-
walk(
|
|
971
|
+
walk(path5.join(d, entry.name));
|
|
434
972
|
} else if (entry.isFile()) {
|
|
435
|
-
files.push(
|
|
973
|
+
files.push(path5.relative(projectRoot, path5.join(d, entry.name)));
|
|
436
974
|
}
|
|
437
975
|
}
|
|
438
976
|
};
|
|
@@ -441,8 +979,8 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
441
979
|
}
|
|
442
980
|
|
|
443
981
|
// src/commands/check-tests.ts
|
|
444
|
-
import * as
|
|
445
|
-
import * as
|
|
982
|
+
import * as fs6 from "fs";
|
|
983
|
+
import * as path6 from "path";
|
|
446
984
|
var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
447
985
|
".ts",
|
|
448
986
|
".tsx",
|
|
@@ -456,44 +994,58 @@ var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
|
456
994
|
]);
|
|
457
995
|
function checkMissingTests(projectRoot, config, severity) {
|
|
458
996
|
const violations = [];
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
997
|
+
for (const pkg of config.packages) {
|
|
998
|
+
const effectiveRules = { ...config.rules, ...pkg.rules };
|
|
999
|
+
const enforceMissing = effectiveRules.enforceMissingTests ?? effectiveRules.testCoverage > 0;
|
|
1000
|
+
if (!enforceMissing) continue;
|
|
1001
|
+
const testPattern = pkg.structure?.testPattern;
|
|
1002
|
+
const srcDir = pkg.structure?.srcDir;
|
|
1003
|
+
if (!testPattern || !srcDir) continue;
|
|
1004
|
+
const packageRoot2 = pkg.path === "." ? projectRoot : path6.join(projectRoot, pkg.path);
|
|
1005
|
+
const srcPath = path6.join(packageRoot2, srcDir);
|
|
1006
|
+
if (!fs6.existsSync(srcPath)) continue;
|
|
1007
|
+
const testSuffix = testPattern.replace("*", "");
|
|
1008
|
+
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1009
|
+
for (const relFile of sourceFiles) {
|
|
1010
|
+
const basename8 = path6.basename(relFile);
|
|
1011
|
+
if (basename8.includes(".test.") || basename8.includes(".spec.") || basename8.startsWith("index.") || basename8.endsWith(".d.ts")) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const ext = path6.extname(basename8);
|
|
1015
|
+
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1016
|
+
const stem = basename8.slice(0, -ext.length);
|
|
1017
|
+
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1018
|
+
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1019
|
+
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
1020
|
+
const testsDir = pkg.structure?.tests;
|
|
1021
|
+
const dedicatedTest = testsDir ? path6.join(packageRoot2, testsDir, expectedTestFile) : null;
|
|
1022
|
+
const hasTest = fs6.existsSync(colocatedTest) || dedicatedTest !== null && fs6.existsSync(dedicatedTest);
|
|
1023
|
+
if (!hasTest) {
|
|
1024
|
+
violations.push({
|
|
1025
|
+
file: relFile,
|
|
1026
|
+
rule: "missing-test",
|
|
1027
|
+
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
1028
|
+
severity
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
488
1031
|
}
|
|
489
1032
|
}
|
|
490
1033
|
return violations;
|
|
491
1034
|
}
|
|
1035
|
+
function resolvePackageForFile(sourceRelPath, config) {
|
|
1036
|
+
const sorted = [...config.packages].filter((p) => p.path !== ".").sort((a, b) => b.path.length - a.path.length);
|
|
1037
|
+
for (const pkg of sorted) {
|
|
1038
|
+
if (sourceRelPath.startsWith(`${pkg.path}/`) || sourceRelPath === pkg.path) {
|
|
1039
|
+
return pkg;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1043
|
+
}
|
|
492
1044
|
|
|
493
1045
|
// src/commands/check.ts
|
|
494
1046
|
var CONFIG_FILE2 = "viberails.config.json";
|
|
495
1047
|
function isTestFile(relPath) {
|
|
496
|
-
const filename =
|
|
1048
|
+
const filename = path7.basename(relPath);
|
|
497
1049
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
498
1050
|
}
|
|
499
1051
|
function printGroupedViolations(violations, limit) {
|
|
@@ -503,7 +1055,13 @@ function printGroupedViolations(violations, limit) {
|
|
|
503
1055
|
existing.push(v);
|
|
504
1056
|
groups.set(v.rule, existing);
|
|
505
1057
|
}
|
|
506
|
-
const ruleOrder = [
|
|
1058
|
+
const ruleOrder = [
|
|
1059
|
+
"file-size",
|
|
1060
|
+
"file-naming",
|
|
1061
|
+
"missing-test",
|
|
1062
|
+
"test-coverage",
|
|
1063
|
+
"boundary-violation"
|
|
1064
|
+
];
|
|
507
1065
|
const sortedKeys = [...groups.keys()].sort(
|
|
508
1066
|
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
509
1067
|
);
|
|
@@ -543,8 +1101,8 @@ async function checkCommand(options, cwd) {
|
|
|
543
1101
|
console.error(`${chalk2.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
544
1102
|
return 1;
|
|
545
1103
|
}
|
|
546
|
-
const configPath =
|
|
547
|
-
if (!
|
|
1104
|
+
const configPath = path7.join(projectRoot, CONFIG_FILE2);
|
|
1105
|
+
if (!fs7.existsSync(configPath)) {
|
|
548
1106
|
console.error(
|
|
549
1107
|
`${chalk2.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
550
1108
|
);
|
|
@@ -552,25 +1110,34 @@ async function checkCommand(options, cwd) {
|
|
|
552
1110
|
}
|
|
553
1111
|
const config = await loadConfig2(configPath);
|
|
554
1112
|
let filesToCheck;
|
|
1113
|
+
let diffAddedFiles = null;
|
|
555
1114
|
if (options.staged) {
|
|
556
1115
|
filesToCheck = getStagedFiles(projectRoot);
|
|
1116
|
+
} else if (options.diffBase) {
|
|
1117
|
+
const diff = getDiffFiles(projectRoot, options.diffBase);
|
|
1118
|
+
filesToCheck = diff.all.filter((f) => SOURCE_EXTS.has(path7.extname(f)));
|
|
1119
|
+
diffAddedFiles = new Set(diff.added);
|
|
557
1120
|
} else if (options.files && options.files.length > 0) {
|
|
558
1121
|
filesToCheck = options.files;
|
|
559
1122
|
} else {
|
|
560
1123
|
filesToCheck = getAllSourceFiles(projectRoot, config);
|
|
561
1124
|
}
|
|
562
1125
|
if (filesToCheck.length === 0) {
|
|
563
|
-
|
|
1126
|
+
if (options.format === "json") {
|
|
1127
|
+
console.log(JSON.stringify({ violations: [], checkedFiles: 0 }));
|
|
1128
|
+
} else {
|
|
1129
|
+
console.log(`${chalk2.green("\u2713")} No files to check.`);
|
|
1130
|
+
}
|
|
564
1131
|
return 0;
|
|
565
1132
|
}
|
|
566
1133
|
const violations = [];
|
|
567
|
-
const severity =
|
|
1134
|
+
const severity = options.enforce ? "error" : "warn";
|
|
568
1135
|
for (const file of filesToCheck) {
|
|
569
|
-
const absPath =
|
|
570
|
-
const relPath =
|
|
1136
|
+
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1137
|
+
const relPath = path7.relative(projectRoot, absPath);
|
|
571
1138
|
const effectiveIgnore = resolveIgnoreForFile(relPath, config);
|
|
572
1139
|
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
573
|
-
if (!
|
|
1140
|
+
if (!fs7.existsSync(absPath)) continue;
|
|
574
1141
|
const resolved = resolveConfigForFile(relPath, config);
|
|
575
1142
|
const testFile = isTestFile(relPath);
|
|
576
1143
|
const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
|
|
@@ -597,23 +1164,34 @@ async function checkCommand(options, cwd) {
|
|
|
597
1164
|
}
|
|
598
1165
|
}
|
|
599
1166
|
}
|
|
600
|
-
if (
|
|
1167
|
+
if (!options.staged && !options.files) {
|
|
601
1168
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
602
|
-
|
|
1169
|
+
if (diffAddedFiles) {
|
|
1170
|
+
violations.push(...testViolations.filter((v) => diffAddedFiles.has(v.file)));
|
|
1171
|
+
} else {
|
|
1172
|
+
violations.push(...testViolations);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (!options.files && !options.staged && !options.diffBase) {
|
|
1176
|
+
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1177
|
+
staged: options.staged,
|
|
1178
|
+
enforce: options.enforce
|
|
1179
|
+
});
|
|
1180
|
+
violations.push(...coverageViolations);
|
|
603
1181
|
}
|
|
604
1182
|
if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
|
|
605
1183
|
const startTime = Date.now();
|
|
606
1184
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
607
|
-
const packages = config.
|
|
1185
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
608
1186
|
const graph = await buildImportGraph(projectRoot, {
|
|
609
1187
|
packages,
|
|
610
1188
|
ignore: config.ignore
|
|
611
1189
|
});
|
|
612
1190
|
const boundaryViolations = checkBoundaries(graph, config.boundaries);
|
|
613
|
-
const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) =>
|
|
1191
|
+
const filterSet = options.staged || options.files || options.diffBase ? new Set(filesToCheck.map((f) => path7.resolve(projectRoot, f))) : null;
|
|
614
1192
|
for (const bv of boundaryViolations) {
|
|
615
1193
|
if (filterSet && !filterSet.has(bv.file)) continue;
|
|
616
|
-
const relFile =
|
|
1194
|
+
const relFile = path7.relative(projectRoot, bv.file);
|
|
617
1195
|
violations.push({
|
|
618
1196
|
file: relFile,
|
|
619
1197
|
rule: "boundary-violation",
|
|
@@ -622,17 +1200,18 @@ async function checkCommand(options, cwd) {
|
|
|
622
1200
|
});
|
|
623
1201
|
}
|
|
624
1202
|
const elapsed = Date.now() - startTime;
|
|
625
|
-
|
|
1203
|
+
if (options.format !== "json") {
|
|
1204
|
+
console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
|
|
1205
|
+
}
|
|
626
1206
|
}
|
|
627
1207
|
if (options.format === "json") {
|
|
628
1208
|
console.log(
|
|
629
1209
|
JSON.stringify({
|
|
630
1210
|
violations,
|
|
631
|
-
checkedFiles: filesToCheck.length
|
|
632
|
-
enforcement: config.enforcement
|
|
1211
|
+
checkedFiles: filesToCheck.length
|
|
633
1212
|
})
|
|
634
1213
|
);
|
|
635
|
-
return
|
|
1214
|
+
return options.enforce && violations.length > 0 ? 1 : 0;
|
|
636
1215
|
}
|
|
637
1216
|
if (violations.length === 0) {
|
|
638
1217
|
console.log(`${chalk2.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
@@ -642,16 +1221,62 @@ async function checkCommand(options, cwd) {
|
|
|
642
1221
|
printGroupedViolations(violations, options.limit);
|
|
643
1222
|
}
|
|
644
1223
|
printSummary(violations);
|
|
645
|
-
if (
|
|
1224
|
+
if (options.enforce) {
|
|
646
1225
|
console.log(chalk2.red("Fix violations before committing."));
|
|
647
1226
|
return 1;
|
|
648
1227
|
}
|
|
649
1228
|
return 0;
|
|
650
1229
|
}
|
|
651
1230
|
|
|
1231
|
+
// src/commands/check-hook.ts
|
|
1232
|
+
import * as fs8 from "fs";
|
|
1233
|
+
function parseHookFilePath(input) {
|
|
1234
|
+
try {
|
|
1235
|
+
if (!input.trim()) return void 0;
|
|
1236
|
+
const parsed = JSON.parse(input);
|
|
1237
|
+
return parsed?.tool_input?.file_path ?? void 0;
|
|
1238
|
+
} catch {
|
|
1239
|
+
return void 0;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function readStdin() {
|
|
1243
|
+
try {
|
|
1244
|
+
return fs8.readFileSync(0, "utf-8");
|
|
1245
|
+
} catch {
|
|
1246
|
+
return "";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async function hookCheckCommand(cwd) {
|
|
1250
|
+
try {
|
|
1251
|
+
const filePath = parseHookFilePath(readStdin());
|
|
1252
|
+
if (!filePath) return 0;
|
|
1253
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
1254
|
+
let captured = "";
|
|
1255
|
+
process.stdout.write = (chunk) => {
|
|
1256
|
+
captured += typeof chunk === "string" ? chunk : chunk.toString();
|
|
1257
|
+
return true;
|
|
1258
|
+
};
|
|
1259
|
+
try {
|
|
1260
|
+
await checkCommand({ files: [filePath], format: "json" }, cwd);
|
|
1261
|
+
} finally {
|
|
1262
|
+
process.stdout.write = originalWrite;
|
|
1263
|
+
}
|
|
1264
|
+
if (!captured.trim()) return 0;
|
|
1265
|
+
const result = JSON.parse(captured);
|
|
1266
|
+
if (result.violations?.length > 0) {
|
|
1267
|
+
process.stderr.write(`${captured.trim()}
|
|
1268
|
+
`);
|
|
1269
|
+
return 2;
|
|
1270
|
+
}
|
|
1271
|
+
return 0;
|
|
1272
|
+
} catch {
|
|
1273
|
+
return 0;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
652
1277
|
// src/commands/fix.ts
|
|
653
|
-
import * as
|
|
654
|
-
import * as
|
|
1278
|
+
import * as fs11 from "fs";
|
|
1279
|
+
import * as path11 from "path";
|
|
655
1280
|
import { loadConfig as loadConfig3 } from "@viberails/config";
|
|
656
1281
|
import chalk4 from "chalk";
|
|
657
1282
|
|
|
@@ -676,7 +1301,8 @@ function checkGitDirty(projectRoot) {
|
|
|
676
1301
|
try {
|
|
677
1302
|
const output = execSync2("git status --porcelain", {
|
|
678
1303
|
cwd: projectRoot,
|
|
679
|
-
encoding: "utf-8"
|
|
1304
|
+
encoding: "utf-8",
|
|
1305
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
680
1306
|
});
|
|
681
1307
|
return output.trim().length > 0;
|
|
682
1308
|
} catch {
|
|
@@ -685,14 +1311,11 @@ function checkGitDirty(projectRoot) {
|
|
|
685
1311
|
}
|
|
686
1312
|
function getConventionValue(convention) {
|
|
687
1313
|
if (typeof convention === "string") return convention;
|
|
688
|
-
if (convention && typeof convention === "object" && "value" in convention) {
|
|
689
|
-
return convention.value;
|
|
690
|
-
}
|
|
691
1314
|
return void 0;
|
|
692
1315
|
}
|
|
693
1316
|
|
|
694
1317
|
// src/commands/fix-imports.ts
|
|
695
|
-
import * as
|
|
1318
|
+
import * as path8 from "path";
|
|
696
1319
|
function stripExtension(filePath) {
|
|
697
1320
|
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
698
1321
|
}
|
|
@@ -710,7 +1333,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
710
1333
|
const renameMap = /* @__PURE__ */ new Map();
|
|
711
1334
|
for (const r of renames) {
|
|
712
1335
|
const oldStripped = stripExtension(r.oldAbsPath);
|
|
713
|
-
const newFilename =
|
|
1336
|
+
const newFilename = path8.basename(r.newPath);
|
|
714
1337
|
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
715
1338
|
renameMap.set(oldStripped, { newBare: newName });
|
|
716
1339
|
}
|
|
@@ -718,14 +1341,14 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
718
1341
|
tsConfigFilePath: void 0,
|
|
719
1342
|
skipAddingFilesFromTsConfig: true
|
|
720
1343
|
});
|
|
721
|
-
project.addSourceFilesAtPaths(
|
|
1344
|
+
project.addSourceFilesAtPaths(path8.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
722
1345
|
const updates = [];
|
|
723
1346
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
724
1347
|
for (const sourceFile of project.getSourceFiles()) {
|
|
725
1348
|
const filePath = sourceFile.getFilePath();
|
|
726
|
-
const segments = filePath.split(
|
|
1349
|
+
const segments = filePath.split(path8.sep);
|
|
727
1350
|
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
728
|
-
const fileDir =
|
|
1351
|
+
const fileDir = path8.dirname(filePath);
|
|
729
1352
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
730
1353
|
const specifier = decl.getModuleSpecifierValue();
|
|
731
1354
|
if (!specifier.startsWith(".")) continue;
|
|
@@ -782,7 +1405,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
782
1405
|
}
|
|
783
1406
|
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
784
1407
|
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
785
|
-
const resolved =
|
|
1408
|
+
const resolved = path8.resolve(fromDir, cleanSpec);
|
|
786
1409
|
for (const ext of extensions) {
|
|
787
1410
|
const candidate = resolved + ext;
|
|
788
1411
|
const stripped = stripExtension(candidate);
|
|
@@ -793,8 +1416,8 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
793
1416
|
}
|
|
794
1417
|
|
|
795
1418
|
// src/commands/fix-naming.ts
|
|
796
|
-
import * as
|
|
797
|
-
import * as
|
|
1419
|
+
import * as fs9 from "fs";
|
|
1420
|
+
import * as path9 from "path";
|
|
798
1421
|
|
|
799
1422
|
// src/commands/convert-name.ts
|
|
800
1423
|
function splitIntoWords(name) {
|
|
@@ -843,8 +1466,8 @@ function capitalize(word) {
|
|
|
843
1466
|
|
|
844
1467
|
// src/commands/fix-naming.ts
|
|
845
1468
|
function computeRename(relPath, targetConvention, projectRoot) {
|
|
846
|
-
const filename =
|
|
847
|
-
const dir =
|
|
1469
|
+
const filename = path9.basename(relPath);
|
|
1470
|
+
const dir = path9.dirname(relPath);
|
|
848
1471
|
const dotIndex = filename.indexOf(".");
|
|
849
1472
|
if (dotIndex === -1) return null;
|
|
850
1473
|
const bare = filename.slice(0, dotIndex);
|
|
@@ -852,15 +1475,15 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
852
1475
|
const newBare = convertName(bare, targetConvention);
|
|
853
1476
|
if (newBare === bare) return null;
|
|
854
1477
|
const newFilename = newBare + suffix;
|
|
855
|
-
const newRelPath =
|
|
856
|
-
const oldAbsPath =
|
|
857
|
-
const newAbsPath =
|
|
858
|
-
if (
|
|
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;
|
|
859
1482
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
860
1483
|
}
|
|
861
1484
|
function executeRename(rename) {
|
|
862
|
-
if (
|
|
863
|
-
|
|
1485
|
+
if (fs9.existsSync(rename.newAbsPath)) return false;
|
|
1486
|
+
fs9.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
864
1487
|
return true;
|
|
865
1488
|
}
|
|
866
1489
|
function deduplicateRenames(renames) {
|
|
@@ -875,33 +1498,38 @@ function deduplicateRenames(renames) {
|
|
|
875
1498
|
}
|
|
876
1499
|
|
|
877
1500
|
// src/commands/fix-tests.ts
|
|
878
|
-
import * as
|
|
879
|
-
import * as
|
|
1501
|
+
import * as fs10 from "fs";
|
|
1502
|
+
import * as path10 from "path";
|
|
880
1503
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
881
|
-
const
|
|
1504
|
+
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
1505
|
+
const testPattern = pkg?.structure?.testPattern;
|
|
882
1506
|
if (!testPattern) return null;
|
|
883
|
-
const
|
|
884
|
-
const
|
|
1507
|
+
const basename8 = path10.basename(sourceRelPath);
|
|
1508
|
+
const ext = path10.extname(basename8);
|
|
1509
|
+
if (!ext) return null;
|
|
1510
|
+
const stem = basename8.slice(0, -ext.length);
|
|
885
1511
|
const testSuffix = testPattern.replace("*", "");
|
|
886
1512
|
const testFilename = `${stem}${testSuffix}`;
|
|
887
|
-
const dir =
|
|
888
|
-
const testAbsPath =
|
|
889
|
-
if (
|
|
1513
|
+
const dir = path10.dirname(path10.join(projectRoot, sourceRelPath));
|
|
1514
|
+
const testAbsPath = path10.join(dir, testFilename);
|
|
1515
|
+
if (fs10.existsSync(testAbsPath)) return null;
|
|
890
1516
|
return {
|
|
891
|
-
path:
|
|
1517
|
+
path: path10.relative(projectRoot, testAbsPath),
|
|
892
1518
|
absPath: testAbsPath,
|
|
893
1519
|
moduleName: stem
|
|
894
1520
|
};
|
|
895
1521
|
}
|
|
896
1522
|
function writeTestStub(stub, config) {
|
|
897
|
-
const
|
|
1523
|
+
const pkg = resolvePackageForFile(stub.path, config);
|
|
1524
|
+
const testRunner = pkg?.stack?.testRunner ?? "";
|
|
1525
|
+
const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
|
|
898
1526
|
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
899
1527
|
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
900
1528
|
it.todo('add tests');
|
|
901
1529
|
});
|
|
902
1530
|
`;
|
|
903
|
-
|
|
904
|
-
|
|
1531
|
+
fs10.mkdirSync(path10.dirname(stub.absPath), { recursive: true });
|
|
1532
|
+
fs10.writeFileSync(stub.absPath, content);
|
|
905
1533
|
}
|
|
906
1534
|
|
|
907
1535
|
// src/commands/fix.ts
|
|
@@ -913,8 +1541,8 @@ async function fixCommand(options, cwd) {
|
|
|
913
1541
|
console.error(`${chalk4.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
914
1542
|
return 1;
|
|
915
1543
|
}
|
|
916
|
-
const configPath =
|
|
917
|
-
if (!
|
|
1544
|
+
const configPath = path11.join(projectRoot, CONFIG_FILE3);
|
|
1545
|
+
if (!fs11.existsSync(configPath)) {
|
|
918
1546
|
console.error(
|
|
919
1547
|
`${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
920
1548
|
);
|
|
@@ -947,7 +1575,7 @@ async function fixCommand(options, cwd) {
|
|
|
947
1575
|
}
|
|
948
1576
|
const dedupedRenames = deduplicateRenames(renames);
|
|
949
1577
|
const testStubs = [];
|
|
950
|
-
if (shouldFixTests
|
|
1578
|
+
if (shouldFixTests) {
|
|
951
1579
|
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
952
1580
|
for (const v of testViolations) {
|
|
953
1581
|
const stub = generateTestStub(v.file, config, projectRoot);
|
|
@@ -978,13 +1606,13 @@ async function fixCommand(options, cwd) {
|
|
|
978
1606
|
}
|
|
979
1607
|
let importUpdateCount = 0;
|
|
980
1608
|
if (renameCount > 0) {
|
|
981
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1609
|
+
const appliedRenames = dedupedRenames.filter((r) => fs11.existsSync(r.newAbsPath));
|
|
982
1610
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
983
1611
|
importUpdateCount = updates.length;
|
|
984
1612
|
}
|
|
985
1613
|
let stubCount = 0;
|
|
986
1614
|
for (const stub of testStubs) {
|
|
987
|
-
if (!
|
|
1615
|
+
if (!fs11.existsSync(stub.absPath)) {
|
|
988
1616
|
writeTestStub(stub, config);
|
|
989
1617
|
stubCount++;
|
|
990
1618
|
}
|
|
@@ -1005,14 +1633,14 @@ async function fixCommand(options, cwd) {
|
|
|
1005
1633
|
}
|
|
1006
1634
|
|
|
1007
1635
|
// src/commands/init.ts
|
|
1008
|
-
import * as
|
|
1009
|
-
import * as
|
|
1010
|
-
import * as
|
|
1011
|
-
import { generateConfig } from "@viberails/config";
|
|
1636
|
+
import * as fs17 from "fs";
|
|
1637
|
+
import * as path17 from "path";
|
|
1638
|
+
import * as clack7 from "@clack/prompts";
|
|
1639
|
+
import { compactConfig as compactConfig2, generateConfig } from "@viberails/config";
|
|
1012
1640
|
import { scan } from "@viberails/scanner";
|
|
1013
|
-
import
|
|
1641
|
+
import chalk10 from "chalk";
|
|
1014
1642
|
|
|
1015
|
-
// src/display
|
|
1643
|
+
// src/display.ts
|
|
1016
1644
|
import {
|
|
1017
1645
|
CONVENTION_LABELS as CONVENTION_LABELS2,
|
|
1018
1646
|
FRAMEWORK_NAMES as FRAMEWORK_NAMES3,
|
|
@@ -1020,6 +1648,7 @@ import {
|
|
|
1020
1648
|
ORM_NAMES as ORM_NAMES2,
|
|
1021
1649
|
STYLING_NAMES as STYLING_NAMES3
|
|
1022
1650
|
} from "@viberails/types";
|
|
1651
|
+
import chalk6 from "chalk";
|
|
1023
1652
|
|
|
1024
1653
|
// src/display-helpers.ts
|
|
1025
1654
|
import { ROLE_DESCRIPTIONS } from "@viberails/types";
|
|
@@ -1070,26 +1699,136 @@ function formatRoleGroup(group) {
|
|
|
1070
1699
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1071
1700
|
}
|
|
1072
1701
|
|
|
1073
|
-
// src/display.ts
|
|
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
|
+
// src/display-text.ts
|
|
1074
1707
|
import {
|
|
1075
1708
|
CONVENTION_LABELS,
|
|
1076
|
-
FRAMEWORK_NAMES
|
|
1709
|
+
FRAMEWORK_NAMES,
|
|
1077
1710
|
LIBRARY_NAMES,
|
|
1078
1711
|
ORM_NAMES,
|
|
1079
|
-
STYLING_NAMES
|
|
1712
|
+
STYLING_NAMES
|
|
1080
1713
|
} from "@viberails/types";
|
|
1081
|
-
|
|
1714
|
+
function plainConfidenceLabel(convention) {
|
|
1715
|
+
const pct = Math.round(convention.consistency);
|
|
1716
|
+
if (convention.confidence === "high") {
|
|
1717
|
+
return `${pct}%`;
|
|
1718
|
+
}
|
|
1719
|
+
return `${pct}%, suggested only`;
|
|
1720
|
+
}
|
|
1721
|
+
function formatConventionsText(scanResult) {
|
|
1722
|
+
const lines = [];
|
|
1723
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
1724
|
+
if (conventionEntries.length === 0) return lines;
|
|
1725
|
+
lines.push("");
|
|
1726
|
+
lines.push("Conventions:");
|
|
1727
|
+
for (const [key, convention] of conventionEntries) {
|
|
1728
|
+
if (convention.confidence === "low") continue;
|
|
1729
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
1730
|
+
if (scanResult.packages.length > 1) {
|
|
1731
|
+
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1732
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
1733
|
+
if (allSame || pkgValues.length <= 1) {
|
|
1734
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1735
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1736
|
+
} else {
|
|
1737
|
+
lines.push(` ~ ${label}: varies by package`);
|
|
1738
|
+
for (const pv of pkgValues) {
|
|
1739
|
+
const pct = Math.round(pv.convention.consistency);
|
|
1740
|
+
lines.push(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
} else {
|
|
1744
|
+
const ind = convention.confidence === "high" ? "\u2713" : "~";
|
|
1745
|
+
lines.push(` ${ind} ${label}: ${convention.value} (${plainConfidenceLabel(convention)})`);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return lines;
|
|
1749
|
+
}
|
|
1750
|
+
function formatRulesText(config) {
|
|
1751
|
+
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1752
|
+
const lines = [];
|
|
1753
|
+
lines.push(`Max file size: ${config.rules.maxFileLines} lines`);
|
|
1754
|
+
if (config.rules.testCoverage > 0) {
|
|
1755
|
+
lines.push(`Test coverage target: ${config.rules.testCoverage}%`);
|
|
1756
|
+
} else {
|
|
1757
|
+
lines.push("Test coverage target: disabled");
|
|
1758
|
+
}
|
|
1759
|
+
const enforceMissing = config.rules.enforceMissingTests ?? config.rules.testCoverage > 0;
|
|
1760
|
+
if (enforceMissing && root?.structure?.testPattern) {
|
|
1761
|
+
lines.push(`Enforce missing tests: yes (${root.structure.testPattern})`);
|
|
1762
|
+
} else {
|
|
1763
|
+
lines.push("Enforce missing tests: no");
|
|
1764
|
+
}
|
|
1765
|
+
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
1766
|
+
lines.push(`Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1767
|
+
} else {
|
|
1768
|
+
lines.push("Enforce file naming: no");
|
|
1769
|
+
}
|
|
1770
|
+
return lines;
|
|
1771
|
+
}
|
|
1772
|
+
function formatScanResultsText(scanResult) {
|
|
1773
|
+
if (scanResult.packages.length > 1) {
|
|
1774
|
+
return formatMonorepoResultsText(scanResult);
|
|
1775
|
+
}
|
|
1776
|
+
const lines = [];
|
|
1777
|
+
const { stack } = scanResult;
|
|
1778
|
+
lines.push("Detected:");
|
|
1779
|
+
if (stack.framework) {
|
|
1780
|
+
lines.push(` \u2713 ${formatItem(stack.framework, FRAMEWORK_NAMES)}`);
|
|
1781
|
+
}
|
|
1782
|
+
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1783
|
+
if (stack.styling) {
|
|
1784
|
+
lines.push(` \u2713 ${formatItem(stack.styling, STYLING_NAMES)}`);
|
|
1785
|
+
}
|
|
1786
|
+
if (stack.backend) {
|
|
1787
|
+
lines.push(` \u2713 ${formatItem(stack.backend, FRAMEWORK_NAMES)}`);
|
|
1788
|
+
}
|
|
1789
|
+
if (stack.orm) {
|
|
1790
|
+
lines.push(` \u2713 ${formatItem(stack.orm, ORM_NAMES)}`);
|
|
1791
|
+
}
|
|
1792
|
+
const secondaryParts = [];
|
|
1793
|
+
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
1794
|
+
if (stack.linter) secondaryParts.push(formatItem(stack.linter));
|
|
1795
|
+
if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
|
|
1796
|
+
if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
|
|
1797
|
+
if (secondaryParts.length > 0) {
|
|
1798
|
+
lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
|
|
1799
|
+
}
|
|
1800
|
+
if (stack.libraries.length > 0) {
|
|
1801
|
+
for (const lib of stack.libraries) {
|
|
1802
|
+
lines.push(` \u2713 ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const groups = groupByRole(scanResult.structure.directories);
|
|
1806
|
+
if (groups.length > 0) {
|
|
1807
|
+
lines.push("");
|
|
1808
|
+
lines.push("Structure:");
|
|
1809
|
+
for (const group of groups) {
|
|
1810
|
+
lines.push(` \u2713 ${formatRoleGroup(group)}`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
lines.push(...formatConventionsText(scanResult));
|
|
1814
|
+
const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
|
|
1815
|
+
lines.push("");
|
|
1816
|
+
lines.push(formatSummary(scanResult.statistics, pkgCount));
|
|
1817
|
+
const ext = formatExtensions(scanResult.statistics.filesByExtension);
|
|
1818
|
+
if (ext) {
|
|
1819
|
+
lines.push(ext);
|
|
1820
|
+
}
|
|
1821
|
+
return lines.join("\n");
|
|
1822
|
+
}
|
|
1082
1823
|
|
|
1083
1824
|
// src/display-monorepo.ts
|
|
1084
|
-
import { FRAMEWORK_NAMES, STYLING_NAMES } from "@viberails/types";
|
|
1085
|
-
import chalk5 from "chalk";
|
|
1086
1825
|
function formatPackageSummary(pkg) {
|
|
1087
1826
|
const parts = [];
|
|
1088
1827
|
if (pkg.stack.framework) {
|
|
1089
|
-
parts.push(formatItem(pkg.stack.framework,
|
|
1828
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES2));
|
|
1090
1829
|
}
|
|
1091
1830
|
if (pkg.stack.styling) {
|
|
1092
|
-
parts.push(formatItem(pkg.stack.styling,
|
|
1831
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES2));
|
|
1093
1832
|
}
|
|
1094
1833
|
const files = `${pkg.statistics.totalFiles} files`;
|
|
1095
1834
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
@@ -1103,11 +1842,15 @@ ${chalk5.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
|
1103
1842
|
if (stack.packageManager) {
|
|
1104
1843
|
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1105
1844
|
}
|
|
1106
|
-
if (stack.linter) {
|
|
1107
|
-
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1845
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1846
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
1847
|
+
} else {
|
|
1848
|
+
if (stack.linter) {
|
|
1849
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (stack.formatter) {
|
|
1852
|
+
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
1853
|
+
}
|
|
1111
1854
|
}
|
|
1112
1855
|
if (stack.testRunner) {
|
|
1113
1856
|
console.log(` ${chalk5.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
@@ -1138,23 +1881,27 @@ ${chalk5.bold("Structure:")}`);
|
|
|
1138
1881
|
function formatPackageSummaryPlain(pkg) {
|
|
1139
1882
|
const parts = [];
|
|
1140
1883
|
if (pkg.stack.framework) {
|
|
1141
|
-
parts.push(formatItem(pkg.stack.framework,
|
|
1884
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES2));
|
|
1142
1885
|
}
|
|
1143
1886
|
if (pkg.stack.styling) {
|
|
1144
|
-
parts.push(formatItem(pkg.stack.styling,
|
|
1887
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES2));
|
|
1145
1888
|
}
|
|
1146
1889
|
const files = `${pkg.statistics.totalFiles} files`;
|
|
1147
1890
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1148
1891
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1149
1892
|
}
|
|
1150
|
-
function formatMonorepoResultsText(scanResult
|
|
1893
|
+
function formatMonorepoResultsText(scanResult) {
|
|
1151
1894
|
const lines = [];
|
|
1152
1895
|
const { stack, packages } = scanResult;
|
|
1153
1896
|
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1154
1897
|
const sharedParts = [formatItem(stack.language)];
|
|
1155
1898
|
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1156
|
-
if (stack.linter
|
|
1157
|
-
|
|
1899
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
1900
|
+
sharedParts.push(`${formatItem(stack.linter)} (lint + format)`);
|
|
1901
|
+
} else {
|
|
1902
|
+
if (stack.linter) sharedParts.push(formatItem(stack.linter));
|
|
1903
|
+
if (stack.formatter) sharedParts.push(formatItem(stack.formatter));
|
|
1904
|
+
}
|
|
1158
1905
|
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1159
1906
|
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1160
1907
|
lines.push("");
|
|
@@ -1184,7 +1931,6 @@ function formatMonorepoResultsText(scanResult, config) {
|
|
|
1184
1931
|
if (ext) {
|
|
1185
1932
|
lines.push(ext);
|
|
1186
1933
|
}
|
|
1187
|
-
lines.push(...formatRulesText(config));
|
|
1188
1934
|
return lines.join("\n");
|
|
1189
1935
|
}
|
|
1190
1936
|
|
|
@@ -1207,7 +1953,7 @@ function displayConventions(scanResult) {
|
|
|
1207
1953
|
${chalk6.bold("Conventions:")}`);
|
|
1208
1954
|
for (const [key, convention] of conventionEntries) {
|
|
1209
1955
|
if (convention.confidence === "low") continue;
|
|
1210
|
-
const label =
|
|
1956
|
+
const label = CONVENTION_LABELS2[key] ?? key;
|
|
1211
1957
|
if (scanResult.packages.length > 1) {
|
|
1212
1958
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1213
1959
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1248,23 +1994,27 @@ function displayScanResults(scanResult) {
|
|
|
1248
1994
|
console.log(`
|
|
1249
1995
|
${chalk6.bold("Detected:")}`);
|
|
1250
1996
|
if (stack.framework) {
|
|
1251
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework,
|
|
1997
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES3)}`);
|
|
1252
1998
|
}
|
|
1253
1999
|
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1254
2000
|
if (stack.styling) {
|
|
1255
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling,
|
|
2001
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES3)}`);
|
|
1256
2002
|
}
|
|
1257
2003
|
if (stack.backend) {
|
|
1258
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend,
|
|
2004
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES3)}`);
|
|
1259
2005
|
}
|
|
1260
2006
|
if (stack.orm) {
|
|
1261
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm,
|
|
2007
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.orm, ORM_NAMES2)}`);
|
|
1262
2008
|
}
|
|
1263
|
-
if (stack.linter) {
|
|
1264
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
|
|
2009
|
+
if (stack.linter && stack.formatter && stack.linter.name === stack.formatter.name) {
|
|
2010
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)} (lint + format)`);
|
|
2011
|
+
} else {
|
|
2012
|
+
if (stack.linter) {
|
|
2013
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
2014
|
+
}
|
|
2015
|
+
if (stack.formatter) {
|
|
2016
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.formatter)}`);
|
|
2017
|
+
}
|
|
1268
2018
|
}
|
|
1269
2019
|
if (stack.testRunner) {
|
|
1270
2020
|
console.log(` ${chalk6.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
@@ -1274,7 +2024,7 @@ ${chalk6.bold("Detected:")}`);
|
|
|
1274
2024
|
}
|
|
1275
2025
|
if (stack.libraries.length > 0) {
|
|
1276
2026
|
for (const lib of stack.libraries) {
|
|
1277
|
-
console.log(` ${chalk6.green("\u2713")} ${formatItem(lib,
|
|
2027
|
+
console.log(` ${chalk6.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES2)}`);
|
|
1278
2028
|
}
|
|
1279
2029
|
}
|
|
1280
2030
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1289,25 +2039,23 @@ ${chalk6.bold("Structure:")}`);
|
|
|
1289
2039
|
displaySummarySection(scanResult);
|
|
1290
2040
|
console.log("");
|
|
1291
2041
|
}
|
|
1292
|
-
function getConventionStr(cv) {
|
|
1293
|
-
return typeof cv === "string" ? cv : cv.value;
|
|
1294
|
-
}
|
|
1295
2042
|
function displayRulesPreview(config) {
|
|
1296
|
-
|
|
2043
|
+
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2044
|
+
console.log(
|
|
2045
|
+
`${chalk6.bold("Rules:")} ${chalk6.dim("(warns on violation; use --enforce in CI to block)")}`
|
|
2046
|
+
);
|
|
1297
2047
|
console.log(` ${chalk6.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1298
|
-
if (config.rules.
|
|
2048
|
+
if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
|
|
1299
2049
|
console.log(
|
|
1300
|
-
` ${chalk6.dim("\u2022")}
|
|
2050
|
+
` ${chalk6.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
|
|
1301
2051
|
);
|
|
1302
|
-
} else if (config.rules.
|
|
1303
|
-
console.log(` ${chalk6.dim("\u2022")}
|
|
2052
|
+
} else if (config.rules.testCoverage > 0) {
|
|
2053
|
+
console.log(` ${chalk6.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}%`);
|
|
1304
2054
|
} else {
|
|
1305
|
-
console.log(` ${chalk6.dim("\u2022")}
|
|
2055
|
+
console.log(` ${chalk6.dim("\u2022")} Test coverage target: disabled`);
|
|
1306
2056
|
}
|
|
1307
|
-
if (config.rules.enforceNaming &&
|
|
1308
|
-
console.log(
|
|
1309
|
-
` ${chalk6.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
|
|
1310
|
-
);
|
|
2057
|
+
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
2058
|
+
console.log(` ${chalk6.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1311
2059
|
} else {
|
|
1312
2060
|
console.log(` ${chalk6.dim("\u2022")} Enforce file naming: no`);
|
|
1313
2061
|
}
|
|
@@ -1315,146 +2063,163 @@ function displayRulesPreview(config) {
|
|
|
1315
2063
|
` ${chalk6.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1316
2064
|
);
|
|
1317
2065
|
console.log("");
|
|
1318
|
-
if (config.enforcement === "enforce") {
|
|
1319
|
-
console.log(`${chalk6.bold("Enforcement mode:")} enforce (violations will block commits)`);
|
|
1320
|
-
} else {
|
|
1321
|
-
console.log(
|
|
1322
|
-
`${chalk6.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
console.log("");
|
|
1326
2066
|
}
|
|
1327
2067
|
|
|
1328
|
-
// src/
|
|
1329
|
-
|
|
1330
|
-
|
|
2068
|
+
// src/utils/check-prerequisites.ts
|
|
2069
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2070
|
+
import * as fs12 from "fs";
|
|
2071
|
+
import * as path12 from "path";
|
|
2072
|
+
import * as clack6 from "@clack/prompts";
|
|
2073
|
+
import chalk7 from "chalk";
|
|
2074
|
+
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2075
|
+
const testRunner = scanResult.stack.testRunner;
|
|
2076
|
+
if (!testRunner) return [];
|
|
2077
|
+
const runner = testRunner.name;
|
|
2078
|
+
const pm = scanResult.stack.packageManager.name;
|
|
2079
|
+
if (runner === "vitest") {
|
|
2080
|
+
const hasV8 = hasDependency(projectRoot, "@vitest/coverage-v8");
|
|
2081
|
+
const hasIstanbul = hasDependency(projectRoot, "@vitest/coverage-istanbul");
|
|
2082
|
+
const installed = hasV8 || hasIstanbul;
|
|
2083
|
+
const addCmd = pm === "yarn" ? "yarn add -D" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
|
|
2084
|
+
return [
|
|
2085
|
+
{
|
|
2086
|
+
label: "@vitest/coverage-v8",
|
|
2087
|
+
installed,
|
|
2088
|
+
installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
|
|
2089
|
+
reason: "Required for coverage percentage checks with vitest"
|
|
2090
|
+
}
|
|
2091
|
+
];
|
|
2092
|
+
}
|
|
2093
|
+
return [];
|
|
1331
2094
|
}
|
|
1332
|
-
function
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
2095
|
+
function displayMissingPrereqs(prereqs) {
|
|
2096
|
+
const missing = prereqs.filter((p) => !p.installed);
|
|
2097
|
+
for (const m of missing) {
|
|
2098
|
+
console.log(` ${chalk7.yellow("!")} ${m.label} not installed \u2014 ${m.reason}`);
|
|
2099
|
+
if (m.installCommand) {
|
|
2100
|
+
console.log(` Install: ${chalk7.cyan(m.installCommand)}`);
|
|
2101
|
+
}
|
|
1336
2102
|
}
|
|
1337
|
-
return `${pct}%, suggested only`;
|
|
1338
2103
|
}
|
|
1339
|
-
function
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
2104
|
+
async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
2105
|
+
const missing = prereqs.filter((p) => !p.installed);
|
|
2106
|
+
if (missing.length === 0) return { disableCoverage: false };
|
|
2107
|
+
const prereqLines = prereqs.map(
|
|
2108
|
+
(p) => `${p.installed ? "\u2713" : "\u2717"} ${p.label}${p.installed ? "" : ` \u2014 ${p.reason}`}`
|
|
2109
|
+
).join("\n");
|
|
2110
|
+
clack6.note(prereqLines, "Coverage prerequisites");
|
|
2111
|
+
let disableCoverage = false;
|
|
2112
|
+
for (const m of missing) {
|
|
2113
|
+
if (!m.installCommand) continue;
|
|
2114
|
+
const choice = await clack6.select({
|
|
2115
|
+
message: `${m.label} is not installed. It is required for coverage percentage checks.`,
|
|
2116
|
+
options: [
|
|
2117
|
+
{
|
|
2118
|
+
value: "install",
|
|
2119
|
+
label: `Yes, install now`,
|
|
2120
|
+
hint: m.installCommand
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
value: "disable",
|
|
2124
|
+
label: "No, disable coverage percentage checks",
|
|
2125
|
+
hint: "missing-test checks still active"
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
value: "skip",
|
|
2129
|
+
label: "Skip for now",
|
|
2130
|
+
hint: `install later: ${m.installCommand}`
|
|
1359
2131
|
}
|
|
2132
|
+
]
|
|
2133
|
+
});
|
|
2134
|
+
assertNotCancelled(choice);
|
|
2135
|
+
if (choice === "install") {
|
|
2136
|
+
const is = clack6.spinner();
|
|
2137
|
+
is.start(`Installing ${m.label}...`);
|
|
2138
|
+
const result = spawnSync2(m.installCommand, {
|
|
2139
|
+
cwd: projectRoot,
|
|
2140
|
+
shell: true,
|
|
2141
|
+
encoding: "utf-8",
|
|
2142
|
+
stdio: "pipe"
|
|
2143
|
+
});
|
|
2144
|
+
if (result.status === 0) {
|
|
2145
|
+
is.stop(`Installed ${m.label}`);
|
|
2146
|
+
} else {
|
|
2147
|
+
is.stop(`Failed to install ${m.label}`);
|
|
2148
|
+
clack6.log.warn(
|
|
2149
|
+
`Install manually: ${m.installCommand}
|
|
2150
|
+
Coverage percentage checks will not work until the dependency is installed.`
|
|
2151
|
+
);
|
|
1360
2152
|
}
|
|
2153
|
+
} else if (choice === "disable") {
|
|
2154
|
+
disableCoverage = true;
|
|
2155
|
+
clack6.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
|
|
1361
2156
|
} else {
|
|
1362
|
-
|
|
1363
|
-
|
|
2157
|
+
clack6.log.info(
|
|
2158
|
+
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2159
|
+
Install later: ${m.installCommand}`
|
|
2160
|
+
);
|
|
1364
2161
|
}
|
|
1365
2162
|
}
|
|
1366
|
-
return
|
|
2163
|
+
return { disableCoverage };
|
|
1367
2164
|
}
|
|
1368
|
-
function
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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");
|
|
2165
|
+
function hasDependency(projectRoot, name) {
|
|
2166
|
+
try {
|
|
2167
|
+
const pkgPath = path12.join(projectRoot, "package.json");
|
|
2168
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
2169
|
+
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2170
|
+
} catch {
|
|
2171
|
+
return false;
|
|
1384
2172
|
}
|
|
1385
|
-
lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
|
|
1386
|
-
return lines;
|
|
1387
2173
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
const
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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)}`);
|
|
2174
|
+
|
|
2175
|
+
// src/utils/filter-confidence.ts
|
|
2176
|
+
function filterHighConfidence(conventions, meta) {
|
|
2177
|
+
if (!meta) return conventions;
|
|
2178
|
+
const filtered = {};
|
|
2179
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
2180
|
+
if (value === void 0) continue;
|
|
2181
|
+
const convMeta = meta[key];
|
|
2182
|
+
if (!convMeta || convMeta.confidence === "high") {
|
|
2183
|
+
filtered[key] = value;
|
|
1419
2184
|
}
|
|
1420
2185
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2186
|
+
return filtered;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/utils/update-gitignore.ts
|
|
2190
|
+
import * as fs13 from "fs";
|
|
2191
|
+
import * as path13 from "path";
|
|
2192
|
+
function updateGitignore(projectRoot) {
|
|
2193
|
+
const gitignorePath = path13.join(projectRoot, ".gitignore");
|
|
2194
|
+
let content = "";
|
|
2195
|
+
if (fs13.existsSync(gitignorePath)) {
|
|
2196
|
+
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1428
2197
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if (ext) {
|
|
1435
|
-
lines.push(ext);
|
|
2198
|
+
if (!content.includes(".viberails/scan-result.json")) {
|
|
2199
|
+
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
2200
|
+
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
2201
|
+
`;
|
|
2202
|
+
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1436
2203
|
}
|
|
1437
|
-
lines.push(...formatRulesText(config));
|
|
1438
|
-
return lines.join("\n");
|
|
1439
2204
|
}
|
|
1440
2205
|
|
|
1441
2206
|
// src/utils/write-generated-files.ts
|
|
1442
|
-
import * as
|
|
1443
|
-
import * as
|
|
2207
|
+
import * as fs14 from "fs";
|
|
2208
|
+
import * as path14 from "path";
|
|
1444
2209
|
import { generateContext } from "@viberails/context";
|
|
1445
2210
|
var CONTEXT_DIR = ".viberails";
|
|
1446
2211
|
var CONTEXT_FILE = "context.md";
|
|
1447
2212
|
var SCAN_RESULT_FILE = "scan-result.json";
|
|
1448
2213
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1449
|
-
const contextDir =
|
|
2214
|
+
const contextDir = path14.join(projectRoot, CONTEXT_DIR);
|
|
1450
2215
|
try {
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
2216
|
+
if (!fs14.existsSync(contextDir)) {
|
|
2217
|
+
fs14.mkdirSync(contextDir, { recursive: true });
|
|
1453
2218
|
}
|
|
1454
2219
|
const context = generateContext(config);
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2220
|
+
fs14.writeFileSync(path14.join(contextDir, CONTEXT_FILE), context);
|
|
2221
|
+
fs14.writeFileSync(
|
|
2222
|
+
path14.join(contextDir, SCAN_RESULT_FILE),
|
|
1458
2223
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1459
2224
|
`
|
|
1460
2225
|
);
|
|
@@ -1465,38 +2230,41 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1465
2230
|
}
|
|
1466
2231
|
|
|
1467
2232
|
// src/commands/init-hooks.ts
|
|
1468
|
-
import * as
|
|
1469
|
-
import * as
|
|
1470
|
-
import
|
|
2233
|
+
import * as fs15 from "fs";
|
|
2234
|
+
import * as path15 from "path";
|
|
2235
|
+
import chalk8 from "chalk";
|
|
2236
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1471
2237
|
function setupPreCommitHook(projectRoot) {
|
|
1472
|
-
const lefthookPath =
|
|
1473
|
-
if (
|
|
2238
|
+
const lefthookPath = path15.join(projectRoot, "lefthook.yml");
|
|
2239
|
+
if (fs15.existsSync(lefthookPath)) {
|
|
1474
2240
|
addLefthookPreCommit(lefthookPath);
|
|
1475
|
-
console.log(` ${
|
|
1476
|
-
return;
|
|
2241
|
+
console.log(` ${chalk8.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2242
|
+
return "lefthook.yml";
|
|
1477
2243
|
}
|
|
1478
|
-
const huskyDir =
|
|
1479
|
-
if (
|
|
2244
|
+
const huskyDir = path15.join(projectRoot, ".husky");
|
|
2245
|
+
if (fs15.existsSync(huskyDir)) {
|
|
1480
2246
|
writeHuskyPreCommit(huskyDir);
|
|
1481
|
-
console.log(` ${
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
const gitDir =
|
|
1485
|
-
if (
|
|
1486
|
-
const hooksDir =
|
|
1487
|
-
if (!
|
|
1488
|
-
|
|
2247
|
+
console.log(` ${chalk8.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2248
|
+
return ".husky/pre-commit";
|
|
2249
|
+
}
|
|
2250
|
+
const gitDir = path15.join(projectRoot, ".git");
|
|
2251
|
+
if (fs15.existsSync(gitDir)) {
|
|
2252
|
+
const hooksDir = path15.join(gitDir, "hooks");
|
|
2253
|
+
if (!fs15.existsSync(hooksDir)) {
|
|
2254
|
+
fs15.mkdirSync(hooksDir, { recursive: true });
|
|
1489
2255
|
}
|
|
1490
2256
|
writeGitHookPreCommit(hooksDir);
|
|
1491
|
-
console.log(` ${
|
|
2257
|
+
console.log(` ${chalk8.green("\u2713")} .git/hooks/pre-commit`);
|
|
2258
|
+
return ".git/hooks/pre-commit";
|
|
1492
2259
|
}
|
|
2260
|
+
return void 0;
|
|
1493
2261
|
}
|
|
1494
2262
|
function writeGitHookPreCommit(hooksDir) {
|
|
1495
|
-
const hookPath =
|
|
1496
|
-
if (
|
|
1497
|
-
const existing =
|
|
2263
|
+
const hookPath = path15.join(hooksDir, "pre-commit");
|
|
2264
|
+
if (fs15.existsSync(hookPath)) {
|
|
2265
|
+
const existing = fs15.readFileSync(hookPath, "utf-8");
|
|
1498
2266
|
if (existing.includes("viberails")) return;
|
|
1499
|
-
|
|
2267
|
+
fs15.writeFileSync(
|
|
1500
2268
|
hookPath,
|
|
1501
2269
|
`${existing.trimEnd()}
|
|
1502
2270
|
|
|
@@ -1513,71 +2281,51 @@ npx viberails check --staged
|
|
|
1513
2281
|
"npx viberails check --staged",
|
|
1514
2282
|
""
|
|
1515
2283
|
].join("\n");
|
|
1516
|
-
|
|
2284
|
+
fs15.writeFileSync(hookPath, script, { mode: 493 });
|
|
1517
2285
|
}
|
|
1518
2286
|
function addLefthookPreCommit(lefthookPath) {
|
|
1519
|
-
const content =
|
|
2287
|
+
const content = fs15.readFileSync(lefthookPath, "utf-8");
|
|
1520
2288
|
if (content.includes("viberails")) return;
|
|
1521
|
-
const
|
|
1522
|
-
if (
|
|
1523
|
-
|
|
1524
|
-
"\n"
|
|
1525
|
-
);
|
|
1526
|
-
const updated = `${content.trimEnd()}
|
|
1527
|
-
${commandBlock}
|
|
1528
|
-
`;
|
|
1529
|
-
fs11.writeFileSync(lefthookPath, updated);
|
|
1530
|
-
} else {
|
|
1531
|
-
const section = [
|
|
1532
|
-
"",
|
|
1533
|
-
"pre-commit:",
|
|
1534
|
-
" commands:",
|
|
1535
|
-
" viberails:",
|
|
1536
|
-
" run: npx viberails check --staged"
|
|
1537
|
-
].join("\n");
|
|
1538
|
-
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
1539
|
-
${section}
|
|
1540
|
-
`);
|
|
2289
|
+
const doc = parseYaml(content) ?? {};
|
|
2290
|
+
if (!doc["pre-commit"]) {
|
|
2291
|
+
doc["pre-commit"] = { commands: {} };
|
|
1541
2292
|
}
|
|
2293
|
+
if (!doc["pre-commit"].commands) {
|
|
2294
|
+
doc["pre-commit"].commands = {};
|
|
2295
|
+
}
|
|
2296
|
+
doc["pre-commit"].commands.viberails = {
|
|
2297
|
+
run: "npx viberails check --staged"
|
|
2298
|
+
};
|
|
2299
|
+
fs15.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
1542
2300
|
}
|
|
1543
2301
|
function detectHookManager(projectRoot) {
|
|
1544
|
-
if (
|
|
1545
|
-
if (
|
|
1546
|
-
if (
|
|
2302
|
+
if (fs15.existsSync(path15.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
2303
|
+
if (fs15.existsSync(path15.join(projectRoot, ".husky"))) return "Husky";
|
|
2304
|
+
if (fs15.existsSync(path15.join(projectRoot, ".git"))) return "git hook";
|
|
1547
2305
|
return void 0;
|
|
1548
2306
|
}
|
|
1549
2307
|
function setupClaudeCodeHook(projectRoot) {
|
|
1550
|
-
const claudeDir =
|
|
1551
|
-
if (!
|
|
1552
|
-
|
|
2308
|
+
const claudeDir = path15.join(projectRoot, ".claude");
|
|
2309
|
+
if (!fs15.existsSync(claudeDir)) {
|
|
2310
|
+
fs15.mkdirSync(claudeDir, { recursive: true });
|
|
1553
2311
|
}
|
|
1554
|
-
const settingsPath =
|
|
2312
|
+
const settingsPath = path15.join(claudeDir, "settings.json");
|
|
1555
2313
|
let settings = {};
|
|
1556
|
-
if (
|
|
2314
|
+
if (fs15.existsSync(settingsPath)) {
|
|
1557
2315
|
try {
|
|
1558
|
-
settings = JSON.parse(
|
|
2316
|
+
settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
|
|
1559
2317
|
} catch {
|
|
1560
2318
|
console.warn(
|
|
1561
|
-
` ${
|
|
2319
|
+
` ${chalk8.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
1562
2320
|
);
|
|
1563
|
-
|
|
2321
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk8.cyan("viberails init --force")}`);
|
|
2322
|
+
return;
|
|
1564
2323
|
}
|
|
1565
2324
|
}
|
|
1566
2325
|
const hooks = settings.hooks ?? {};
|
|
1567
2326
|
const existing = hooks.PostToolUse ?? [];
|
|
1568
2327
|
if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
|
|
1569
|
-
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;
|
|
2328
|
+
const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
|
|
1581
2329
|
hooks.PostToolUse = [
|
|
1582
2330
|
...existing,
|
|
1583
2331
|
{
|
|
@@ -1591,228 +2339,406 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1591
2339
|
}
|
|
1592
2340
|
];
|
|
1593
2341
|
settings.hooks = hooks;
|
|
1594
|
-
|
|
2342
|
+
fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1595
2343
|
`);
|
|
1596
|
-
console.log(` ${
|
|
2344
|
+
console.log(` ${chalk8.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1597
2345
|
}
|
|
1598
2346
|
function setupClaudeMdReference(projectRoot) {
|
|
1599
|
-
const claudeMdPath =
|
|
2347
|
+
const claudeMdPath = path15.join(projectRoot, "CLAUDE.md");
|
|
1600
2348
|
let content = "";
|
|
1601
|
-
if (
|
|
1602
|
-
content =
|
|
2349
|
+
if (fs15.existsSync(claudeMdPath)) {
|
|
2350
|
+
content = fs15.readFileSync(claudeMdPath, "utf-8");
|
|
1603
2351
|
}
|
|
1604
2352
|
if (content.includes("@.viberails/context.md")) return;
|
|
1605
2353
|
const ref = "\n@.viberails/context.md\n";
|
|
1606
2354
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1607
|
-
|
|
1608
|
-
console.log(` ${
|
|
2355
|
+
fs15.writeFileSync(claudeMdPath, prefix + ref);
|
|
2356
|
+
console.log(` ${chalk8.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2357
|
+
}
|
|
2358
|
+
function setupGithubAction(projectRoot, packageManager) {
|
|
2359
|
+
const workflowDir = path15.join(projectRoot, ".github", "workflows");
|
|
2360
|
+
const workflowPath = path15.join(workflowDir, "viberails.yml");
|
|
2361
|
+
if (fs15.existsSync(workflowPath)) {
|
|
2362
|
+
const existing = fs15.readFileSync(workflowPath, "utf-8");
|
|
2363
|
+
if (existing.includes("viberails")) return void 0;
|
|
2364
|
+
}
|
|
2365
|
+
fs15.mkdirSync(workflowDir, { recursive: true });
|
|
2366
|
+
const pm = packageManager || "npm";
|
|
2367
|
+
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
2368
|
+
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
2369
|
+
const lines = [
|
|
2370
|
+
"name: viberails",
|
|
2371
|
+
"",
|
|
2372
|
+
"on:",
|
|
2373
|
+
" pull_request:",
|
|
2374
|
+
" branches: [main]",
|
|
2375
|
+
"",
|
|
2376
|
+
"jobs:",
|
|
2377
|
+
" check:",
|
|
2378
|
+
" runs-on: ubuntu-latest",
|
|
2379
|
+
" steps:",
|
|
2380
|
+
" - uses: actions/checkout@v4",
|
|
2381
|
+
" with:",
|
|
2382
|
+
" fetch-depth: 0",
|
|
2383
|
+
""
|
|
2384
|
+
];
|
|
2385
|
+
if (pm === "pnpm") {
|
|
2386
|
+
lines.push(" - uses: pnpm/action-setup@v4", "");
|
|
2387
|
+
}
|
|
2388
|
+
lines.push(
|
|
2389
|
+
" - uses: actions/setup-node@v4",
|
|
2390
|
+
" with:",
|
|
2391
|
+
" node-version: 22",
|
|
2392
|
+
pm !== "npm" ? ` cache: ${pm}` : "",
|
|
2393
|
+
"",
|
|
2394
|
+
` - run: ${installCmd}`,
|
|
2395
|
+
` - run: ${runPrefix} viberails check --enforce --diff-base origin/\${{ github.event.pull_request.base.ref }}`,
|
|
2396
|
+
""
|
|
2397
|
+
);
|
|
2398
|
+
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
2399
|
+
fs15.writeFileSync(workflowPath, content);
|
|
2400
|
+
return ".github/workflows/viberails.yml";
|
|
1609
2401
|
}
|
|
1610
2402
|
function writeHuskyPreCommit(huskyDir) {
|
|
1611
|
-
const hookPath =
|
|
1612
|
-
if (
|
|
1613
|
-
const existing =
|
|
2403
|
+
const hookPath = path15.join(huskyDir, "pre-commit");
|
|
2404
|
+
if (fs15.existsSync(hookPath)) {
|
|
2405
|
+
const existing = fs15.readFileSync(hookPath, "utf-8");
|
|
1614
2406
|
if (!existing.includes("viberails")) {
|
|
1615
|
-
|
|
2407
|
+
fs15.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1616
2408
|
npx viberails check --staged
|
|
1617
2409
|
`);
|
|
1618
2410
|
}
|
|
1619
2411
|
return;
|
|
1620
2412
|
}
|
|
1621
|
-
|
|
2413
|
+
fs15.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1622
2414
|
}
|
|
1623
2415
|
|
|
1624
|
-
// src/commands/init.ts
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2416
|
+
// src/commands/init-hooks-extra.ts
|
|
2417
|
+
import * as fs16 from "fs";
|
|
2418
|
+
import * as path16 from "path";
|
|
2419
|
+
import chalk9 from "chalk";
|
|
2420
|
+
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
2421
|
+
function addPreCommitStep(projectRoot, name, command, marker) {
|
|
2422
|
+
const lefthookPath = path16.join(projectRoot, "lefthook.yml");
|
|
2423
|
+
if (fs16.existsSync(lefthookPath)) {
|
|
2424
|
+
const content = fs16.readFileSync(lefthookPath, "utf-8");
|
|
2425
|
+
if (content.includes(marker)) return void 0;
|
|
2426
|
+
const doc = parseYaml2(content) ?? {};
|
|
2427
|
+
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
2428
|
+
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
2429
|
+
doc["pre-commit"].commands[name] = { run: command };
|
|
2430
|
+
fs16.writeFileSync(lefthookPath, stringifyYaml2(doc));
|
|
2431
|
+
return "lefthook.yml";
|
|
2432
|
+
}
|
|
2433
|
+
const huskyDir = path16.join(projectRoot, ".husky");
|
|
2434
|
+
if (fs16.existsSync(huskyDir)) {
|
|
2435
|
+
const hookPath = path16.join(huskyDir, "pre-commit");
|
|
2436
|
+
if (fs16.existsSync(hookPath)) {
|
|
2437
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
2438
|
+
if (existing.includes(marker)) return void 0;
|
|
2439
|
+
fs16.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2440
|
+
${command}
|
|
2441
|
+
`);
|
|
2442
|
+
} else {
|
|
2443
|
+
fs16.writeFileSync(hookPath, `#!/bin/sh
|
|
2444
|
+
${command}
|
|
2445
|
+
`, { mode: 493 });
|
|
2446
|
+
}
|
|
2447
|
+
return ".husky/pre-commit";
|
|
2448
|
+
}
|
|
2449
|
+
const gitDir = path16.join(projectRoot, ".git");
|
|
2450
|
+
if (fs16.existsSync(gitDir)) {
|
|
2451
|
+
const hooksDir = path16.join(gitDir, "hooks");
|
|
2452
|
+
if (!fs16.existsSync(hooksDir)) fs16.mkdirSync(hooksDir, { recursive: true });
|
|
2453
|
+
const hookPath = path16.join(hooksDir, "pre-commit");
|
|
2454
|
+
if (fs16.existsSync(hookPath)) {
|
|
2455
|
+
const existing = fs16.readFileSync(hookPath, "utf-8");
|
|
2456
|
+
if (existing.includes(marker)) return void 0;
|
|
2457
|
+
fs16.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2458
|
+
|
|
2459
|
+
# ${name}
|
|
2460
|
+
${command}
|
|
2461
|
+
`);
|
|
2462
|
+
} else {
|
|
2463
|
+
fs16.writeFileSync(hookPath, `#!/bin/sh
|
|
2464
|
+
# Generated by viberails
|
|
2465
|
+
|
|
2466
|
+
# ${name}
|
|
2467
|
+
${command}
|
|
2468
|
+
`, {
|
|
2469
|
+
mode: 493
|
|
2470
|
+
});
|
|
1634
2471
|
}
|
|
2472
|
+
return ".git/hooks/pre-commit";
|
|
1635
2473
|
}
|
|
1636
|
-
return
|
|
2474
|
+
return void 0;
|
|
1637
2475
|
}
|
|
1638
|
-
function
|
|
1639
|
-
|
|
1640
|
-
|
|
2476
|
+
function setupTypecheckHook(projectRoot) {
|
|
2477
|
+
const target = addPreCommitStep(projectRoot, "typecheck", "npx tsc --noEmit", "tsc");
|
|
2478
|
+
if (target) {
|
|
2479
|
+
console.log(` ${chalk9.green("\u2713")} ${target} \u2014 added typecheck (tsc --noEmit)`);
|
|
2480
|
+
}
|
|
2481
|
+
return target;
|
|
1641
2482
|
}
|
|
1642
|
-
function
|
|
1643
|
-
|
|
1644
|
-
|
|
2483
|
+
function setupLintHook(projectRoot, linter) {
|
|
2484
|
+
const command = linter === "biome" ? "npx biome check ." : "npx eslint .";
|
|
2485
|
+
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
2486
|
+
const target = addPreCommitStep(projectRoot, "lint", command, linter);
|
|
2487
|
+
if (target) {
|
|
2488
|
+
console.log(` ${chalk9.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
2489
|
+
}
|
|
2490
|
+
return target;
|
|
2491
|
+
}
|
|
2492
|
+
function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
2493
|
+
const created = [];
|
|
2494
|
+
if (integrations.preCommitHook) {
|
|
2495
|
+
const t = setupPreCommitHook(projectRoot);
|
|
2496
|
+
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
2497
|
+
}
|
|
2498
|
+
if (integrations.typecheckHook) {
|
|
2499
|
+
const t = setupTypecheckHook(projectRoot);
|
|
2500
|
+
if (t) created.push(`${t} \u2014 added typecheck`);
|
|
2501
|
+
}
|
|
2502
|
+
if (integrations.lintHook && opts.linter) {
|
|
2503
|
+
const t = setupLintHook(projectRoot, opts.linter);
|
|
2504
|
+
if (t) created.push(`${t} \u2014 added lint check`);
|
|
2505
|
+
}
|
|
2506
|
+
if (integrations.claudeCodeHook) {
|
|
2507
|
+
setupClaudeCodeHook(projectRoot);
|
|
2508
|
+
created.push(".claude/settings.json \u2014 added viberails hook");
|
|
2509
|
+
}
|
|
2510
|
+
if (integrations.claudeMdRef) {
|
|
2511
|
+
setupClaudeMdReference(projectRoot);
|
|
2512
|
+
created.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
2513
|
+
}
|
|
2514
|
+
if (integrations.githubAction) {
|
|
2515
|
+
const t = setupGithubAction(projectRoot, opts.packageManager ?? "npm");
|
|
2516
|
+
if (t) created.push(`${t} \u2014 blocks PRs on violations`);
|
|
2517
|
+
}
|
|
2518
|
+
return created;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/commands/init.ts
|
|
2522
|
+
var CONFIG_FILE4 = "viberails.config.json";
|
|
2523
|
+
function getExemptedPackages(config) {
|
|
2524
|
+
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
1645
2525
|
}
|
|
1646
2526
|
async function initCommand(options, cwd) {
|
|
1647
|
-
const
|
|
1648
|
-
const projectRoot = findProjectRoot(startDir);
|
|
2527
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
1649
2528
|
if (!projectRoot) {
|
|
1650
2529
|
throw new Error(
|
|
1651
|
-
"No package.json found
|
|
2530
|
+
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
1652
2531
|
);
|
|
1653
2532
|
}
|
|
1654
|
-
const configPath =
|
|
1655
|
-
if (
|
|
2533
|
+
const configPath = path17.join(projectRoot, CONFIG_FILE4);
|
|
2534
|
+
if (fs17.existsSync(configPath) && !options.force) {
|
|
1656
2535
|
console.log(
|
|
1657
|
-
`${
|
|
1658
|
-
Run ${
|
|
2536
|
+
`${chalk10.yellow("!")} viberails is already initialized.
|
|
2537
|
+
Run ${chalk10.cyan("viberails sync")} to update, or ${chalk10.cyan("viberails init --force")} to start fresh.`
|
|
1659
2538
|
);
|
|
1660
2539
|
return;
|
|
1661
2540
|
}
|
|
1662
|
-
if (options.yes)
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2541
|
+
if (options.yes) return initNonInteractive(projectRoot, configPath);
|
|
2542
|
+
await initInteractive(projectRoot, configPath, options);
|
|
2543
|
+
}
|
|
2544
|
+
async function initNonInteractive(projectRoot, configPath) {
|
|
2545
|
+
console.log(chalk10.dim("Scanning project..."));
|
|
2546
|
+
const scanResult = await scan(projectRoot);
|
|
2547
|
+
const config = generateConfig(scanResult);
|
|
2548
|
+
for (const pkg of config.packages) {
|
|
2549
|
+
const pkgMeta = config._meta?.packages?.[pkg.path]?.conventions;
|
|
2550
|
+
pkg.conventions = filterHighConfidence(pkg.conventions ?? {}, pkgMeta);
|
|
2551
|
+
}
|
|
2552
|
+
displayMissingPrereqs(checkCoveragePrereqs(projectRoot, scanResult));
|
|
2553
|
+
displayScanResults(scanResult);
|
|
2554
|
+
displayRulesPreview(config);
|
|
2555
|
+
const exempted = getExemptedPackages(config);
|
|
2556
|
+
if (exempted.length > 0) {
|
|
2557
|
+
console.log(
|
|
2558
|
+
` ${chalk10.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk10.dim("(types-only)")}`
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
if (config.packages.length > 1) {
|
|
2562
|
+
console.log(chalk10.dim("Building import graph..."));
|
|
2563
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2564
|
+
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2565
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
2566
|
+
const inferred = inferBoundaries(graph);
|
|
2567
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
2568
|
+
if (denyCount > 0) {
|
|
2569
|
+
config.boundaries = inferred;
|
|
2570
|
+
config.rules.enforceBoundaries = true;
|
|
2571
|
+
console.log(` Inferred ${denyCount} boundary rules`);
|
|
1684
2572
|
}
|
|
1685
|
-
|
|
2573
|
+
}
|
|
2574
|
+
const compacted = compactConfig2(config);
|
|
2575
|
+
fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
1686
2576
|
`);
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2577
|
+
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2578
|
+
updateGitignore(projectRoot);
|
|
2579
|
+
setupClaudeCodeHook(projectRoot);
|
|
2580
|
+
setupClaudeMdReference(projectRoot);
|
|
2581
|
+
const preCommitTarget = setupPreCommitHook(projectRoot);
|
|
2582
|
+
const rootPkg = config.packages[0];
|
|
2583
|
+
const rootPkgPm = rootPkg?.stack?.packageManager ?? "npm";
|
|
2584
|
+
const actionTarget = setupGithubAction(projectRoot, rootPkgPm);
|
|
2585
|
+
const typecheckTarget = rootPkg?.stack?.language === "typescript" ? setupTypecheckHook(projectRoot) : void 0;
|
|
2586
|
+
const linter = rootPkg?.stack?.linter?.split("@")[0];
|
|
2587
|
+
const lintTarget = linter ? setupLintHook(projectRoot, linter) : void 0;
|
|
2588
|
+
const ok = chalk10.green("\u2713");
|
|
2589
|
+
const created = [
|
|
2590
|
+
`${ok} ${path17.basename(configPath)}`,
|
|
2591
|
+
`${ok} .viberails/context.md`,
|
|
2592
|
+
`${ok} .viberails/scan-result.json`,
|
|
2593
|
+
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
2594
|
+
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
2595
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk10.yellow("!")} pre-commit hook skipped`,
|
|
2596
|
+
typecheckTarget ? `${ok} ${typecheckTarget} \u2014 added typecheck` : "",
|
|
2597
|
+
lintTarget ? `${ok} ${lintTarget} \u2014 added lint check` : "",
|
|
2598
|
+
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
2599
|
+
].filter(Boolean);
|
|
2600
|
+
console.log(`
|
|
2601
|
+
Created:
|
|
2602
|
+
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
2603
|
+
}
|
|
2604
|
+
async function initInteractive(projectRoot, configPath, options) {
|
|
2605
|
+
clack7.intro("viberails");
|
|
2606
|
+
if (fs17.existsSync(configPath) && options.force) {
|
|
2607
|
+
const replace = await confirmDangerous(
|
|
2608
|
+
`${path17.basename(configPath)} already exists and will be replaced. Continue?`
|
|
2609
|
+
);
|
|
2610
|
+
if (!replace) {
|
|
2611
|
+
clack7.outro("Aborted. No files were written.");
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
1696
2614
|
}
|
|
1697
|
-
|
|
1698
|
-
const s = clack2.spinner();
|
|
2615
|
+
const s = clack7.spinner();
|
|
1699
2616
|
s.start("Scanning project...");
|
|
1700
2617
|
const scanResult = await scan(projectRoot);
|
|
1701
2618
|
const config = generateConfig(scanResult);
|
|
1702
2619
|
s.stop("Scan complete");
|
|
2620
|
+
const prereqResult = await promptMissingPrereqs(
|
|
2621
|
+
projectRoot,
|
|
2622
|
+
checkCoveragePrereqs(projectRoot, scanResult)
|
|
2623
|
+
);
|
|
2624
|
+
if (prereqResult.disableCoverage) {
|
|
2625
|
+
config.rules.testCoverage = 0;
|
|
2626
|
+
}
|
|
1703
2627
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1704
|
-
|
|
1705
|
-
"No source files detected.
|
|
2628
|
+
clack7.log.warn(
|
|
2629
|
+
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
1706
2630
|
);
|
|
1707
2631
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
2632
|
+
clack7.note(formatScanResultsText(scanResult), "Scan results");
|
|
2633
|
+
const rulesLines = formatRulesText(config);
|
|
2634
|
+
const exemptedPkgs = getExemptedPackages(config);
|
|
2635
|
+
if (exemptedPkgs.length > 0)
|
|
2636
|
+
rulesLines.push(`Auto-exempted from coverage: ${exemptedPkgs.join(", ")} (types-only)`);
|
|
2637
|
+
clack7.note(rulesLines.join("\n"), "Rules");
|
|
1710
2638
|
const decision = await promptInitDecision();
|
|
1711
2639
|
if (decision === "customize") {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
"Rules"
|
|
1715
|
-
);
|
|
1716
|
-
const overrides = await promptRuleCustomization({
|
|
2640
|
+
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2641
|
+
const overrides = await promptRuleMenu({
|
|
1717
2642
|
maxFileLines: config.rules.maxFileLines,
|
|
1718
|
-
|
|
2643
|
+
testCoverage: config.rules.testCoverage,
|
|
2644
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
1719
2645
|
enforceNaming: config.rules.enforceNaming,
|
|
1720
|
-
|
|
1721
|
-
|
|
2646
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2647
|
+
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
2648
|
+
coverageCommand: config.defaults?.coverage?.command,
|
|
2649
|
+
packageOverrides: config.packages
|
|
1722
2650
|
});
|
|
2651
|
+
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1723
2652
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1724
|
-
config.rules.
|
|
2653
|
+
config.rules.testCoverage = overrides.testCoverage;
|
|
2654
|
+
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
1725
2655
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1726
|
-
config.
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
)
|
|
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
|
+
}
|
|
1732
2676
|
}
|
|
1733
2677
|
}
|
|
1734
|
-
if (config.
|
|
1735
|
-
|
|
2678
|
+
if (config.packages.length > 1) {
|
|
2679
|
+
clack7.note(
|
|
1736
2680
|
"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
2681
|
"Boundaries"
|
|
1738
2682
|
);
|
|
1739
|
-
const shouldInfer = await
|
|
2683
|
+
const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
|
|
1740
2684
|
if (shouldInfer) {
|
|
1741
|
-
const bs =
|
|
2685
|
+
const bs = clack7.spinner();
|
|
1742
2686
|
bs.start("Building import graph...");
|
|
1743
2687
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1744
|
-
const packages = resolveWorkspacePackages(projectRoot, config.
|
|
1745
|
-
const graph = await buildImportGraph(projectRoot, {
|
|
1746
|
-
packages,
|
|
1747
|
-
ignore: config.ignore
|
|
1748
|
-
});
|
|
2688
|
+
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2689
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
1749
2690
|
const inferred = inferBoundaries(graph);
|
|
1750
2691
|
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1751
2692
|
if (denyCount > 0) {
|
|
1752
2693
|
config.boundaries = inferred;
|
|
1753
2694
|
config.rules.enforceBoundaries = true;
|
|
1754
2695
|
bs.stop(`Inferred ${denyCount} boundary rules`);
|
|
2696
|
+
const boundaryLines = Object.entries(inferred.deny).map(([pkg, denied]) => `${pkg} must NOT import from: ${denied.join(", ")}`).join("\n");
|
|
2697
|
+
clack7.note(boundaryLines, "Boundary rules");
|
|
1755
2698
|
} else {
|
|
1756
2699
|
bs.stop("No boundary rules inferred");
|
|
1757
2700
|
}
|
|
1758
2701
|
}
|
|
1759
2702
|
}
|
|
1760
2703
|
const hookManager = detectHookManager(projectRoot);
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2704
|
+
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
2705
|
+
const integrations = await promptIntegrations(hookManager, {
|
|
2706
|
+
isTypeScript: rootPkgStack?.language === "typescript",
|
|
2707
|
+
linter: rootPkgStack?.linter?.split("@")[0]
|
|
2708
|
+
});
|
|
2709
|
+
const shouldWrite = await confirm3("Write configuration and set up selected integrations?");
|
|
2710
|
+
if (!shouldWrite) {
|
|
2711
|
+
clack7.outro("Aborted. No files were written.");
|
|
2712
|
+
return;
|
|
1767
2713
|
}
|
|
1768
|
-
|
|
2714
|
+
const compacted = compactConfig2(config);
|
|
2715
|
+
fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
1769
2716
|
`);
|
|
1770
2717
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1771
2718
|
updateGitignore(projectRoot);
|
|
1772
2719
|
const createdFiles = [
|
|
1773
|
-
|
|
2720
|
+
path17.basename(configPath),
|
|
1774
2721
|
".viberails/context.md",
|
|
1775
|
-
".viberails/scan-result.json"
|
|
2722
|
+
".viberails/scan-result.json",
|
|
2723
|
+
...setupSelectedIntegrations(projectRoot, integrations, {
|
|
2724
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
2725
|
+
packageManager: rootPkgStack?.packageManager
|
|
2726
|
+
})
|
|
1776
2727
|
];
|
|
1777
|
-
|
|
1778
|
-
setupPreCommitHook(projectRoot);
|
|
1779
|
-
const hookMgr = detectHookManager(projectRoot);
|
|
1780
|
-
if (hookMgr) {
|
|
1781
|
-
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
if (integrations.claudeCodeHook) {
|
|
1785
|
-
setupClaudeCodeHook(projectRoot);
|
|
1786
|
-
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1787
|
-
}
|
|
1788
|
-
if (integrations.claudeMdRef) {
|
|
1789
|
-
setupClaudeMdReference(projectRoot);
|
|
1790
|
-
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1791
|
-
}
|
|
1792
|
-
clack2.log.success(`Created:
|
|
2728
|
+
clack7.log.success(`Created:
|
|
1793
2729
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
let content = "";
|
|
1799
|
-
if (fs12.existsSync(gitignorePath)) {
|
|
1800
|
-
content = fs12.readFileSync(gitignorePath, "utf-8");
|
|
1801
|
-
}
|
|
1802
|
-
if (!content.includes(".viberails/scan-result.json")) {
|
|
1803
|
-
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1804
|
-
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1805
|
-
`;
|
|
1806
|
-
fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1807
|
-
}
|
|
2730
|
+
clack7.outro(
|
|
2731
|
+
`Done! Next: review viberails.config.json, then run viberails check
|
|
2732
|
+
${chalk10.dim("Tip: use")} ${chalk10.cyan("viberails check --enforce")} ${chalk10.dim("in CI to block PRs on violations.")}`
|
|
2733
|
+
);
|
|
1808
2734
|
}
|
|
1809
2735
|
|
|
1810
2736
|
// src/commands/sync.ts
|
|
1811
|
-
import * as
|
|
1812
|
-
import * as
|
|
1813
|
-
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
2737
|
+
import * as fs18 from "fs";
|
|
2738
|
+
import * as path18 from "path";
|
|
2739
|
+
import { compactConfig as compactConfig3, loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
1814
2740
|
import { scan as scan2 } from "@viberails/scanner";
|
|
1815
|
-
import
|
|
2741
|
+
import chalk11 from "chalk";
|
|
1816
2742
|
|
|
1817
2743
|
// src/utils/diff-configs.ts
|
|
1818
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";
|
|
@@ -1833,11 +2759,8 @@ function displayStackName(s) {
|
|
|
1833
2759
|
const display = allMaps[name] ?? name;
|
|
1834
2760
|
return version ? `${display} ${version}` : display;
|
|
1835
2761
|
}
|
|
1836
|
-
function
|
|
1837
|
-
return
|
|
1838
|
-
}
|
|
1839
|
-
function isDetected(cv) {
|
|
1840
|
-
return typeof cv !== "string" && cv._detected === true;
|
|
2762
|
+
function isNewlyDetected(config, pkgPath, key) {
|
|
2763
|
+
return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
|
|
1841
2764
|
}
|
|
1842
2765
|
var STACK_FIELDS = [
|
|
1843
2766
|
"framework",
|
|
@@ -1864,59 +2787,66 @@ var STRUCTURE_FIELDS = [
|
|
|
1864
2787
|
{ key: "tests", label: "tests directory" },
|
|
1865
2788
|
{ key: "testPattern", label: "test pattern" }
|
|
1866
2789
|
];
|
|
1867
|
-
function
|
|
2790
|
+
function diffPackage(existing, merged, mergedConfig) {
|
|
1868
2791
|
const changes = [];
|
|
2792
|
+
const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
|
|
1869
2793
|
for (const field of STACK_FIELDS) {
|
|
1870
|
-
const oldVal = existing.stack[field];
|
|
1871
|
-
const newVal = merged.stack[field];
|
|
2794
|
+
const oldVal = existing.stack?.[field];
|
|
2795
|
+
const newVal = merged.stack?.[field];
|
|
1872
2796
|
if (!oldVal && newVal) {
|
|
1873
|
-
changes.push({
|
|
2797
|
+
changes.push({
|
|
2798
|
+
type: "added",
|
|
2799
|
+
description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
|
|
2800
|
+
});
|
|
1874
2801
|
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1875
2802
|
changes.push({
|
|
1876
2803
|
type: "changed",
|
|
1877
|
-
description:
|
|
2804
|
+
description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1878
2805
|
});
|
|
1879
2806
|
}
|
|
1880
2807
|
}
|
|
1881
2808
|
for (const key of CONVENTION_KEYS) {
|
|
1882
|
-
const oldVal = existing.conventions[key];
|
|
1883
|
-
const newVal = merged.conventions[key];
|
|
2809
|
+
const oldVal = existing.conventions?.[key];
|
|
2810
|
+
const newVal = merged.conventions?.[key];
|
|
1884
2811
|
const label = CONVENTION_LABELS3[key] ?? key;
|
|
1885
2812
|
if (!oldVal && newVal) {
|
|
1886
2813
|
changes.push({
|
|
1887
2814
|
type: "added",
|
|
1888
|
-
description:
|
|
2815
|
+
description: `${pkgPrefix}New convention: ${label} (${newVal})`
|
|
1889
2816
|
});
|
|
1890
|
-
} else if (oldVal && newVal &&
|
|
2817
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
2818
|
+
const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
|
|
1891
2819
|
changes.push({
|
|
1892
2820
|
type: "changed",
|
|
1893
|
-
description:
|
|
2821
|
+
description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
|
|
1894
2822
|
});
|
|
1895
2823
|
}
|
|
1896
2824
|
}
|
|
1897
2825
|
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1898
|
-
const oldVal = existing.structure[key];
|
|
1899
|
-
const newVal = merged.structure[key];
|
|
2826
|
+
const oldVal = existing.structure?.[key];
|
|
2827
|
+
const newVal = merged.structure?.[key];
|
|
1900
2828
|
if (!oldVal && newVal) {
|
|
1901
|
-
changes.push({
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
for (const pkg of merged.packages ?? []) {
|
|
1906
|
-
if (!existingPaths.has(pkg.path)) {
|
|
1907
|
-
changes.push({ type: "added", description: `New package: ${pkg.path}` });
|
|
2829
|
+
changes.push({
|
|
2830
|
+
type: "added",
|
|
2831
|
+
description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
|
|
2832
|
+
});
|
|
1908
2833
|
}
|
|
1909
2834
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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));
|
|
1915
2845
|
}
|
|
1916
2846
|
}
|
|
1917
|
-
for (const
|
|
1918
|
-
if (!
|
|
1919
|
-
changes.push({ type: "
|
|
2847
|
+
for (const mergedPkg of merged.packages) {
|
|
2848
|
+
if (!existingByPath.has(mergedPkg.path)) {
|
|
2849
|
+
changes.push({ type: "added", description: `New package: ${mergedPkg.path}` });
|
|
1920
2850
|
}
|
|
1921
2851
|
}
|
|
1922
2852
|
return changes;
|
|
@@ -1941,9 +2871,9 @@ function formatStatsDelta(oldStats, newStats) {
|
|
|
1941
2871
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
1942
2872
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
1943
2873
|
function loadPreviousStats(projectRoot) {
|
|
1944
|
-
const scanResultPath =
|
|
2874
|
+
const scanResultPath = path18.join(projectRoot, SCAN_RESULT_FILE2);
|
|
1945
2875
|
try {
|
|
1946
|
-
const raw =
|
|
2876
|
+
const raw = fs18.readFileSync(scanResultPath, "utf-8");
|
|
1947
2877
|
const parsed = JSON.parse(raw);
|
|
1948
2878
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
1949
2879
|
return parsed.statistics;
|
|
@@ -1960,44 +2890,47 @@ async function syncCommand(cwd) {
|
|
|
1960
2890
|
"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"
|
|
1961
2891
|
);
|
|
1962
2892
|
}
|
|
1963
|
-
const configPath =
|
|
2893
|
+
const configPath = path18.join(projectRoot, CONFIG_FILE5);
|
|
1964
2894
|
const existing = await loadConfig4(configPath);
|
|
1965
2895
|
const previousStats = loadPreviousStats(projectRoot);
|
|
1966
|
-
console.log(
|
|
2896
|
+
console.log(chalk11.dim("Scanning project..."));
|
|
1967
2897
|
const scanResult = await scan2(projectRoot);
|
|
1968
2898
|
const merged = mergeConfig(existing, scanResult);
|
|
1969
|
-
const
|
|
1970
|
-
const
|
|
1971
|
-
const
|
|
2899
|
+
const compacted = compactConfig3(merged);
|
|
2900
|
+
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
2901
|
+
const rawDisk = fs18.readFileSync(configPath, "utf-8").trim();
|
|
2902
|
+
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
2903
|
+
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
2904
|
+
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
1972
2905
|
const changes = configChanged ? diffConfigs(existing, merged) : [];
|
|
1973
2906
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
1974
2907
|
if (changes.length > 0 || statsDelta) {
|
|
1975
2908
|
console.log(`
|
|
1976
|
-
${
|
|
2909
|
+
${chalk11.bold("Changes:")}`);
|
|
1977
2910
|
for (const change of changes) {
|
|
1978
|
-
const icon = change.type === "removed" ?
|
|
2911
|
+
const icon = change.type === "removed" ? chalk11.red("-") : chalk11.green("+");
|
|
1979
2912
|
console.log(` ${icon} ${change.description}`);
|
|
1980
2913
|
}
|
|
1981
2914
|
if (statsDelta) {
|
|
1982
|
-
console.log(` ${
|
|
2915
|
+
console.log(` ${chalk11.dim(statsDelta)}`);
|
|
1983
2916
|
}
|
|
1984
2917
|
}
|
|
1985
|
-
|
|
2918
|
+
fs18.writeFileSync(configPath, `${compactedJson}
|
|
1986
2919
|
`);
|
|
1987
2920
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
1988
2921
|
console.log(`
|
|
1989
|
-
${
|
|
2922
|
+
${chalk11.bold("Synced:")}`);
|
|
1990
2923
|
if (configChanged) {
|
|
1991
|
-
console.log(` ${
|
|
2924
|
+
console.log(` ${chalk11.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
|
|
1992
2925
|
} else {
|
|
1993
|
-
console.log(` ${
|
|
2926
|
+
console.log(` ${chalk11.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
|
|
1994
2927
|
}
|
|
1995
|
-
console.log(` ${
|
|
1996
|
-
console.log(` ${
|
|
2928
|
+
console.log(` ${chalk11.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
2929
|
+
console.log(` ${chalk11.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
1997
2930
|
}
|
|
1998
2931
|
|
|
1999
2932
|
// src/index.ts
|
|
2000
|
-
var VERSION = "0.
|
|
2933
|
+
var VERSION = "0.5.0";
|
|
2001
2934
|
var program = new Command();
|
|
2002
2935
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
2003
2936
|
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) => {
|
|
@@ -2005,7 +2938,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
2005
2938
|
await initCommand(options);
|
|
2006
2939
|
} catch (err) {
|
|
2007
2940
|
const message = err instanceof Error ? err.message : String(err);
|
|
2008
|
-
console.error(`${
|
|
2941
|
+
console.error(`${chalk12.red("Error:")} ${message}`);
|
|
2009
2942
|
process.exit(1);
|
|
2010
2943
|
}
|
|
2011
2944
|
});
|
|
@@ -2014,22 +2947,28 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
2014
2947
|
await syncCommand();
|
|
2015
2948
|
} catch (err) {
|
|
2016
2949
|
const message = err instanceof Error ? err.message : String(err);
|
|
2017
|
-
console.error(`${
|
|
2950
|
+
console.error(`${chalk12.red("Error:")} ${message}`);
|
|
2018
2951
|
process.exit(1);
|
|
2019
2952
|
}
|
|
2020
2953
|
});
|
|
2021
|
-
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").action(
|
|
2954
|
+
program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--diff-base <ref>", "Only check files changed since <ref> (for CI on PRs)").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).option("--format <format>", "Output format: text (default) or json").option("--enforce", "Exit with error on violations (for CI)").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").action(
|
|
2022
2955
|
async (options) => {
|
|
2023
2956
|
try {
|
|
2957
|
+
if (options.hook) {
|
|
2958
|
+
const exitCode2 = await hookCheckCommand();
|
|
2959
|
+
process.exit(exitCode2);
|
|
2960
|
+
}
|
|
2024
2961
|
const exitCode = await checkCommand({
|
|
2025
2962
|
...options,
|
|
2963
|
+
diffBase: options.diffBase,
|
|
2964
|
+
enforce: options.enforce,
|
|
2026
2965
|
noBoundaries: options.boundaries === false,
|
|
2027
2966
|
format: options.format === "json" ? "json" : "text"
|
|
2028
2967
|
});
|
|
2029
2968
|
process.exit(exitCode);
|
|
2030
2969
|
} catch (err) {
|
|
2031
2970
|
const message = err instanceof Error ? err.message : String(err);
|
|
2032
|
-
console.error(`${
|
|
2971
|
+
console.error(`${chalk12.red("Error:")} ${message}`);
|
|
2033
2972
|
process.exit(1);
|
|
2034
2973
|
}
|
|
2035
2974
|
}
|
|
@@ -2040,7 +2979,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
2040
2979
|
process.exit(exitCode);
|
|
2041
2980
|
} catch (err) {
|
|
2042
2981
|
const message = err instanceof Error ? err.message : String(err);
|
|
2043
|
-
console.error(`${
|
|
2982
|
+
console.error(`${chalk12.red("Error:")} ${message}`);
|
|
2044
2983
|
process.exit(1);
|
|
2045
2984
|
}
|
|
2046
2985
|
});
|
|
@@ -2049,7 +2988,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
2049
2988
|
await boundariesCommand(options);
|
|
2050
2989
|
} catch (err) {
|
|
2051
2990
|
const message = err instanceof Error ? err.message : String(err);
|
|
2052
|
-
console.error(`${
|
|
2991
|
+
console.error(`${chalk12.red("Error:")} ${message}`);
|
|
2053
2992
|
process.exit(1);
|
|
2054
2993
|
}
|
|
2055
2994
|
});
|