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