viberails 0.4.0 → 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 +1512 -657
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1505 -650
- package/dist/index.js.map +1 -1
- package/package.json +6 -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,183 +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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
88
115
|
});
|
|
89
116
|
assertNotCancelled(result);
|
|
90
|
-
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
|
+
};
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{ value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
|
|
106
|
-
];
|
|
107
|
-
if (state.packageOverrides && state.packageOverrides.length > 0) {
|
|
108
|
-
const count = state.packageOverrides.length;
|
|
109
|
-
options.push({
|
|
110
|
-
value: "packageOverrides",
|
|
111
|
-
label: "Per-package overrides",
|
|
112
|
-
hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
|
|
113
|
-
});
|
|
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;
|
|
114
139
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
141
|
+
delete pkg.coverage;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return packages;
|
|
145
|
+
}
|
|
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
|
+
]
|
|
119
174
|
});
|
|
120
|
-
assertNotCancelled(
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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;
|
|
129
210
|
}
|
|
211
|
+
} else {
|
|
212
|
+
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
130
213
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
134
226
|
}
|
|
227
|
+
} else {
|
|
228
|
+
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
135
229
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const n = Number.parseInt(v, 10);
|
|
153
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
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 };
|
|
154
246
|
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
247
|
+
}
|
|
248
|
+
if (choice === "reset") {
|
|
249
|
+
if (target.rules) {
|
|
250
|
+
delete target.rules.testCoverage;
|
|
251
|
+
}
|
|
252
|
+
delete target.coverage;
|
|
253
|
+
}
|
|
254
|
+
normalizePackageOverrides(editablePackages);
|
|
158
255
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
256
|
+
}
|
|
257
|
+
return normalizePackageOverrides(packages);
|
|
258
|
+
}
|
|
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]}`);
|
|
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]}`);
|
|
166
283
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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}%`
|
|
316
|
+
});
|
|
317
|
+
options.push({
|
|
318
|
+
value: "enforceMissingTests",
|
|
319
|
+
label: "Enforce missing tests",
|
|
320
|
+
hint: state.enforceMissingTests ? "yes" : "no"
|
|
321
|
+
});
|
|
322
|
+
if (state.testCoverage > 0) {
|
|
323
|
+
options.push(
|
|
324
|
+
{
|
|
325
|
+
value: "coverageSummaryPath",
|
|
326
|
+
label: "Coverage summary path",
|
|
327
|
+
hint: state.coverageSummaryPath
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
value: "coverageCommand",
|
|
331
|
+
label: "Coverage command",
|
|
332
|
+
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
333
|
+
}
|
|
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`
|
|
171
340
|
});
|
|
172
|
-
assertNotCancelled(result);
|
|
173
|
-
state.enforceNaming = result;
|
|
174
341
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
191
384
|
});
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
}
|
|
194
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);
|
|
195
489
|
}
|
|
196
490
|
return {
|
|
197
491
|
maxFileLines: state.maxFileLines,
|
|
198
|
-
|
|
492
|
+
testCoverage: state.testCoverage,
|
|
493
|
+
enforceMissingTests: state.enforceMissingTests,
|
|
199
494
|
enforceNaming: state.enforceNaming,
|
|
200
|
-
|
|
495
|
+
fileNamingValue: state.fileNamingValue,
|
|
496
|
+
coverageSummaryPath: state.coverageSummaryPath,
|
|
497
|
+
coverageCommand: state.coverageCommand,
|
|
498
|
+
packageOverrides: state.packageOverrides
|
|
201
499
|
};
|
|
202
500
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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?",
|
|
207
522
|
options: [
|
|
208
523
|
{
|
|
209
|
-
value: "
|
|
210
|
-
label:
|
|
211
|
-
hint: "
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
value: "claude",
|
|
215
|
-
label: "Claude Code hook",
|
|
216
|
-
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"
|
|
217
527
|
},
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
label: "CLAUDE.md reference",
|
|
221
|
-
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
222
|
-
}
|
|
223
|
-
],
|
|
224
|
-
initialValues: ["preCommit", "claude", "claudeMd"],
|
|
225
|
-
required: false
|
|
528
|
+
{ value: "customize", label: "Let me customize rules" }
|
|
529
|
+
]
|
|
226
530
|
});
|
|
227
531
|
assertNotCancelled(result);
|
|
228
|
-
return
|
|
229
|
-
preCommitHook: result.includes("preCommit"),
|
|
230
|
-
claudeCodeHook: result.includes("claude"),
|
|
231
|
-
claudeMdRef: result.includes("claudeMd")
|
|
232
|
-
};
|
|
532
|
+
return result;
|
|
233
533
|
}
|
|
234
534
|
|
|
235
535
|
// src/utils/resolve-workspace-packages.ts
|
|
236
536
|
var fs2 = __toESM(require("fs"), 1);
|
|
237
537
|
var path2 = __toESM(require("path"), 1);
|
|
238
|
-
function resolveWorkspacePackages(projectRoot,
|
|
239
|
-
const
|
|
240
|
-
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;
|
|
241
543
|
const absPath = path2.join(projectRoot, relativePath);
|
|
242
544
|
const pkgJsonPath = path2.join(absPath, "package.json");
|
|
243
545
|
if (!fs2.existsSync(pkgJsonPath)) continue;
|
|
@@ -253,13 +555,13 @@ function resolveWorkspacePackages(projectRoot, workspace) {
|
|
|
253
555
|
...Object.keys(pkg.dependencies ?? {}),
|
|
254
556
|
...Object.keys(pkg.devDependencies ?? {})
|
|
255
557
|
];
|
|
256
|
-
|
|
558
|
+
resolved.push({ name, path: absPath, relativePath, internalDeps: allDeps });
|
|
257
559
|
}
|
|
258
|
-
const packageNames = new Set(
|
|
259
|
-
for (const pkg of
|
|
560
|
+
const packageNames = new Set(resolved.map((p) => p.name));
|
|
561
|
+
for (const pkg of resolved) {
|
|
260
562
|
pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
|
|
261
563
|
}
|
|
262
|
-
return
|
|
564
|
+
return resolved;
|
|
263
565
|
}
|
|
264
566
|
|
|
265
567
|
// src/commands/boundaries.ts
|
|
@@ -310,7 +612,7 @@ Enforcement: ${config.rules.enforceBoundaries ? import_chalk.default.green("on")
|
|
|
310
612
|
async function inferAndDisplay(projectRoot, config, configPath) {
|
|
311
613
|
console.log(import_chalk.default.dim("Analyzing imports..."));
|
|
312
614
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
313
|
-
const packages = config.
|
|
615
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
314
616
|
const graph = await buildImportGraph(projectRoot, {
|
|
315
617
|
packages,
|
|
316
618
|
ignore: config.ignore
|
|
@@ -334,11 +636,11 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
|
|
|
334
636
|
console.log(`
|
|
335
637
|
${totalRules} denied`);
|
|
336
638
|
console.log("");
|
|
337
|
-
const shouldSave = await
|
|
639
|
+
const shouldSave = await confirm3("Save to viberails.config.json?");
|
|
338
640
|
if (shouldSave) {
|
|
339
641
|
config.boundaries = inferred;
|
|
340
642
|
config.rules.enforceBoundaries = true;
|
|
341
|
-
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
643
|
+
fs3.writeFileSync(configPath, `${JSON.stringify((0, import_config.compactConfig)(config), null, 2)}
|
|
342
644
|
`);
|
|
343
645
|
console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
|
|
344
646
|
}
|
|
@@ -346,7 +648,7 @@ ${import_chalk.default.bold("Inferred boundary rules:")}
|
|
|
346
648
|
async function showGraph(projectRoot, config) {
|
|
347
649
|
console.log(import_chalk.default.dim("Building import graph..."));
|
|
348
650
|
const { buildImportGraph } = await import("@viberails/graph");
|
|
349
|
-
const packages = config.
|
|
651
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
350
652
|
const graph = await buildImportGraph(projectRoot, {
|
|
351
653
|
packages,
|
|
352
654
|
ignore: config.ignore
|
|
@@ -374,42 +676,191 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
|
|
|
374
676
|
}
|
|
375
677
|
|
|
376
678
|
// src/commands/check.ts
|
|
377
|
-
var
|
|
378
|
-
var
|
|
379
|
-
var
|
|
679
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
680
|
+
var path7 = __toESM(require("path"), 1);
|
|
681
|
+
var import_config4 = require("@viberails/config");
|
|
380
682
|
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
381
683
|
|
|
382
684
|
// src/commands/check-config.ts
|
|
685
|
+
var import_config2 = require("@viberails/config");
|
|
383
686
|
function resolveConfigForFile(relPath, config) {
|
|
384
|
-
if (!config.packages || config.packages.length === 0) {
|
|
385
|
-
return { rules: config.rules, conventions: config.conventions };
|
|
386
|
-
}
|
|
387
687
|
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
388
688
|
for (const pkg of sortedPackages) {
|
|
689
|
+
if (pkg.path === ".") continue;
|
|
389
690
|
if (relPath.startsWith(`${pkg.path}/`) || relPath === pkg.path) {
|
|
390
691
|
return {
|
|
391
692
|
rules: { ...config.rules, ...pkg.rules },
|
|
392
|
-
conventions:
|
|
693
|
+
conventions: pkg.conventions ?? {},
|
|
694
|
+
coverage: {
|
|
695
|
+
...config.defaults?.coverage ?? {},
|
|
696
|
+
...pkg.coverage ?? {}
|
|
697
|
+
}
|
|
393
698
|
};
|
|
394
699
|
}
|
|
395
700
|
}
|
|
396
|
-
|
|
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 ?? []];
|
|
397
713
|
}
|
|
398
714
|
function resolveIgnoreForFile(relPath, config) {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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];
|
|
405
721
|
}
|
|
406
|
-
return
|
|
722
|
+
return withRoot;
|
|
407
723
|
}
|
|
408
724
|
|
|
409
|
-
// src/commands/check-
|
|
725
|
+
// src/commands/check-coverage.ts
|
|
410
726
|
var import_node_child_process = require("child_process");
|
|
411
727
|
var fs4 = __toESM(require("fs"), 1);
|
|
412
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");
|
|
413
864
|
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
414
865
|
var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
415
866
|
"node_modules",
|
|
@@ -452,7 +903,7 @@ function isIgnored(relPath, ignorePatterns) {
|
|
|
452
903
|
}
|
|
453
904
|
function countFileLines(filePath) {
|
|
454
905
|
try {
|
|
455
|
-
const content =
|
|
906
|
+
const content = fs5.readFileSync(filePath, "utf-8");
|
|
456
907
|
if (content.length === 0) return 0;
|
|
457
908
|
let count = 1;
|
|
458
909
|
for (let i = 0; i < content.length; i++) {
|
|
@@ -464,14 +915,14 @@ function countFileLines(filePath) {
|
|
|
464
915
|
}
|
|
465
916
|
}
|
|
466
917
|
function checkNaming(relPath, conventions) {
|
|
467
|
-
const filename =
|
|
468
|
-
const ext =
|
|
918
|
+
const filename = path5.basename(relPath);
|
|
919
|
+
const ext = path5.extname(filename);
|
|
469
920
|
if (!SOURCE_EXTS.has(ext)) return void 0;
|
|
470
921
|
if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".") || filename.startsWith("_") || filename.startsWith("+") || filename.startsWith("$") || filename.startsWith("[")) {
|
|
471
922
|
return void 0;
|
|
472
923
|
}
|
|
473
924
|
const bare = filename.slice(0, filename.indexOf("."));
|
|
474
|
-
const convention =
|
|
925
|
+
const convention = conventions.fileNaming;
|
|
475
926
|
if (!convention) return void 0;
|
|
476
927
|
const pattern = NAMING_PATTERNS[convention];
|
|
477
928
|
if (!pattern || pattern.test(bare)) return void 0;
|
|
@@ -479,35 +930,57 @@ function checkNaming(relPath, conventions) {
|
|
|
479
930
|
}
|
|
480
931
|
function getStagedFiles(projectRoot) {
|
|
481
932
|
try {
|
|
482
|
-
const output = (0,
|
|
933
|
+
const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACM", {
|
|
483
934
|
cwd: projectRoot,
|
|
484
|
-
encoding: "utf-8"
|
|
935
|
+
encoding: "utf-8",
|
|
936
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
485
937
|
});
|
|
486
938
|
return output.trim().split("\n").filter(Boolean);
|
|
487
939
|
} catch {
|
|
488
940
|
return [];
|
|
489
941
|
}
|
|
490
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
|
+
}
|
|
491
963
|
function getAllSourceFiles(projectRoot, config) {
|
|
964
|
+
const effectiveIgnore = [...import_config3.BUILTIN_IGNORE, ...config.ignore ?? []];
|
|
492
965
|
const files = [];
|
|
493
966
|
const walk = (dir) => {
|
|
494
967
|
let entries;
|
|
495
968
|
try {
|
|
496
|
-
entries =
|
|
969
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
497
970
|
} catch {
|
|
498
971
|
return;
|
|
499
972
|
}
|
|
500
973
|
for (const entry of entries) {
|
|
501
|
-
const rel =
|
|
974
|
+
const rel = path5.relative(projectRoot, path5.join(dir, entry.name));
|
|
502
975
|
if (entry.isDirectory()) {
|
|
503
976
|
if (ALWAYS_SKIP_DIRS.has(entry.name)) {
|
|
504
977
|
continue;
|
|
505
978
|
}
|
|
506
|
-
if (isIgnored(rel,
|
|
507
|
-
walk(
|
|
979
|
+
if (isIgnored(rel, effectiveIgnore)) continue;
|
|
980
|
+
walk(path5.join(dir, entry.name));
|
|
508
981
|
} else if (entry.isFile()) {
|
|
509
|
-
const ext =
|
|
510
|
-
if (SOURCE_EXTS.has(ext) && !isIgnored(rel,
|
|
982
|
+
const ext = path5.extname(entry.name);
|
|
983
|
+
if (SOURCE_EXTS.has(ext) && !isIgnored(rel, effectiveIgnore)) {
|
|
511
984
|
files.push(rel);
|
|
512
985
|
}
|
|
513
986
|
}
|
|
@@ -521,16 +994,16 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
521
994
|
const walk = (d) => {
|
|
522
995
|
let entries;
|
|
523
996
|
try {
|
|
524
|
-
entries =
|
|
997
|
+
entries = fs5.readdirSync(d, { withFileTypes: true });
|
|
525
998
|
} catch {
|
|
526
999
|
return;
|
|
527
1000
|
}
|
|
528
1001
|
for (const entry of entries) {
|
|
529
1002
|
if (entry.isDirectory()) {
|
|
530
1003
|
if (entry.name === "node_modules") continue;
|
|
531
|
-
walk(
|
|
1004
|
+
walk(path5.join(d, entry.name));
|
|
532
1005
|
} else if (entry.isFile()) {
|
|
533
|
-
files.push(
|
|
1006
|
+
files.push(path5.relative(projectRoot, path5.join(d, entry.name)));
|
|
534
1007
|
}
|
|
535
1008
|
}
|
|
536
1009
|
};
|
|
@@ -539,8 +1012,8 @@ function collectSourceFiles(dir, projectRoot) {
|
|
|
539
1012
|
}
|
|
540
1013
|
|
|
541
1014
|
// src/commands/check-tests.ts
|
|
542
|
-
var
|
|
543
|
-
var
|
|
1015
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
1016
|
+
var path6 = __toESM(require("path"), 1);
|
|
544
1017
|
var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
545
1018
|
".ts",
|
|
546
1019
|
".tsx",
|
|
@@ -554,44 +1027,58 @@ var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
|
554
1027
|
]);
|
|
555
1028
|
function checkMissingTests(projectRoot, config, severity) {
|
|
556
1029
|
const violations = [];
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
}
|
|
586
1064
|
}
|
|
587
1065
|
}
|
|
588
1066
|
return violations;
|
|
589
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
|
+
}
|
|
590
1077
|
|
|
591
1078
|
// src/commands/check.ts
|
|
592
1079
|
var CONFIG_FILE2 = "viberails.config.json";
|
|
593
1080
|
function isTestFile(relPath) {
|
|
594
|
-
const filename =
|
|
1081
|
+
const filename = path7.basename(relPath);
|
|
595
1082
|
return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
|
|
596
1083
|
}
|
|
597
1084
|
function printGroupedViolations(violations, limit) {
|
|
@@ -601,7 +1088,13 @@ function printGroupedViolations(violations, limit) {
|
|
|
601
1088
|
existing.push(v);
|
|
602
1089
|
groups.set(v.rule, existing);
|
|
603
1090
|
}
|
|
604
|
-
const ruleOrder = [
|
|
1091
|
+
const ruleOrder = [
|
|
1092
|
+
"file-size",
|
|
1093
|
+
"file-naming",
|
|
1094
|
+
"missing-test",
|
|
1095
|
+
"test-coverage",
|
|
1096
|
+
"boundary-violation"
|
|
1097
|
+
];
|
|
605
1098
|
const sortedKeys = [...groups.keys()].sort(
|
|
606
1099
|
(a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
|
|
607
1100
|
);
|
|
@@ -641,17 +1134,22 @@ async function checkCommand(options, cwd) {
|
|
|
641
1134
|
console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
642
1135
|
return 1;
|
|
643
1136
|
}
|
|
644
|
-
const configPath =
|
|
645
|
-
if (!
|
|
1137
|
+
const configPath = path7.join(projectRoot, CONFIG_FILE2);
|
|
1138
|
+
if (!fs7.existsSync(configPath)) {
|
|
646
1139
|
console.error(
|
|
647
1140
|
`${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
648
1141
|
);
|
|
649
1142
|
return 1;
|
|
650
1143
|
}
|
|
651
|
-
const config = await (0,
|
|
1144
|
+
const config = await (0, import_config4.loadConfig)(configPath);
|
|
652
1145
|
let filesToCheck;
|
|
1146
|
+
let diffAddedFiles = null;
|
|
653
1147
|
if (options.staged) {
|
|
654
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);
|
|
655
1153
|
} else if (options.files && options.files.length > 0) {
|
|
656
1154
|
filesToCheck = options.files;
|
|
657
1155
|
} else {
|
|
@@ -659,22 +1157,20 @@ async function checkCommand(options, cwd) {
|
|
|
659
1157
|
}
|
|
660
1158
|
if (filesToCheck.length === 0) {
|
|
661
1159
|
if (options.format === "json") {
|
|
662
|
-
console.log(
|
|
663
|
-
JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
|
|
664
|
-
);
|
|
1160
|
+
console.log(JSON.stringify({ violations: [], checkedFiles: 0 }));
|
|
665
1161
|
} else {
|
|
666
1162
|
console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
|
|
667
1163
|
}
|
|
668
1164
|
return 0;
|
|
669
1165
|
}
|
|
670
1166
|
const violations = [];
|
|
671
|
-
const severity =
|
|
1167
|
+
const severity = options.enforce ? "error" : "warn";
|
|
672
1168
|
for (const file of filesToCheck) {
|
|
673
|
-
const absPath =
|
|
674
|
-
const relPath =
|
|
1169
|
+
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1170
|
+
const relPath = path7.relative(projectRoot, absPath);
|
|
675
1171
|
const effectiveIgnore = resolveIgnoreForFile(relPath, config);
|
|
676
1172
|
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
677
|
-
if (!
|
|
1173
|
+
if (!fs7.existsSync(absPath)) continue;
|
|
678
1174
|
const resolved = resolveConfigForFile(relPath, config);
|
|
679
1175
|
const testFile = isTestFile(relPath);
|
|
680
1176
|
const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
|
|
@@ -701,23 +1197,34 @@ async function checkCommand(options, cwd) {
|
|
|
701
1197
|
}
|
|
702
1198
|
}
|
|
703
1199
|
}
|
|
704
|
-
if (
|
|
1200
|
+
if (!options.staged && !options.files) {
|
|
705
1201
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
706
|
-
|
|
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);
|
|
707
1214
|
}
|
|
708
1215
|
if (config.rules.enforceBoundaries && config.boundaries && Object.keys(config.boundaries.deny).length > 0 && !options.noBoundaries) {
|
|
709
1216
|
const startTime = Date.now();
|
|
710
1217
|
const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
|
|
711
|
-
const packages = config.
|
|
1218
|
+
const packages = config.packages.length > 1 ? resolveWorkspacePackages(projectRoot, config.packages) : void 0;
|
|
712
1219
|
const graph = await buildImportGraph(projectRoot, {
|
|
713
1220
|
packages,
|
|
714
1221
|
ignore: config.ignore
|
|
715
1222
|
});
|
|
716
1223
|
const boundaryViolations = checkBoundaries(graph, config.boundaries);
|
|
717
|
-
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;
|
|
718
1225
|
for (const bv of boundaryViolations) {
|
|
719
1226
|
if (filterSet && !filterSet.has(bv.file)) continue;
|
|
720
|
-
const relFile =
|
|
1227
|
+
const relFile = path7.relative(projectRoot, bv.file);
|
|
721
1228
|
violations.push({
|
|
722
1229
|
file: relFile,
|
|
723
1230
|
rule: "boundary-violation",
|
|
@@ -734,11 +1241,10 @@ async function checkCommand(options, cwd) {
|
|
|
734
1241
|
console.log(
|
|
735
1242
|
JSON.stringify({
|
|
736
1243
|
violations,
|
|
737
|
-
checkedFiles: filesToCheck.length
|
|
738
|
-
enforcement: config.enforcement
|
|
1244
|
+
checkedFiles: filesToCheck.length
|
|
739
1245
|
})
|
|
740
1246
|
);
|
|
741
|
-
return
|
|
1247
|
+
return options.enforce && violations.length > 0 ? 1 : 0;
|
|
742
1248
|
}
|
|
743
1249
|
if (violations.length === 0) {
|
|
744
1250
|
console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
|
|
@@ -748,7 +1254,7 @@ async function checkCommand(options, cwd) {
|
|
|
748
1254
|
printGroupedViolations(violations, options.limit);
|
|
749
1255
|
}
|
|
750
1256
|
printSummary(violations);
|
|
751
|
-
if (
|
|
1257
|
+
if (options.enforce) {
|
|
752
1258
|
console.log(import_chalk2.default.red("Fix violations before committing."));
|
|
753
1259
|
return 1;
|
|
754
1260
|
}
|
|
@@ -756,7 +1262,7 @@ async function checkCommand(options, cwd) {
|
|
|
756
1262
|
}
|
|
757
1263
|
|
|
758
1264
|
// src/commands/check-hook.ts
|
|
759
|
-
var
|
|
1265
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
760
1266
|
function parseHookFilePath(input) {
|
|
761
1267
|
try {
|
|
762
1268
|
if (!input.trim()) return void 0;
|
|
@@ -768,7 +1274,7 @@ function parseHookFilePath(input) {
|
|
|
768
1274
|
}
|
|
769
1275
|
function readStdin() {
|
|
770
1276
|
try {
|
|
771
|
-
return
|
|
1277
|
+
return fs8.readFileSync(0, "utf-8");
|
|
772
1278
|
} catch {
|
|
773
1279
|
return "";
|
|
774
1280
|
}
|
|
@@ -802,13 +1308,13 @@ async function hookCheckCommand(cwd) {
|
|
|
802
1308
|
}
|
|
803
1309
|
|
|
804
1310
|
// src/commands/fix.ts
|
|
805
|
-
var
|
|
806
|
-
var
|
|
807
|
-
var
|
|
1311
|
+
var fs11 = __toESM(require("fs"), 1);
|
|
1312
|
+
var path11 = __toESM(require("path"), 1);
|
|
1313
|
+
var import_config5 = require("@viberails/config");
|
|
808
1314
|
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
809
1315
|
|
|
810
1316
|
// src/commands/fix-helpers.ts
|
|
811
|
-
var
|
|
1317
|
+
var import_node_child_process3 = require("child_process");
|
|
812
1318
|
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
813
1319
|
function printPlan(renames, stubs) {
|
|
814
1320
|
if (renames.length > 0) {
|
|
@@ -826,9 +1332,10 @@ function printPlan(renames, stubs) {
|
|
|
826
1332
|
}
|
|
827
1333
|
function checkGitDirty(projectRoot) {
|
|
828
1334
|
try {
|
|
829
|
-
const output = (0,
|
|
1335
|
+
const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
|
|
830
1336
|
cwd: projectRoot,
|
|
831
|
-
encoding: "utf-8"
|
|
1337
|
+
encoding: "utf-8",
|
|
1338
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
832
1339
|
});
|
|
833
1340
|
return output.trim().length > 0;
|
|
834
1341
|
} catch {
|
|
@@ -837,14 +1344,11 @@ function checkGitDirty(projectRoot) {
|
|
|
837
1344
|
}
|
|
838
1345
|
function getConventionValue(convention) {
|
|
839
1346
|
if (typeof convention === "string") return convention;
|
|
840
|
-
if (convention && typeof convention === "object" && "value" in convention) {
|
|
841
|
-
return convention.value;
|
|
842
|
-
}
|
|
843
1347
|
return void 0;
|
|
844
1348
|
}
|
|
845
1349
|
|
|
846
1350
|
// src/commands/fix-imports.ts
|
|
847
|
-
var
|
|
1351
|
+
var path8 = __toESM(require("path"), 1);
|
|
848
1352
|
function stripExtension(filePath) {
|
|
849
1353
|
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
850
1354
|
}
|
|
@@ -862,7 +1366,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
862
1366
|
const renameMap = /* @__PURE__ */ new Map();
|
|
863
1367
|
for (const r of renames) {
|
|
864
1368
|
const oldStripped = stripExtension(r.oldAbsPath);
|
|
865
|
-
const newFilename =
|
|
1369
|
+
const newFilename = path8.basename(r.newPath);
|
|
866
1370
|
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
867
1371
|
renameMap.set(oldStripped, { newBare: newName });
|
|
868
1372
|
}
|
|
@@ -870,14 +1374,14 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
870
1374
|
tsConfigFilePath: void 0,
|
|
871
1375
|
skipAddingFilesFromTsConfig: true
|
|
872
1376
|
});
|
|
873
|
-
project.addSourceFilesAtPaths(
|
|
1377
|
+
project.addSourceFilesAtPaths(path8.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
874
1378
|
const updates = [];
|
|
875
1379
|
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
876
1380
|
for (const sourceFile of project.getSourceFiles()) {
|
|
877
1381
|
const filePath = sourceFile.getFilePath();
|
|
878
|
-
const segments = filePath.split(
|
|
1382
|
+
const segments = filePath.split(path8.sep);
|
|
879
1383
|
if (segments.includes("node_modules") || segments.includes("dist")) continue;
|
|
880
|
-
const fileDir =
|
|
1384
|
+
const fileDir = path8.dirname(filePath);
|
|
881
1385
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
882
1386
|
const specifier = decl.getModuleSpecifierValue();
|
|
883
1387
|
if (!specifier.startsWith(".")) continue;
|
|
@@ -934,7 +1438,7 @@ async function updateImportsAfterRenames(renames, projectRoot) {
|
|
|
934
1438
|
}
|
|
935
1439
|
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
936
1440
|
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
937
|
-
const resolved =
|
|
1441
|
+
const resolved = path8.resolve(fromDir, cleanSpec);
|
|
938
1442
|
for (const ext of extensions) {
|
|
939
1443
|
const candidate = resolved + ext;
|
|
940
1444
|
const stripped = stripExtension(candidate);
|
|
@@ -945,8 +1449,8 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
|
945
1449
|
}
|
|
946
1450
|
|
|
947
1451
|
// src/commands/fix-naming.ts
|
|
948
|
-
var
|
|
949
|
-
var
|
|
1452
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
1453
|
+
var path9 = __toESM(require("path"), 1);
|
|
950
1454
|
|
|
951
1455
|
// src/commands/convert-name.ts
|
|
952
1456
|
function splitIntoWords(name) {
|
|
@@ -995,8 +1499,8 @@ function capitalize(word) {
|
|
|
995
1499
|
|
|
996
1500
|
// src/commands/fix-naming.ts
|
|
997
1501
|
function computeRename(relPath, targetConvention, projectRoot) {
|
|
998
|
-
const filename =
|
|
999
|
-
const dir =
|
|
1502
|
+
const filename = path9.basename(relPath);
|
|
1503
|
+
const dir = path9.dirname(relPath);
|
|
1000
1504
|
const dotIndex = filename.indexOf(".");
|
|
1001
1505
|
if (dotIndex === -1) return null;
|
|
1002
1506
|
const bare = filename.slice(0, dotIndex);
|
|
@@ -1004,15 +1508,15 @@ function computeRename(relPath, targetConvention, projectRoot) {
|
|
|
1004
1508
|
const newBare = convertName(bare, targetConvention);
|
|
1005
1509
|
if (newBare === bare) return null;
|
|
1006
1510
|
const newFilename = newBare + suffix;
|
|
1007
|
-
const newRelPath =
|
|
1008
|
-
const oldAbsPath =
|
|
1009
|
-
const newAbsPath =
|
|
1010
|
-
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;
|
|
1011
1515
|
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
1012
1516
|
}
|
|
1013
1517
|
function executeRename(rename) {
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1518
|
+
if (fs9.existsSync(rename.newAbsPath)) return false;
|
|
1519
|
+
fs9.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
1016
1520
|
return true;
|
|
1017
1521
|
}
|
|
1018
1522
|
function deduplicateRenames(renames) {
|
|
@@ -1027,33 +1531,38 @@ function deduplicateRenames(renames) {
|
|
|
1027
1531
|
}
|
|
1028
1532
|
|
|
1029
1533
|
// src/commands/fix-tests.ts
|
|
1030
|
-
var
|
|
1031
|
-
var
|
|
1534
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
1535
|
+
var path10 = __toESM(require("path"), 1);
|
|
1032
1536
|
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
1033
|
-
const
|
|
1537
|
+
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
1538
|
+
const testPattern = pkg?.structure?.testPattern;
|
|
1034
1539
|
if (!testPattern) return null;
|
|
1035
|
-
const
|
|
1036
|
-
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);
|
|
1037
1544
|
const testSuffix = testPattern.replace("*", "");
|
|
1038
1545
|
const testFilename = `${stem}${testSuffix}`;
|
|
1039
|
-
const dir =
|
|
1040
|
-
const testAbsPath =
|
|
1041
|
-
if (
|
|
1546
|
+
const dir = path10.dirname(path10.join(projectRoot, sourceRelPath));
|
|
1547
|
+
const testAbsPath = path10.join(dir, testFilename);
|
|
1548
|
+
if (fs10.existsSync(testAbsPath)) return null;
|
|
1042
1549
|
return {
|
|
1043
|
-
path:
|
|
1550
|
+
path: path10.relative(projectRoot, testAbsPath),
|
|
1044
1551
|
absPath: testAbsPath,
|
|
1045
1552
|
moduleName: stem
|
|
1046
1553
|
};
|
|
1047
1554
|
}
|
|
1048
1555
|
function writeTestStub(stub, config) {
|
|
1049
|
-
const
|
|
1556
|
+
const pkg = resolvePackageForFile(stub.path, config);
|
|
1557
|
+
const testRunner = pkg?.stack?.testRunner ?? "";
|
|
1558
|
+
const runner = testRunner.startsWith("jest") ? "jest" : "vitest";
|
|
1050
1559
|
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
1051
1560
|
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
1052
1561
|
it.todo('add tests');
|
|
1053
1562
|
});
|
|
1054
1563
|
`;
|
|
1055
|
-
|
|
1056
|
-
|
|
1564
|
+
fs10.mkdirSync(path10.dirname(stub.absPath), { recursive: true });
|
|
1565
|
+
fs10.writeFileSync(stub.absPath, content);
|
|
1057
1566
|
}
|
|
1058
1567
|
|
|
1059
1568
|
// src/commands/fix.ts
|
|
@@ -1065,14 +1574,14 @@ async function fixCommand(options, cwd) {
|
|
|
1065
1574
|
console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
1066
1575
|
return 1;
|
|
1067
1576
|
}
|
|
1068
|
-
const configPath =
|
|
1069
|
-
if (!
|
|
1577
|
+
const configPath = path11.join(projectRoot, CONFIG_FILE3);
|
|
1578
|
+
if (!fs11.existsSync(configPath)) {
|
|
1070
1579
|
console.error(
|
|
1071
1580
|
`${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
1072
1581
|
);
|
|
1073
1582
|
return 1;
|
|
1074
1583
|
}
|
|
1075
|
-
const config = await (0,
|
|
1584
|
+
const config = await (0, import_config5.loadConfig)(configPath);
|
|
1076
1585
|
if (!options.dryRun) {
|
|
1077
1586
|
const isDirty = checkGitDirty(projectRoot);
|
|
1078
1587
|
if (isDirty) {
|
|
@@ -1099,7 +1608,7 @@ async function fixCommand(options, cwd) {
|
|
|
1099
1608
|
}
|
|
1100
1609
|
const dedupedRenames = deduplicateRenames(renames);
|
|
1101
1610
|
const testStubs = [];
|
|
1102
|
-
if (shouldFixTests
|
|
1611
|
+
if (shouldFixTests) {
|
|
1103
1612
|
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
1104
1613
|
for (const v of testViolations) {
|
|
1105
1614
|
const stub = generateTestStub(v.file, config, projectRoot);
|
|
@@ -1130,13 +1639,13 @@ async function fixCommand(options, cwd) {
|
|
|
1130
1639
|
}
|
|
1131
1640
|
let importUpdateCount = 0;
|
|
1132
1641
|
if (renameCount > 0) {
|
|
1133
|
-
const appliedRenames = dedupedRenames.filter((r) =>
|
|
1642
|
+
const appliedRenames = dedupedRenames.filter((r) => fs11.existsSync(r.newAbsPath));
|
|
1134
1643
|
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
1135
1644
|
importUpdateCount = updates.length;
|
|
1136
1645
|
}
|
|
1137
1646
|
let stubCount = 0;
|
|
1138
1647
|
for (const stub of testStubs) {
|
|
1139
|
-
if (!
|
|
1648
|
+
if (!fs11.existsSync(stub.absPath)) {
|
|
1140
1649
|
writeTestStub(stub, config);
|
|
1141
1650
|
stubCount++;
|
|
1142
1651
|
}
|
|
@@ -1157,15 +1666,16 @@ async function fixCommand(options, cwd) {
|
|
|
1157
1666
|
}
|
|
1158
1667
|
|
|
1159
1668
|
// src/commands/init.ts
|
|
1160
|
-
var
|
|
1161
|
-
var
|
|
1162
|
-
var
|
|
1163
|
-
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");
|
|
1164
1673
|
var import_scanner = require("@viberails/scanner");
|
|
1165
|
-
var
|
|
1674
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
1166
1675
|
|
|
1167
|
-
// src/display
|
|
1676
|
+
// src/display.ts
|
|
1168
1677
|
var import_types4 = require("@viberails/types");
|
|
1678
|
+
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
1169
1679
|
|
|
1170
1680
|
// src/display-helpers.ts
|
|
1171
1681
|
var import_types = require("@viberails/types");
|
|
@@ -1216,20 +1726,130 @@ function formatRoleGroup(group) {
|
|
|
1216
1726
|
return `${group.label} \u2014 ${dirs} (${files})`;
|
|
1217
1727
|
}
|
|
1218
1728
|
|
|
1219
|
-
// src/display.ts
|
|
1729
|
+
// src/display-monorepo.ts
|
|
1220
1730
|
var import_types3 = require("@viberails/types");
|
|
1221
|
-
var
|
|
1731
|
+
var import_chalk5 = __toESM(require("chalk"), 1);
|
|
1222
1732
|
|
|
1223
|
-
// src/display-
|
|
1733
|
+
// src/display-text.ts
|
|
1224
1734
|
var import_types2 = require("@viberails/types");
|
|
1225
|
-
|
|
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
|
|
1226
1846
|
function formatPackageSummary(pkg) {
|
|
1227
1847
|
const parts = [];
|
|
1228
1848
|
if (pkg.stack.framework) {
|
|
1229
|
-
parts.push(formatItem(pkg.stack.framework,
|
|
1849
|
+
parts.push(formatItem(pkg.stack.framework, import_types3.FRAMEWORK_NAMES));
|
|
1230
1850
|
}
|
|
1231
1851
|
if (pkg.stack.styling) {
|
|
1232
|
-
parts.push(formatItem(pkg.stack.styling,
|
|
1852
|
+
parts.push(formatItem(pkg.stack.styling, import_types3.STYLING_NAMES));
|
|
1233
1853
|
}
|
|
1234
1854
|
const files = `${pkg.statistics.totalFiles} files`;
|
|
1235
1855
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
@@ -1243,11 +1863,15 @@ ${import_chalk5.default.bold(`Detected: (monorepo, ${packages.length} packages)`
|
|
|
1243
1863
|
if (stack.packageManager) {
|
|
1244
1864
|
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
1245
1865
|
}
|
|
1246
|
-
if (stack.linter) {
|
|
1247
|
-
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
+
}
|
|
1251
1875
|
}
|
|
1252
1876
|
if (stack.testRunner) {
|
|
1253
1877
|
console.log(` ${import_chalk5.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
@@ -1278,23 +1902,27 @@ ${import_chalk5.default.bold("Structure:")}`);
|
|
|
1278
1902
|
function formatPackageSummaryPlain(pkg) {
|
|
1279
1903
|
const parts = [];
|
|
1280
1904
|
if (pkg.stack.framework) {
|
|
1281
|
-
parts.push(formatItem(pkg.stack.framework,
|
|
1905
|
+
parts.push(formatItem(pkg.stack.framework, import_types3.FRAMEWORK_NAMES));
|
|
1282
1906
|
}
|
|
1283
1907
|
if (pkg.stack.styling) {
|
|
1284
|
-
parts.push(formatItem(pkg.stack.styling,
|
|
1908
|
+
parts.push(formatItem(pkg.stack.styling, import_types3.STYLING_NAMES));
|
|
1285
1909
|
}
|
|
1286
1910
|
const files = `${pkg.statistics.totalFiles} files`;
|
|
1287
1911
|
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
1288
1912
|
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
1289
1913
|
}
|
|
1290
|
-
function formatMonorepoResultsText(scanResult
|
|
1914
|
+
function formatMonorepoResultsText(scanResult) {
|
|
1291
1915
|
const lines = [];
|
|
1292
1916
|
const { stack, packages } = scanResult;
|
|
1293
1917
|
lines.push(`Detected: (monorepo, ${packages.length} packages)`);
|
|
1294
1918
|
const sharedParts = [formatItem(stack.language)];
|
|
1295
1919
|
if (stack.packageManager) sharedParts.push(formatItem(stack.packageManager));
|
|
1296
|
-
if (stack.linter
|
|
1297
|
-
|
|
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
|
+
}
|
|
1298
1926
|
if (stack.testRunner) sharedParts.push(formatItem(stack.testRunner));
|
|
1299
1927
|
lines.push(` \u2713 ${sharedParts.join(" \xB7 ")}`);
|
|
1300
1928
|
lines.push("");
|
|
@@ -1324,7 +1952,6 @@ function formatMonorepoResultsText(scanResult, config) {
|
|
|
1324
1952
|
if (ext) {
|
|
1325
1953
|
lines.push(ext);
|
|
1326
1954
|
}
|
|
1327
|
-
lines.push(...formatRulesText(config));
|
|
1328
1955
|
return lines.join("\n");
|
|
1329
1956
|
}
|
|
1330
1957
|
|
|
@@ -1347,7 +1974,7 @@ function displayConventions(scanResult) {
|
|
|
1347
1974
|
${import_chalk6.default.bold("Conventions:")}`);
|
|
1348
1975
|
for (const [key, convention] of conventionEntries) {
|
|
1349
1976
|
if (convention.confidence === "low") continue;
|
|
1350
|
-
const label =
|
|
1977
|
+
const label = import_types4.CONVENTION_LABELS[key] ?? key;
|
|
1351
1978
|
if (scanResult.packages.length > 1) {
|
|
1352
1979
|
const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
1353
1980
|
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
@@ -1388,23 +2015,27 @@ function displayScanResults(scanResult) {
|
|
|
1388
2015
|
console.log(`
|
|
1389
2016
|
${import_chalk6.default.bold("Detected:")}`);
|
|
1390
2017
|
if (stack.framework) {
|
|
1391
|
-
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)}`);
|
|
1392
2019
|
}
|
|
1393
2020
|
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
|
|
1394
2021
|
if (stack.styling) {
|
|
1395
|
-
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)}`);
|
|
1396
2023
|
}
|
|
1397
2024
|
if (stack.backend) {
|
|
1398
|
-
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)}`);
|
|
1399
2026
|
}
|
|
1400
2027
|
if (stack.orm) {
|
|
1401
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm,
|
|
1402
|
-
}
|
|
1403
|
-
if (stack.linter) {
|
|
1404
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
2028
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
|
|
1405
2029
|
}
|
|
1406
|
-
if (stack.formatter) {
|
|
1407
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.
|
|
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
|
+
}
|
|
1408
2039
|
}
|
|
1409
2040
|
if (stack.testRunner) {
|
|
1410
2041
|
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
@@ -1414,7 +2045,7 @@ ${import_chalk6.default.bold("Detected:")}`);
|
|
|
1414
2045
|
}
|
|
1415
2046
|
if (stack.libraries.length > 0) {
|
|
1416
2047
|
for (const lib of stack.libraries) {
|
|
1417
|
-
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib,
|
|
2048
|
+
console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(lib, import_types4.LIBRARY_NAMES)}`);
|
|
1418
2049
|
}
|
|
1419
2050
|
}
|
|
1420
2051
|
const groups = groupByRole(scanResult.structure.directories);
|
|
@@ -1429,25 +2060,23 @@ ${import_chalk6.default.bold("Structure:")}`);
|
|
|
1429
2060
|
displaySummarySection(scanResult);
|
|
1430
2061
|
console.log("");
|
|
1431
2062
|
}
|
|
1432
|
-
function getConventionStr(cv) {
|
|
1433
|
-
return typeof cv === "string" ? cv : cv.value;
|
|
1434
|
-
}
|
|
1435
2063
|
function displayRulesPreview(config) {
|
|
1436
|
-
|
|
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
|
+
);
|
|
1437
2068
|
console.log(` ${import_chalk6.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
|
|
1438
|
-
if (config.rules.
|
|
2069
|
+
if (config.rules.testCoverage > 0 && root?.structure?.testPattern) {
|
|
1439
2070
|
console.log(
|
|
1440
|
-
` ${import_chalk6.default.dim("\u2022")}
|
|
2071
|
+
` ${import_chalk6.default.dim("\u2022")} Test coverage target: ${config.rules.testCoverage}% (${root.structure.testPattern})`
|
|
1441
2072
|
);
|
|
1442
|
-
} else if (config.rules.
|
|
1443
|
-
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}%`);
|
|
1444
2075
|
} else {
|
|
1445
|
-
console.log(` ${import_chalk6.default.dim("\u2022")}
|
|
2076
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Test coverage target: disabled`);
|
|
1446
2077
|
}
|
|
1447
|
-
if (config.rules.enforceNaming &&
|
|
1448
|
-
console.log(
|
|
1449
|
-
` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
|
|
1450
|
-
);
|
|
2078
|
+
if (config.rules.enforceNaming && root?.conventions?.fileNaming) {
|
|
2079
|
+
console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: ${root.conventions.fileNaming}`);
|
|
1451
2080
|
} else {
|
|
1452
2081
|
console.log(` ${import_chalk6.default.dim("\u2022")} Enforce file naming: no`);
|
|
1453
2082
|
}
|
|
@@ -1455,146 +2084,163 @@ function displayRulesPreview(config) {
|
|
|
1455
2084
|
` ${import_chalk6.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
|
|
1456
2085
|
);
|
|
1457
2086
|
console.log("");
|
|
1458
|
-
if (config.enforcement === "enforce") {
|
|
1459
|
-
console.log(`${import_chalk6.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
|
|
1460
|
-
} else {
|
|
1461
|
-
console.log(
|
|
1462
|
-
`${import_chalk6.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
|
|
1463
|
-
);
|
|
1464
|
-
}
|
|
1465
|
-
console.log("");
|
|
1466
2087
|
}
|
|
1467
2088
|
|
|
1468
|
-
// src/
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
+
];
|
|
1476
2113
|
}
|
|
1477
|
-
return
|
|
1478
|
-
}
|
|
1479
|
-
function
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
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
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
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}`
|
|
1499
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
|
+
);
|
|
1500
2173
|
}
|
|
2174
|
+
} else if (choice === "disable") {
|
|
2175
|
+
disableCoverage = true;
|
|
2176
|
+
clack6.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
|
|
1501
2177
|
} else {
|
|
1502
|
-
|
|
1503
|
-
|
|
2178
|
+
clack6.log.info(
|
|
2179
|
+
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2180
|
+
Install later: ${m.installCommand}`
|
|
2181
|
+
);
|
|
1504
2182
|
}
|
|
1505
2183
|
}
|
|
1506
|
-
return
|
|
2184
|
+
return { disableCoverage };
|
|
1507
2185
|
}
|
|
1508
|
-
function
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
} else if (config.rules.requireTests) {
|
|
1516
|
-
lines.push(" \u2022 Require test files: yes");
|
|
1517
|
-
} else {
|
|
1518
|
-
lines.push(" \u2022 Require test files: no");
|
|
1519
|
-
}
|
|
1520
|
-
if (config.rules.enforceNaming && config.conventions.fileNaming) {
|
|
1521
|
-
lines.push(` \u2022 Enforce file naming: ${getConventionStr2(config.conventions.fileNaming)}`);
|
|
1522
|
-
} else {
|
|
1523
|
-
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;
|
|
1524
2193
|
}
|
|
1525
|
-
lines.push(` \u2022 Enforcement mode: ${config.enforcement}`);
|
|
1526
|
-
return lines;
|
|
1527
2194
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
lines.push(` \u2713 ${formatItem(stack.language)}`);
|
|
1539
|
-
if (stack.styling) {
|
|
1540
|
-
lines.push(` \u2713 ${formatItem(stack.styling, import_types4.STYLING_NAMES)}`);
|
|
1541
|
-
}
|
|
1542
|
-
if (stack.backend) {
|
|
1543
|
-
lines.push(` \u2713 ${formatItem(stack.backend, import_types4.FRAMEWORK_NAMES)}`);
|
|
1544
|
-
}
|
|
1545
|
-
if (stack.orm) {
|
|
1546
|
-
lines.push(` \u2713 ${formatItem(stack.orm, import_types4.ORM_NAMES)}`);
|
|
1547
|
-
}
|
|
1548
|
-
const secondaryParts = [];
|
|
1549
|
-
if (stack.packageManager) secondaryParts.push(formatItem(stack.packageManager));
|
|
1550
|
-
if (stack.linter) secondaryParts.push(formatItem(stack.linter));
|
|
1551
|
-
if (stack.formatter) secondaryParts.push(formatItem(stack.formatter));
|
|
1552
|
-
if (stack.testRunner) secondaryParts.push(formatItem(stack.testRunner));
|
|
1553
|
-
if (secondaryParts.length > 0) {
|
|
1554
|
-
lines.push(` \u2713 ${secondaryParts.join(" \xB7 ")}`);
|
|
1555
|
-
}
|
|
1556
|
-
if (stack.libraries.length > 0) {
|
|
1557
|
-
for (const lib of stack.libraries) {
|
|
1558
|
-
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;
|
|
1559
2205
|
}
|
|
1560
2206
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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");
|
|
1568
2218
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
if (ext) {
|
|
1575
|
-
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}`);
|
|
1576
2224
|
}
|
|
1577
|
-
lines.push(...formatRulesText(config));
|
|
1578
|
-
return lines.join("\n");
|
|
1579
2225
|
}
|
|
1580
2226
|
|
|
1581
2227
|
// src/utils/write-generated-files.ts
|
|
1582
|
-
var
|
|
1583
|
-
var
|
|
2228
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
2229
|
+
var path14 = __toESM(require("path"), 1);
|
|
1584
2230
|
var import_context = require("@viberails/context");
|
|
1585
2231
|
var CONTEXT_DIR = ".viberails";
|
|
1586
2232
|
var CONTEXT_FILE = "context.md";
|
|
1587
2233
|
var SCAN_RESULT_FILE = "scan-result.json";
|
|
1588
2234
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
1589
|
-
const contextDir =
|
|
2235
|
+
const contextDir = path14.join(projectRoot, CONTEXT_DIR);
|
|
1590
2236
|
try {
|
|
1591
|
-
if (!
|
|
1592
|
-
|
|
2237
|
+
if (!fs14.existsSync(contextDir)) {
|
|
2238
|
+
fs14.mkdirSync(contextDir, { recursive: true });
|
|
1593
2239
|
}
|
|
1594
2240
|
const context = (0, import_context.generateContext)(config);
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2241
|
+
fs14.writeFileSync(path14.join(contextDir, CONTEXT_FILE), context);
|
|
2242
|
+
fs14.writeFileSync(
|
|
2243
|
+
path14.join(contextDir, SCAN_RESULT_FILE),
|
|
1598
2244
|
`${JSON.stringify(scanResult, null, 2)}
|
|
1599
2245
|
`
|
|
1600
2246
|
);
|
|
@@ -1605,39 +2251,41 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
|
1605
2251
|
}
|
|
1606
2252
|
|
|
1607
2253
|
// src/commands/init-hooks.ts
|
|
1608
|
-
var
|
|
1609
|
-
var
|
|
1610
|
-
var
|
|
2254
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
2255
|
+
var path15 = __toESM(require("path"), 1);
|
|
2256
|
+
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
1611
2257
|
var import_yaml = require("yaml");
|
|
1612
2258
|
function setupPreCommitHook(projectRoot) {
|
|
1613
|
-
const lefthookPath =
|
|
1614
|
-
if (
|
|
2259
|
+
const lefthookPath = path15.join(projectRoot, "lefthook.yml");
|
|
2260
|
+
if (fs15.existsSync(lefthookPath)) {
|
|
1615
2261
|
addLefthookPreCommit(lefthookPath);
|
|
1616
|
-
console.log(` ${
|
|
1617
|
-
return;
|
|
2262
|
+
console.log(` ${import_chalk8.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2263
|
+
return "lefthook.yml";
|
|
1618
2264
|
}
|
|
1619
|
-
const huskyDir =
|
|
1620
|
-
if (
|
|
2265
|
+
const huskyDir = path15.join(projectRoot, ".husky");
|
|
2266
|
+
if (fs15.existsSync(huskyDir)) {
|
|
1621
2267
|
writeHuskyPreCommit(huskyDir);
|
|
1622
|
-
console.log(` ${
|
|
1623
|
-
return;
|
|
2268
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2269
|
+
return ".husky/pre-commit";
|
|
1624
2270
|
}
|
|
1625
|
-
const gitDir =
|
|
1626
|
-
if (
|
|
1627
|
-
const hooksDir =
|
|
1628
|
-
if (!
|
|
1629
|
-
|
|
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 });
|
|
1630
2276
|
}
|
|
1631
2277
|
writeGitHookPreCommit(hooksDir);
|
|
1632
|
-
console.log(` ${
|
|
2278
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
2279
|
+
return ".git/hooks/pre-commit";
|
|
1633
2280
|
}
|
|
2281
|
+
return void 0;
|
|
1634
2282
|
}
|
|
1635
2283
|
function writeGitHookPreCommit(hooksDir) {
|
|
1636
|
-
const hookPath =
|
|
1637
|
-
if (
|
|
1638
|
-
const existing =
|
|
2284
|
+
const hookPath = path15.join(hooksDir, "pre-commit");
|
|
2285
|
+
if (fs15.existsSync(hookPath)) {
|
|
2286
|
+
const existing = fs15.readFileSync(hookPath, "utf-8");
|
|
1639
2287
|
if (existing.includes("viberails")) return;
|
|
1640
|
-
|
|
2288
|
+
fs15.writeFileSync(
|
|
1641
2289
|
hookPath,
|
|
1642
2290
|
`${existing.trimEnd()}
|
|
1643
2291
|
|
|
@@ -1654,10 +2302,10 @@ npx viberails check --staged
|
|
|
1654
2302
|
"npx viberails check --staged",
|
|
1655
2303
|
""
|
|
1656
2304
|
].join("\n");
|
|
1657
|
-
|
|
2305
|
+
fs15.writeFileSync(hookPath, script, { mode: 493 });
|
|
1658
2306
|
}
|
|
1659
2307
|
function addLefthookPreCommit(lefthookPath) {
|
|
1660
|
-
const content =
|
|
2308
|
+
const content = fs15.readFileSync(lefthookPath, "utf-8");
|
|
1661
2309
|
if (content.includes("viberails")) return;
|
|
1662
2310
|
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
1663
2311
|
if (!doc["pre-commit"]) {
|
|
@@ -1669,29 +2317,29 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
1669
2317
|
doc["pre-commit"].commands.viberails = {
|
|
1670
2318
|
run: "npx viberails check --staged"
|
|
1671
2319
|
};
|
|
1672
|
-
|
|
2320
|
+
fs15.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
1673
2321
|
}
|
|
1674
2322
|
function detectHookManager(projectRoot) {
|
|
1675
|
-
if (
|
|
1676
|
-
if (
|
|
1677
|
-
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";
|
|
1678
2326
|
return void 0;
|
|
1679
2327
|
}
|
|
1680
2328
|
function setupClaudeCodeHook(projectRoot) {
|
|
1681
|
-
const claudeDir =
|
|
1682
|
-
if (!
|
|
1683
|
-
|
|
2329
|
+
const claudeDir = path15.join(projectRoot, ".claude");
|
|
2330
|
+
if (!fs15.existsSync(claudeDir)) {
|
|
2331
|
+
fs15.mkdirSync(claudeDir, { recursive: true });
|
|
1684
2332
|
}
|
|
1685
|
-
const settingsPath =
|
|
2333
|
+
const settingsPath = path15.join(claudeDir, "settings.json");
|
|
1686
2334
|
let settings = {};
|
|
1687
|
-
if (
|
|
2335
|
+
if (fs15.existsSync(settingsPath)) {
|
|
1688
2336
|
try {
|
|
1689
|
-
settings = JSON.parse(
|
|
2337
|
+
settings = JSON.parse(fs15.readFileSync(settingsPath, "utf-8"));
|
|
1690
2338
|
} catch {
|
|
1691
2339
|
console.warn(
|
|
1692
|
-
` ${
|
|
2340
|
+
` ${import_chalk8.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
1693
2341
|
);
|
|
1694
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2342
|
+
console.warn(` Fix the JSON manually, then re-run ${import_chalk8.default.cyan("viberails init --force")}`);
|
|
1695
2343
|
return;
|
|
1696
2344
|
}
|
|
1697
2345
|
}
|
|
@@ -1712,208 +2360,406 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
1712
2360
|
}
|
|
1713
2361
|
];
|
|
1714
2362
|
settings.hooks = hooks;
|
|
1715
|
-
|
|
2363
|
+
fs15.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
1716
2364
|
`);
|
|
1717
|
-
console.log(` ${
|
|
2365
|
+
console.log(` ${import_chalk8.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
1718
2366
|
}
|
|
1719
2367
|
function setupClaudeMdReference(projectRoot) {
|
|
1720
|
-
const claudeMdPath =
|
|
2368
|
+
const claudeMdPath = path15.join(projectRoot, "CLAUDE.md");
|
|
1721
2369
|
let content = "";
|
|
1722
|
-
if (
|
|
1723
|
-
content =
|
|
2370
|
+
if (fs15.existsSync(claudeMdPath)) {
|
|
2371
|
+
content = fs15.readFileSync(claudeMdPath, "utf-8");
|
|
1724
2372
|
}
|
|
1725
2373
|
if (content.includes("@.viberails/context.md")) return;
|
|
1726
2374
|
const ref = "\n@.viberails/context.md\n";
|
|
1727
2375
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
1728
|
-
|
|
1729
|
-
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";
|
|
1730
2422
|
}
|
|
1731
2423
|
function writeHuskyPreCommit(huskyDir) {
|
|
1732
|
-
const hookPath =
|
|
1733
|
-
if (
|
|
1734
|
-
const existing =
|
|
2424
|
+
const hookPath = path15.join(huskyDir, "pre-commit");
|
|
2425
|
+
if (fs15.existsSync(hookPath)) {
|
|
2426
|
+
const existing = fs15.readFileSync(hookPath, "utf-8");
|
|
1735
2427
|
if (!existing.includes("viberails")) {
|
|
1736
|
-
|
|
2428
|
+
fs15.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
1737
2429
|
npx viberails check --staged
|
|
1738
2430
|
`);
|
|
1739
2431
|
}
|
|
1740
2432
|
return;
|
|
1741
2433
|
}
|
|
1742
|
-
|
|
2434
|
+
fs15.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
1743
2435
|
}
|
|
1744
2436
|
|
|
1745
|
-
// src/commands/init.ts
|
|
1746
|
-
var
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
+
});
|
|
1755
2492
|
}
|
|
2493
|
+
return ".git/hooks/pre-commit";
|
|
1756
2494
|
}
|
|
1757
|
-
return
|
|
2495
|
+
return void 0;
|
|
1758
2496
|
}
|
|
1759
|
-
function
|
|
1760
|
-
|
|
1761
|
-
|
|
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;
|
|
2503
|
+
}
|
|
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);
|
|
1762
2546
|
}
|
|
1763
2547
|
async function initCommand(options, cwd) {
|
|
1764
|
-
const
|
|
1765
|
-
const projectRoot = findProjectRoot(startDir);
|
|
2548
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
1766
2549
|
if (!projectRoot) {
|
|
1767
2550
|
throw new Error(
|
|
1768
|
-
"No package.json found
|
|
2551
|
+
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
1769
2552
|
);
|
|
1770
2553
|
}
|
|
1771
|
-
const configPath =
|
|
1772
|
-
if (
|
|
2554
|
+
const configPath = path17.join(projectRoot, CONFIG_FILE4);
|
|
2555
|
+
if (fs17.existsSync(configPath) && !options.force) {
|
|
1773
2556
|
console.log(
|
|
1774
|
-
`${
|
|
1775
|
-
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.`
|
|
1776
2559
|
);
|
|
1777
2560
|
return;
|
|
1778
2561
|
}
|
|
1779
|
-
if (options.yes)
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
}
|
|
1802
|
-
|
|
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`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
const compacted = (0, import_config6.compactConfig)(config);
|
|
2596
|
+
fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
1803
2597
|
`);
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
+
}
|
|
1813
2635
|
}
|
|
1814
|
-
|
|
1815
|
-
const s = clack2.spinner();
|
|
2636
|
+
const s = clack7.spinner();
|
|
1816
2637
|
s.start("Scanning project...");
|
|
1817
2638
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
1818
|
-
const config = (0,
|
|
2639
|
+
const config = (0, import_config6.generateConfig)(scanResult);
|
|
1819
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
|
+
}
|
|
1820
2648
|
if (scanResult.statistics.totalFiles === 0) {
|
|
1821
|
-
|
|
1822
|
-
"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."
|
|
1823
2651
|
);
|
|
1824
2652
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
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");
|
|
1827
2659
|
const decision = await promptInitDecision();
|
|
1828
2660
|
if (decision === "customize") {
|
|
2661
|
+
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1829
2662
|
const overrides = await promptRuleMenu({
|
|
1830
2663
|
maxFileLines: config.rules.maxFileLines,
|
|
1831
|
-
|
|
2664
|
+
testCoverage: config.rules.testCoverage,
|
|
2665
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
1832
2666
|
enforceNaming: config.rules.enforceNaming,
|
|
1833
|
-
|
|
1834
|
-
|
|
2667
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2668
|
+
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
2669
|
+
coverageCommand: config.defaults?.coverage?.command,
|
|
1835
2670
|
packageOverrides: config.packages
|
|
1836
2671
|
});
|
|
2672
|
+
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1837
2673
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1838
|
-
config.rules.
|
|
2674
|
+
config.rules.testCoverage = overrides.testCoverage;
|
|
2675
|
+
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
1839
2676
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
1840
|
-
config.
|
|
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
|
+
}
|
|
2697
|
+
}
|
|
1841
2698
|
}
|
|
1842
|
-
if (config.
|
|
1843
|
-
|
|
2699
|
+
if (config.packages.length > 1) {
|
|
2700
|
+
clack7.note(
|
|
1844
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.",
|
|
1845
2702
|
"Boundaries"
|
|
1846
2703
|
);
|
|
1847
|
-
const shouldInfer = await
|
|
2704
|
+
const shouldInfer = await confirm3("Infer boundary rules from import patterns?");
|
|
1848
2705
|
if (shouldInfer) {
|
|
1849
|
-
const bs =
|
|
2706
|
+
const bs = clack7.spinner();
|
|
1850
2707
|
bs.start("Building import graph...");
|
|
1851
2708
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1852
|
-
const packages = resolveWorkspacePackages(projectRoot, config.
|
|
1853
|
-
const graph = await buildImportGraph(projectRoot, {
|
|
1854
|
-
packages,
|
|
1855
|
-
ignore: config.ignore
|
|
1856
|
-
});
|
|
2709
|
+
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2710
|
+
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
1857
2711
|
const inferred = inferBoundaries(graph);
|
|
1858
2712
|
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
1859
2713
|
if (denyCount > 0) {
|
|
1860
2714
|
config.boundaries = inferred;
|
|
1861
2715
|
config.rules.enforceBoundaries = true;
|
|
1862
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");
|
|
1863
2719
|
} else {
|
|
1864
2720
|
bs.stop("No boundary rules inferred");
|
|
1865
2721
|
}
|
|
1866
2722
|
}
|
|
1867
2723
|
}
|
|
1868
2724
|
const hookManager = detectHookManager(projectRoot);
|
|
1869
|
-
const
|
|
1870
|
-
|
|
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;
|
|
2734
|
+
}
|
|
2735
|
+
const compacted = (0, import_config6.compactConfig)(config);
|
|
2736
|
+
fs17.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
1871
2737
|
`);
|
|
1872
2738
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1873
2739
|
updateGitignore(projectRoot);
|
|
1874
2740
|
const createdFiles = [
|
|
1875
|
-
|
|
2741
|
+
path17.basename(configPath),
|
|
1876
2742
|
".viberails/context.md",
|
|
1877
|
-
".viberails/scan-result.json"
|
|
2743
|
+
".viberails/scan-result.json",
|
|
2744
|
+
...setupSelectedIntegrations(projectRoot, integrations, {
|
|
2745
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
2746
|
+
packageManager: rootPkgStack?.packageManager
|
|
2747
|
+
})
|
|
1878
2748
|
];
|
|
1879
|
-
|
|
1880
|
-
setupPreCommitHook(projectRoot);
|
|
1881
|
-
if (hookManager === "Lefthook") {
|
|
1882
|
-
createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
if (integrations.claudeCodeHook) {
|
|
1886
|
-
setupClaudeCodeHook(projectRoot);
|
|
1887
|
-
createdFiles.push(".claude/settings.json \u2014 added viberails hook");
|
|
1888
|
-
}
|
|
1889
|
-
if (integrations.claudeMdRef) {
|
|
1890
|
-
setupClaudeMdReference(projectRoot);
|
|
1891
|
-
createdFiles.push("CLAUDE.md \u2014 added @.viberails/context.md reference");
|
|
1892
|
-
}
|
|
1893
|
-
clack2.log.success(`Created:
|
|
2749
|
+
clack7.log.success(`Created:
|
|
1894
2750
|
${createdFiles.map((f) => ` ${f}`).join("\n")}`);
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
let content = "";
|
|
1900
|
-
if (fs13.existsSync(gitignorePath)) {
|
|
1901
|
-
content = fs13.readFileSync(gitignorePath, "utf-8");
|
|
1902
|
-
}
|
|
1903
|
-
if (!content.includes(".viberails/scan-result.json")) {
|
|
1904
|
-
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
1905
|
-
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
1906
|
-
`;
|
|
1907
|
-
fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
1908
|
-
}
|
|
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
|
+
);
|
|
1909
2755
|
}
|
|
1910
2756
|
|
|
1911
2757
|
// src/commands/sync.ts
|
|
1912
|
-
var
|
|
1913
|
-
var
|
|
1914
|
-
var
|
|
2758
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
2759
|
+
var path18 = __toESM(require("path"), 1);
|
|
2760
|
+
var import_config7 = require("@viberails/config");
|
|
1915
2761
|
var import_scanner2 = require("@viberails/scanner");
|
|
1916
|
-
var
|
|
2762
|
+
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
1917
2763
|
|
|
1918
2764
|
// src/utils/diff-configs.ts
|
|
1919
2765
|
var import_types5 = require("@viberails/types");
|
|
@@ -1934,11 +2780,8 @@ function displayStackName(s) {
|
|
|
1934
2780
|
const display = allMaps[name] ?? name;
|
|
1935
2781
|
return version ? `${display} ${version}` : display;
|
|
1936
2782
|
}
|
|
1937
|
-
function
|
|
1938
|
-
return
|
|
1939
|
-
}
|
|
1940
|
-
function isDetected(cv) {
|
|
1941
|
-
return typeof cv !== "string" && cv._detected === true;
|
|
2783
|
+
function isNewlyDetected(config, pkgPath, key) {
|
|
2784
|
+
return config._meta?.packages?.[pkgPath]?.conventions?.[key]?.detected === true;
|
|
1942
2785
|
}
|
|
1943
2786
|
var STACK_FIELDS = [
|
|
1944
2787
|
"framework",
|
|
@@ -1965,59 +2808,66 @@ var STRUCTURE_FIELDS = [
|
|
|
1965
2808
|
{ key: "tests", label: "tests directory" },
|
|
1966
2809
|
{ key: "testPattern", label: "test pattern" }
|
|
1967
2810
|
];
|
|
1968
|
-
function
|
|
2811
|
+
function diffPackage(existing, merged, mergedConfig) {
|
|
1969
2812
|
const changes = [];
|
|
2813
|
+
const pkgPrefix = existing.path === "." ? "" : `${existing.path}: `;
|
|
1970
2814
|
for (const field of STACK_FIELDS) {
|
|
1971
|
-
const oldVal = existing.stack[field];
|
|
1972
|
-
const newVal = merged.stack[field];
|
|
2815
|
+
const oldVal = existing.stack?.[field];
|
|
2816
|
+
const newVal = merged.stack?.[field];
|
|
1973
2817
|
if (!oldVal && newVal) {
|
|
1974
|
-
changes.push({
|
|
2818
|
+
changes.push({
|
|
2819
|
+
type: "added",
|
|
2820
|
+
description: `${pkgPrefix}Stack: added ${displayStackName(newVal)}`
|
|
2821
|
+
});
|
|
1975
2822
|
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
1976
2823
|
changes.push({
|
|
1977
2824
|
type: "changed",
|
|
1978
|
-
description:
|
|
2825
|
+
description: `${pkgPrefix}Stack: ${displayStackName(oldVal)} \u2192 ${displayStackName(newVal)}`
|
|
1979
2826
|
});
|
|
1980
2827
|
}
|
|
1981
2828
|
}
|
|
1982
2829
|
for (const key of CONVENTION_KEYS) {
|
|
1983
|
-
const oldVal = existing.conventions[key];
|
|
1984
|
-
const newVal = merged.conventions[key];
|
|
2830
|
+
const oldVal = existing.conventions?.[key];
|
|
2831
|
+
const newVal = merged.conventions?.[key];
|
|
1985
2832
|
const label = import_types5.CONVENTION_LABELS[key] ?? key;
|
|
1986
2833
|
if (!oldVal && newVal) {
|
|
1987
2834
|
changes.push({
|
|
1988
2835
|
type: "added",
|
|
1989
|
-
description:
|
|
2836
|
+
description: `${pkgPrefix}New convention: ${label} (${newVal})`
|
|
1990
2837
|
});
|
|
1991
|
-
} else if (oldVal && newVal &&
|
|
2838
|
+
} else if (oldVal && newVal && oldVal !== newVal) {
|
|
2839
|
+
const suffix = isNewlyDetected(mergedConfig, merged.path, key) ? " (newly detected)" : "";
|
|
1992
2840
|
changes.push({
|
|
1993
2841
|
type: "changed",
|
|
1994
|
-
description:
|
|
2842
|
+
description: `${pkgPrefix}Convention updated: ${label} (${newVal})${suffix}`
|
|
1995
2843
|
});
|
|
1996
2844
|
}
|
|
1997
2845
|
}
|
|
1998
2846
|
for (const { key, label } of STRUCTURE_FIELDS) {
|
|
1999
|
-
const oldVal = existing.structure[key];
|
|
2000
|
-
const newVal = merged.structure[key];
|
|
2847
|
+
const oldVal = existing.structure?.[key];
|
|
2848
|
+
const newVal = merged.structure?.[key];
|
|
2001
2849
|
if (!oldVal && newVal) {
|
|
2002
|
-
changes.push({
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
for (const pkg of merged.packages ?? []) {
|
|
2007
|
-
if (!existingPaths.has(pkg.path)) {
|
|
2008
|
-
changes.push({ type: "added", description: `New package: ${pkg.path}` });
|
|
2850
|
+
changes.push({
|
|
2851
|
+
type: "added",
|
|
2852
|
+
description: `${pkgPrefix}Structure: detected ${label} (${newVal})`
|
|
2853
|
+
});
|
|
2009
2854
|
}
|
|
2010
2855
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
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));
|
|
2016
2866
|
}
|
|
2017
2867
|
}
|
|
2018
|
-
for (const
|
|
2019
|
-
if (!
|
|
2020
|
-
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}` });
|
|
2021
2871
|
}
|
|
2022
2872
|
}
|
|
2023
2873
|
return changes;
|
|
@@ -2042,9 +2892,9 @@ function formatStatsDelta(oldStats, newStats) {
|
|
|
2042
2892
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
2043
2893
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
2044
2894
|
function loadPreviousStats(projectRoot) {
|
|
2045
|
-
const scanResultPath =
|
|
2895
|
+
const scanResultPath = path18.join(projectRoot, SCAN_RESULT_FILE2);
|
|
2046
2896
|
try {
|
|
2047
|
-
const raw =
|
|
2897
|
+
const raw = fs18.readFileSync(scanResultPath, "utf-8");
|
|
2048
2898
|
const parsed = JSON.parse(raw);
|
|
2049
2899
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
2050
2900
|
return parsed.statistics;
|
|
@@ -2061,44 +2911,47 @@ async function syncCommand(cwd) {
|
|
|
2061
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"
|
|
2062
2912
|
);
|
|
2063
2913
|
}
|
|
2064
|
-
const configPath =
|
|
2065
|
-
const existing = await (0,
|
|
2914
|
+
const configPath = path18.join(projectRoot, CONFIG_FILE5);
|
|
2915
|
+
const existing = await (0, import_config7.loadConfig)(configPath);
|
|
2066
2916
|
const previousStats = loadPreviousStats(projectRoot);
|
|
2067
|
-
console.log(
|
|
2917
|
+
console.log(import_chalk11.default.dim("Scanning project..."));
|
|
2068
2918
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
2069
|
-
const merged = (0,
|
|
2070
|
-
const
|
|
2071
|
-
const
|
|
2072
|
-
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;
|
|
2073
2926
|
const changes = configChanged ? diffConfigs(existing, merged) : [];
|
|
2074
2927
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
2075
2928
|
if (changes.length > 0 || statsDelta) {
|
|
2076
2929
|
console.log(`
|
|
2077
|
-
${
|
|
2930
|
+
${import_chalk11.default.bold("Changes:")}`);
|
|
2078
2931
|
for (const change of changes) {
|
|
2079
|
-
const icon = change.type === "removed" ?
|
|
2932
|
+
const icon = change.type === "removed" ? import_chalk11.default.red("-") : import_chalk11.default.green("+");
|
|
2080
2933
|
console.log(` ${icon} ${change.description}`);
|
|
2081
2934
|
}
|
|
2082
2935
|
if (statsDelta) {
|
|
2083
|
-
console.log(` ${
|
|
2936
|
+
console.log(` ${import_chalk11.default.dim(statsDelta)}`);
|
|
2084
2937
|
}
|
|
2085
2938
|
}
|
|
2086
|
-
|
|
2939
|
+
fs18.writeFileSync(configPath, `${compactedJson}
|
|
2087
2940
|
`);
|
|
2088
2941
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
2089
2942
|
console.log(`
|
|
2090
|
-
${
|
|
2943
|
+
${import_chalk11.default.bold("Synced:")}`);
|
|
2091
2944
|
if (configChanged) {
|
|
2092
|
-
console.log(` ${
|
|
2945
|
+
console.log(` ${import_chalk11.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
|
|
2093
2946
|
} else {
|
|
2094
|
-
console.log(` ${
|
|
2947
|
+
console.log(` ${import_chalk11.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
|
|
2095
2948
|
}
|
|
2096
|
-
console.log(` ${
|
|
2097
|
-
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`);
|
|
2098
2951
|
}
|
|
2099
2952
|
|
|
2100
2953
|
// src/index.ts
|
|
2101
|
-
var VERSION = "0.
|
|
2954
|
+
var VERSION = "0.5.0";
|
|
2102
2955
|
var program = new import_commander.Command();
|
|
2103
2956
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
2104
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) => {
|
|
@@ -2106,7 +2959,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
2106
2959
|
await initCommand(options);
|
|
2107
2960
|
} catch (err) {
|
|
2108
2961
|
const message = err instanceof Error ? err.message : String(err);
|
|
2109
|
-
console.error(`${
|
|
2962
|
+
console.error(`${import_chalk12.default.red("Error:")} ${message}`);
|
|
2110
2963
|
process.exit(1);
|
|
2111
2964
|
}
|
|
2112
2965
|
});
|
|
@@ -2115,11 +2968,11 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
2115
2968
|
await syncCommand();
|
|
2116
2969
|
} catch (err) {
|
|
2117
2970
|
const message = err instanceof Error ? err.message : String(err);
|
|
2118
|
-
console.error(`${
|
|
2971
|
+
console.error(`${import_chalk12.default.red("Error:")} ${message}`);
|
|
2119
2972
|
process.exit(1);
|
|
2120
2973
|
}
|
|
2121
2974
|
});
|
|
2122
|
-
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").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").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(
|
|
2123
2976
|
async (options) => {
|
|
2124
2977
|
try {
|
|
2125
2978
|
if (options.hook) {
|
|
@@ -2128,13 +2981,15 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
2128
2981
|
}
|
|
2129
2982
|
const exitCode = await checkCommand({
|
|
2130
2983
|
...options,
|
|
2984
|
+
diffBase: options.diffBase,
|
|
2985
|
+
enforce: options.enforce,
|
|
2131
2986
|
noBoundaries: options.boundaries === false,
|
|
2132
2987
|
format: options.format === "json" ? "json" : "text"
|
|
2133
2988
|
});
|
|
2134
2989
|
process.exit(exitCode);
|
|
2135
2990
|
} catch (err) {
|
|
2136
2991
|
const message = err instanceof Error ? err.message : String(err);
|
|
2137
|
-
console.error(`${
|
|
2992
|
+
console.error(`${import_chalk12.default.red("Error:")} ${message}`);
|
|
2138
2993
|
process.exit(1);
|
|
2139
2994
|
}
|
|
2140
2995
|
}
|
|
@@ -2145,7 +3000,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
2145
3000
|
process.exit(exitCode);
|
|
2146
3001
|
} catch (err) {
|
|
2147
3002
|
const message = err instanceof Error ? err.message : String(err);
|
|
2148
|
-
console.error(`${
|
|
3003
|
+
console.error(`${import_chalk12.default.red("Error:")} ${message}`);
|
|
2149
3004
|
process.exit(1);
|
|
2150
3005
|
}
|
|
2151
3006
|
});
|
|
@@ -2154,7 +3009,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
2154
3009
|
await boundariesCommand(options);
|
|
2155
3010
|
} catch (err) {
|
|
2156
3011
|
const message = err instanceof Error ? err.message : String(err);
|
|
2157
|
-
console.error(`${
|
|
3012
|
+
console.error(`${import_chalk12.default.red("Error:")} ${message}`);
|
|
2158
3013
|
process.exit(1);
|
|
2159
3014
|
}
|
|
2160
3015
|
});
|