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