viberails 0.6.3 → 0.6.5
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/chunk-XQKOK3FU.js +821 -0
- package/dist/chunk-XQKOK3FU.js.map +1 -0
- package/dist/index.cjs +1094 -526
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +392 -752
- package/dist/index.js.map +1 -1
- package/dist/prompt-naming-default-AH54HEBC.js +57 -0
- package/dist/prompt-naming-default-AH54HEBC.js.map +1 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertNotCancelled,
|
|
4
|
+
confirm,
|
|
5
|
+
confirmDangerous,
|
|
6
|
+
getRootPackage,
|
|
7
|
+
promptExistingConfigAction,
|
|
8
|
+
promptInitDecision,
|
|
9
|
+
promptIntegrations,
|
|
10
|
+
promptRuleMenu,
|
|
11
|
+
spawnAsync
|
|
12
|
+
} from "./chunk-XQKOK3FU.js";
|
|
2
13
|
|
|
3
14
|
// src/index.ts
|
|
4
|
-
import
|
|
15
|
+
import chalk16 from "chalk";
|
|
5
16
|
import { Command } from "commander";
|
|
6
17
|
|
|
7
18
|
// src/commands/boundaries.ts
|
|
@@ -27,530 +38,6 @@ function findProjectRoot(startDir) {
|
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
// src/utils/prompt.ts
|
|
31
|
-
import * as clack5 from "@clack/prompts";
|
|
32
|
-
|
|
33
|
-
// src/utils/prompt-integrations.ts
|
|
34
|
-
import { spawnSync } from "child_process";
|
|
35
|
-
import * as clack from "@clack/prompts";
|
|
36
|
-
async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
|
|
37
|
-
const choice = await clack.select({
|
|
38
|
-
message: "No git hook manager detected. Install Lefthook for shareable pre-commit hooks?",
|
|
39
|
-
options: [
|
|
40
|
-
{
|
|
41
|
-
value: "install",
|
|
42
|
-
label: "Yes, install Lefthook",
|
|
43
|
-
hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
value: "skip",
|
|
47
|
-
label: "No, skip",
|
|
48
|
-
hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
});
|
|
52
|
-
assertNotCancelled(choice);
|
|
53
|
-
if (choice !== "install") return void 0;
|
|
54
|
-
const pm = packageManager || "npm";
|
|
55
|
-
const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
|
|
56
|
-
const s = clack.spinner();
|
|
57
|
-
s.start("Installing Lefthook...");
|
|
58
|
-
const result = spawnSync(installCmd, {
|
|
59
|
-
cwd: projectRoot,
|
|
60
|
-
shell: true,
|
|
61
|
-
encoding: "utf-8",
|
|
62
|
-
stdio: "pipe"
|
|
63
|
-
});
|
|
64
|
-
if (result.status === 0) {
|
|
65
|
-
const fs21 = await import("fs");
|
|
66
|
-
const path21 = await import("path");
|
|
67
|
-
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
68
|
-
if (!fs21.existsSync(lefthookPath)) {
|
|
69
|
-
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
70
|
-
}
|
|
71
|
-
s.stop("Installed Lefthook");
|
|
72
|
-
return "Lefthook";
|
|
73
|
-
}
|
|
74
|
-
s.stop("Failed to install Lefthook");
|
|
75
|
-
clack.log.warn(`Install manually: ${installCmd}`);
|
|
76
|
-
return void 0;
|
|
77
|
-
}
|
|
78
|
-
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
79
|
-
let resolvedHookManager = hookManager;
|
|
80
|
-
if (!resolvedHookManager) {
|
|
81
|
-
resolvedHookManager = await promptHookManagerInstall(
|
|
82
|
-
projectRoot,
|
|
83
|
-
tools?.packageManager ?? "npm",
|
|
84
|
-
tools?.isWorkspace
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
const isBareHook = !resolvedHookManager;
|
|
88
|
-
const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
|
|
89
|
-
const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
|
|
90
|
-
const options = [
|
|
91
|
-
{
|
|
92
|
-
value: "preCommit",
|
|
93
|
-
label: hookLabel,
|
|
94
|
-
hint: hookHint
|
|
95
|
-
}
|
|
96
|
-
];
|
|
97
|
-
if (tools?.isTypeScript) {
|
|
98
|
-
options.push({
|
|
99
|
-
value: "typecheck",
|
|
100
|
-
label: "Typecheck (tsc --noEmit)",
|
|
101
|
-
hint: "pre-commit hook + CI check"
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (tools?.linter) {
|
|
105
|
-
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
106
|
-
options.push({
|
|
107
|
-
value: "lint",
|
|
108
|
-
label: `Lint check (${linterName})`,
|
|
109
|
-
hint: "pre-commit hook + CI check"
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
options.push(
|
|
113
|
-
{
|
|
114
|
-
value: "claude",
|
|
115
|
-
label: "Claude Code hook",
|
|
116
|
-
hint: "checks files when Claude edits them"
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
value: "claudeMd",
|
|
120
|
-
label: "CLAUDE.md reference",
|
|
121
|
-
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
value: "githubAction",
|
|
125
|
-
label: "GitHub Actions workflow",
|
|
126
|
-
hint: "blocks PRs that fail viberails check"
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
|
|
130
|
-
const result = await clack.multiselect({
|
|
131
|
-
message: "Set up integrations?",
|
|
132
|
-
options,
|
|
133
|
-
initialValues,
|
|
134
|
-
required: false
|
|
135
|
-
});
|
|
136
|
-
assertNotCancelled(result);
|
|
137
|
-
return {
|
|
138
|
-
preCommitHook: result.includes("preCommit"),
|
|
139
|
-
claudeCodeHook: result.includes("claude"),
|
|
140
|
-
claudeMdRef: result.includes("claudeMd"),
|
|
141
|
-
githubAction: result.includes("githubAction"),
|
|
142
|
-
typecheckHook: result.includes("typecheck"),
|
|
143
|
-
lintHook: result.includes("lint")
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/utils/prompt-rules.ts
|
|
148
|
-
import * as clack4 from "@clack/prompts";
|
|
149
|
-
|
|
150
|
-
// src/utils/prompt-menu-handlers.ts
|
|
151
|
-
import * as clack3 from "@clack/prompts";
|
|
152
|
-
|
|
153
|
-
// src/utils/prompt-package-overrides.ts
|
|
154
|
-
import * as clack2 from "@clack/prompts";
|
|
155
|
-
function normalizePackageOverrides(packages) {
|
|
156
|
-
for (const pkg of packages) {
|
|
157
|
-
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
158
|
-
delete pkg.rules;
|
|
159
|
-
}
|
|
160
|
-
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
161
|
-
delete pkg.coverage;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return packages;
|
|
165
|
-
}
|
|
166
|
-
function packageCoverageHint(pkg, defaults) {
|
|
167
|
-
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
168
|
-
const isExempt = coverage === 0;
|
|
169
|
-
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
170
|
-
const defaultCommand = defaults.coverageCommand ?? "";
|
|
171
|
-
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
172
|
-
const tags = [];
|
|
173
|
-
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
174
|
-
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
175
|
-
tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
|
|
176
|
-
if (hasSummaryOverride) tags.push("summary override");
|
|
177
|
-
if (hasCommandOverride) tags.push("command override");
|
|
178
|
-
return tags.join(", ");
|
|
179
|
-
}
|
|
180
|
-
async function promptPackageCoverageOverrides(packages, defaults) {
|
|
181
|
-
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
182
|
-
if (editablePackages.length === 0) return packages;
|
|
183
|
-
while (true) {
|
|
184
|
-
const selectedPath = await clack2.select({
|
|
185
|
-
message: "Select package to edit coverage overrides",
|
|
186
|
-
options: [
|
|
187
|
-
...editablePackages.map((pkg) => ({
|
|
188
|
-
value: pkg.path,
|
|
189
|
-
label: `${pkg.path} (${pkg.name})`,
|
|
190
|
-
hint: packageCoverageHint(pkg, defaults)
|
|
191
|
-
})),
|
|
192
|
-
{ value: "__done__", label: "Done" }
|
|
193
|
-
]
|
|
194
|
-
});
|
|
195
|
-
assertNotCancelled(selectedPath);
|
|
196
|
-
if (selectedPath === "__done__") break;
|
|
197
|
-
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
198
|
-
if (!target) continue;
|
|
199
|
-
while (true) {
|
|
200
|
-
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
201
|
-
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
202
|
-
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
|
|
203
|
-
const choice = await clack2.select({
|
|
204
|
-
message: `Edit coverage overrides for ${target.path}`,
|
|
205
|
-
options: [
|
|
206
|
-
{ value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
|
|
207
|
-
{ value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
|
|
208
|
-
{ value: "command", label: "coverage.command", hint: effectiveCommand },
|
|
209
|
-
{ value: "reset", label: "Reset this package to inherit defaults" },
|
|
210
|
-
{ value: "back", label: "Back to package list" }
|
|
211
|
-
]
|
|
212
|
-
});
|
|
213
|
-
assertNotCancelled(choice);
|
|
214
|
-
if (choice === "back") break;
|
|
215
|
-
if (choice === "testCoverage") {
|
|
216
|
-
const result = await clack2.text({
|
|
217
|
-
message: "Package testCoverage (0 to exempt package)?",
|
|
218
|
-
initialValue: String(effectiveCoverage),
|
|
219
|
-
validate: (v) => {
|
|
220
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
221
|
-
const n = Number.parseInt(v, 10);
|
|
222
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
assertNotCancelled(result);
|
|
226
|
-
const nextCoverage = Number.parseInt(result, 10);
|
|
227
|
-
if (nextCoverage === defaults.testCoverage) {
|
|
228
|
-
if (target.rules) {
|
|
229
|
-
delete target.rules.testCoverage;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (choice === "summaryPath") {
|
|
236
|
-
const result = await clack2.text({
|
|
237
|
-
message: "Package coverage.summaryPath (blank to inherit default)?",
|
|
238
|
-
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
239
|
-
placeholder: defaults.coverageSummaryPath
|
|
240
|
-
});
|
|
241
|
-
assertNotCancelled(result);
|
|
242
|
-
const value = result.trim();
|
|
243
|
-
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
244
|
-
if (target.coverage) {
|
|
245
|
-
delete target.coverage.summaryPath;
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (choice === "command") {
|
|
252
|
-
const result = await clack2.text({
|
|
253
|
-
message: "Package coverage.command (blank to inherit default/auto)?",
|
|
254
|
-
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
255
|
-
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
256
|
-
});
|
|
257
|
-
assertNotCancelled(result);
|
|
258
|
-
const value = result.trim();
|
|
259
|
-
const defaultCommand = defaults.coverageCommand ?? "";
|
|
260
|
-
if (value.length === 0 || value === defaultCommand) {
|
|
261
|
-
if (target.coverage) {
|
|
262
|
-
delete target.coverage.command;
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (choice === "reset") {
|
|
269
|
-
if (target.rules) {
|
|
270
|
-
delete target.rules.testCoverage;
|
|
271
|
-
}
|
|
272
|
-
delete target.coverage;
|
|
273
|
-
}
|
|
274
|
-
normalizePackageOverrides(editablePackages);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return normalizePackageOverrides(packages);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/utils/prompt-menu-handlers.ts
|
|
281
|
-
function getPackageDiffs(pkg, root) {
|
|
282
|
-
const diffs = [];
|
|
283
|
-
const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
|
|
284
|
-
for (const key of convKeys) {
|
|
285
|
-
if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
|
|
286
|
-
diffs.push(`${key}: ${pkg.conventions[key]}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const stackKeys = [
|
|
290
|
-
"framework",
|
|
291
|
-
"language",
|
|
292
|
-
"styling",
|
|
293
|
-
"backend",
|
|
294
|
-
"orm",
|
|
295
|
-
"linter",
|
|
296
|
-
"formatter",
|
|
297
|
-
"testRunner",
|
|
298
|
-
"packageManager"
|
|
299
|
-
];
|
|
300
|
-
for (const key of stackKeys) {
|
|
301
|
-
if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
|
|
302
|
-
diffs.push(`${key}: ${pkg.stack[key]}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
306
|
-
diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
|
|
307
|
-
}
|
|
308
|
-
if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
|
|
309
|
-
diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
|
|
310
|
-
}
|
|
311
|
-
if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
|
|
312
|
-
diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
|
|
313
|
-
}
|
|
314
|
-
if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
|
|
315
|
-
diffs.push("coverage.command: (override)");
|
|
316
|
-
}
|
|
317
|
-
return diffs;
|
|
318
|
-
}
|
|
319
|
-
function buildMenuOptions(state, packageCount) {
|
|
320
|
-
const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
|
|
321
|
-
const options = [
|
|
322
|
-
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
323
|
-
{ value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
|
|
324
|
-
];
|
|
325
|
-
if (state.fileNamingValue) {
|
|
326
|
-
options.push({
|
|
327
|
-
value: "fileNaming",
|
|
328
|
-
label: "File naming convention",
|
|
329
|
-
hint: state.fileNamingValue
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
const isMonorepo = packageCount > 0;
|
|
333
|
-
const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
|
|
334
|
-
const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
|
|
335
|
-
options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
|
|
336
|
-
options.push({
|
|
337
|
-
value: "enforceMissingTests",
|
|
338
|
-
label: "Enforce missing tests",
|
|
339
|
-
hint: state.enforceMissingTests ? "yes" : "no"
|
|
340
|
-
});
|
|
341
|
-
if (state.testCoverage > 0) {
|
|
342
|
-
options.push(
|
|
343
|
-
{
|
|
344
|
-
value: "coverageSummaryPath",
|
|
345
|
-
label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
|
|
346
|
-
hint: state.coverageSummaryPath
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
value: "coverageCommand",
|
|
350
|
-
label: isMonorepo ? "Default coverage command" : "Coverage command",
|
|
351
|
-
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
352
|
-
}
|
|
353
|
-
);
|
|
354
|
-
if (isMonorepo) {
|
|
355
|
-
options.push({
|
|
356
|
-
value: "packageOverrides",
|
|
357
|
-
label: "Per-package coverage overrides",
|
|
358
|
-
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
options.push(
|
|
363
|
-
{ value: "reset", label: "Reset all to detected defaults" },
|
|
364
|
-
{ value: "done", label: "Done" }
|
|
365
|
-
);
|
|
366
|
-
return options;
|
|
367
|
-
}
|
|
368
|
-
function clonePackages(packages) {
|
|
369
|
-
return packages?.map((pkg) => ({
|
|
370
|
-
...pkg,
|
|
371
|
-
stack: pkg.stack ? { ...pkg.stack } : void 0,
|
|
372
|
-
structure: pkg.structure ? { ...pkg.structure } : void 0,
|
|
373
|
-
conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
|
|
374
|
-
rules: pkg.rules ? { ...pkg.rules } : void 0,
|
|
375
|
-
coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
|
|
376
|
-
ignore: pkg.ignore ? [...pkg.ignore] : void 0,
|
|
377
|
-
boundaries: pkg.boundaries ? {
|
|
378
|
-
deny: [...pkg.boundaries.deny],
|
|
379
|
-
ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
|
|
380
|
-
} : void 0
|
|
381
|
-
}));
|
|
382
|
-
}
|
|
383
|
-
async function handleMenuChoice(choice, state, defaults, root) {
|
|
384
|
-
if (choice === "reset") {
|
|
385
|
-
state.maxFileLines = defaults.maxFileLines;
|
|
386
|
-
state.testCoverage = defaults.testCoverage;
|
|
387
|
-
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
388
|
-
state.enforceNaming = defaults.enforceNaming;
|
|
389
|
-
state.fileNamingValue = defaults.fileNamingValue;
|
|
390
|
-
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
391
|
-
state.coverageCommand = defaults.coverageCommand;
|
|
392
|
-
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
393
|
-
clack3.log.info("Reset all rules to detected defaults.");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
if (choice === "packageOverrides") {
|
|
397
|
-
if (state.packageOverrides) {
|
|
398
|
-
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
399
|
-
state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
|
|
400
|
-
testCoverage: state.testCoverage,
|
|
401
|
-
coverageSummaryPath: state.coverageSummaryPath,
|
|
402
|
-
coverageCommand: state.coverageCommand
|
|
403
|
-
});
|
|
404
|
-
const lines = packageDiffs.map((entry) => `${entry.pkg.path}
|
|
405
|
-
${entry.diffs.join(", ")}`);
|
|
406
|
-
if (lines.length > 0) {
|
|
407
|
-
clack3.note(lines.join("\n\n"), "Existing package differences");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (choice === "maxFileLines") {
|
|
413
|
-
const result = await clack3.text({
|
|
414
|
-
message: "Maximum lines per source file?",
|
|
415
|
-
initialValue: String(state.maxFileLines),
|
|
416
|
-
validate: (v) => {
|
|
417
|
-
if (typeof v !== "string") return "Enter a positive number";
|
|
418
|
-
const n = Number.parseInt(v, 10);
|
|
419
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
assertNotCancelled(result);
|
|
423
|
-
state.maxFileLines = Number.parseInt(result, 10);
|
|
424
|
-
}
|
|
425
|
-
if (choice === "enforceMissingTests") {
|
|
426
|
-
const result = await clack3.confirm({
|
|
427
|
-
message: "Require every source file to have a corresponding test file?",
|
|
428
|
-
initialValue: state.enforceMissingTests
|
|
429
|
-
});
|
|
430
|
-
assertNotCancelled(result);
|
|
431
|
-
state.enforceMissingTests = result;
|
|
432
|
-
}
|
|
433
|
-
if (choice === "testCoverage") {
|
|
434
|
-
const result = await clack3.text({
|
|
435
|
-
message: "Test coverage target (0 disables coverage checks)?",
|
|
436
|
-
initialValue: String(state.testCoverage),
|
|
437
|
-
validate: (v) => {
|
|
438
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
439
|
-
const n = Number.parseInt(v, 10);
|
|
440
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
assertNotCancelled(result);
|
|
444
|
-
state.testCoverage = Number.parseInt(result, 10);
|
|
445
|
-
}
|
|
446
|
-
if (choice === "coverageSummaryPath") {
|
|
447
|
-
const result = await clack3.text({
|
|
448
|
-
message: "Coverage summary path (relative to package root)?",
|
|
449
|
-
initialValue: state.coverageSummaryPath,
|
|
450
|
-
validate: (v) => {
|
|
451
|
-
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
assertNotCancelled(result);
|
|
455
|
-
state.coverageSummaryPath = result.trim();
|
|
456
|
-
}
|
|
457
|
-
if (choice === "coverageCommand") {
|
|
458
|
-
const result = await clack3.text({
|
|
459
|
-
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
460
|
-
initialValue: state.coverageCommand ?? "",
|
|
461
|
-
placeholder: "(auto-detect from package.json test runner)"
|
|
462
|
-
});
|
|
463
|
-
assertNotCancelled(result);
|
|
464
|
-
const trimmed = result.trim();
|
|
465
|
-
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
466
|
-
}
|
|
467
|
-
if (choice === "enforceNaming") {
|
|
468
|
-
const result = await clack3.confirm({
|
|
469
|
-
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
470
|
-
initialValue: state.enforceNaming
|
|
471
|
-
});
|
|
472
|
-
assertNotCancelled(result);
|
|
473
|
-
state.enforceNaming = result;
|
|
474
|
-
}
|
|
475
|
-
if (choice === "fileNaming") {
|
|
476
|
-
const selected = await clack3.select({
|
|
477
|
-
message: "Which file naming convention should be enforced?",
|
|
478
|
-
options: [
|
|
479
|
-
{ value: "kebab-case", label: "kebab-case" },
|
|
480
|
-
{ value: "camelCase", label: "camelCase" },
|
|
481
|
-
{ value: "PascalCase", label: "PascalCase" },
|
|
482
|
-
{ value: "snake_case", label: "snake_case" }
|
|
483
|
-
],
|
|
484
|
-
initialValue: state.fileNamingValue
|
|
485
|
-
});
|
|
486
|
-
assertNotCancelled(selected);
|
|
487
|
-
state.fileNamingValue = selected;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// src/utils/prompt-rules.ts
|
|
492
|
-
function getRootPackage(packages) {
|
|
493
|
-
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
494
|
-
}
|
|
495
|
-
async function promptRuleMenu(defaults) {
|
|
496
|
-
const state = {
|
|
497
|
-
...defaults,
|
|
498
|
-
packageOverrides: clonePackages(defaults.packageOverrides)
|
|
499
|
-
};
|
|
500
|
-
const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
|
|
501
|
-
const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
|
|
502
|
-
while (true) {
|
|
503
|
-
const options = buildMenuOptions(state, packageCount);
|
|
504
|
-
const choice = await clack4.select({ message: "Customize rules", options });
|
|
505
|
-
assertNotCancelled(choice);
|
|
506
|
-
if (choice === "done") break;
|
|
507
|
-
await handleMenuChoice(choice, state, defaults, root);
|
|
508
|
-
}
|
|
509
|
-
return {
|
|
510
|
-
maxFileLines: state.maxFileLines,
|
|
511
|
-
testCoverage: state.testCoverage,
|
|
512
|
-
enforceMissingTests: state.enforceMissingTests,
|
|
513
|
-
enforceNaming: state.enforceNaming,
|
|
514
|
-
fileNamingValue: state.fileNamingValue,
|
|
515
|
-
coverageSummaryPath: state.coverageSummaryPath,
|
|
516
|
-
coverageCommand: state.coverageCommand,
|
|
517
|
-
packageOverrides: state.packageOverrides
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// src/utils/prompt.ts
|
|
522
|
-
function assertNotCancelled(value) {
|
|
523
|
-
if (clack5.isCancel(value)) {
|
|
524
|
-
clack5.cancel("Setup cancelled.");
|
|
525
|
-
process.exit(0);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
async function confirm3(message) {
|
|
529
|
-
const result = await clack5.confirm({ message, initialValue: true });
|
|
530
|
-
assertNotCancelled(result);
|
|
531
|
-
return result;
|
|
532
|
-
}
|
|
533
|
-
async function confirmDangerous(message) {
|
|
534
|
-
const result = await clack5.confirm({ message, initialValue: false });
|
|
535
|
-
assertNotCancelled(result);
|
|
536
|
-
return result;
|
|
537
|
-
}
|
|
538
|
-
async function promptInitDecision() {
|
|
539
|
-
const result = await clack5.select({
|
|
540
|
-
message: "Accept these rules?",
|
|
541
|
-
options: [
|
|
542
|
-
{
|
|
543
|
-
value: "accept",
|
|
544
|
-
label: "Yes, looks good",
|
|
545
|
-
hint: "warns on violation; use --enforce in CI to block"
|
|
546
|
-
},
|
|
547
|
-
{ value: "customize", label: "Let me customize rules" }
|
|
548
|
-
]
|
|
549
|
-
});
|
|
550
|
-
assertNotCancelled(result);
|
|
551
|
-
return result;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
41
|
// src/utils/resolve-workspace-packages.ts
|
|
555
42
|
import * as fs2 from "fs";
|
|
556
43
|
import * as path2 from "path";
|
|
@@ -655,7 +142,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
655
142
|
console.log(`
|
|
656
143
|
${totalRules} denied`);
|
|
657
144
|
console.log("");
|
|
658
|
-
const shouldSave = await
|
|
145
|
+
const shouldSave = await confirm("Save to viberails.config.json?");
|
|
659
146
|
if (shouldSave) {
|
|
660
147
|
config.boundaries = inferred;
|
|
661
148
|
config.rules.enforceBoundaries = true;
|
|
@@ -742,7 +229,7 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
742
229
|
}
|
|
743
230
|
|
|
744
231
|
// src/commands/check-coverage.ts
|
|
745
|
-
import { spawnSync
|
|
232
|
+
import { spawnSync } from "child_process";
|
|
746
233
|
import * as fs4 from "fs";
|
|
747
234
|
import * as path4 from "path";
|
|
748
235
|
import { inferCoverageCommand } from "@viberails/config";
|
|
@@ -785,7 +272,7 @@ function readCoveragePercentage(summaryPath) {
|
|
|
785
272
|
}
|
|
786
273
|
}
|
|
787
274
|
function runCoverageCommand(pkgRoot, command) {
|
|
788
|
-
const result =
|
|
275
|
+
const result = spawnSync(command, {
|
|
789
276
|
cwd: pkgRoot,
|
|
790
277
|
shell: true,
|
|
791
278
|
encoding: "utf-8",
|
|
@@ -1167,13 +654,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1167
654
|
const testSuffix = testPattern.replace("*", "");
|
|
1168
655
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1169
656
|
for (const relFile of sourceFiles) {
|
|
1170
|
-
const
|
|
1171
|
-
if (
|
|
657
|
+
const basename10 = path6.basename(relFile);
|
|
658
|
+
if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
|
|
1172
659
|
continue;
|
|
1173
660
|
}
|
|
1174
|
-
const ext = path6.extname(
|
|
661
|
+
const ext = path6.extname(basename10);
|
|
1175
662
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1176
|
-
const stem =
|
|
663
|
+
const stem = basename10.slice(0, -ext.length);
|
|
1177
664
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1178
665
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1179
666
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
@@ -1254,9 +741,9 @@ async function checkCommand(options, cwd) {
|
|
|
1254
741
|
}
|
|
1255
742
|
const violations = [];
|
|
1256
743
|
const severity = options.enforce ? "error" : "warn";
|
|
1257
|
-
const
|
|
744
|
+
const log5 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
|
|
1258
745
|
};
|
|
1259
|
-
|
|
746
|
+
log5(" Checking files...");
|
|
1260
747
|
for (const file of filesToCheck) {
|
|
1261
748
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1262
749
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1289,9 +776,9 @@ async function checkCommand(options, cwd) {
|
|
|
1289
776
|
}
|
|
1290
777
|
}
|
|
1291
778
|
}
|
|
1292
|
-
|
|
779
|
+
log5(" done\n");
|
|
1293
780
|
if (!options.files) {
|
|
1294
|
-
|
|
781
|
+
log5(" Checking missing tests...");
|
|
1295
782
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1296
783
|
if (options.staged) {
|
|
1297
784
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1304,14 +791,14 @@ async function checkCommand(options, cwd) {
|
|
|
1304
791
|
} else {
|
|
1305
792
|
violations.push(...testViolations);
|
|
1306
793
|
}
|
|
1307
|
-
|
|
794
|
+
log5(" done\n");
|
|
1308
795
|
}
|
|
1309
796
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1310
|
-
|
|
797
|
+
log5(" Running test coverage...\n");
|
|
1311
798
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1312
799
|
staged: options.staged,
|
|
1313
800
|
enforce: options.enforce,
|
|
1314
|
-
onProgress: (pkg) =>
|
|
801
|
+
onProgress: (pkg) => log5(` Coverage: ${pkg}...
|
|
1315
802
|
`)
|
|
1316
803
|
});
|
|
1317
804
|
violations.push(...coverageViolations);
|
|
@@ -1336,7 +823,7 @@ async function checkCommand(options, cwd) {
|
|
|
1336
823
|
severity
|
|
1337
824
|
});
|
|
1338
825
|
}
|
|
1339
|
-
|
|
826
|
+
log5(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1340
827
|
`);
|
|
1341
828
|
}
|
|
1342
829
|
if (options.format === "json") {
|
|
@@ -1412,7 +899,7 @@ async function hookCheckCommand(cwd) {
|
|
|
1412
899
|
// src/commands/config.ts
|
|
1413
900
|
import * as fs10 from "fs";
|
|
1414
901
|
import * as path9 from "path";
|
|
1415
|
-
import * as
|
|
902
|
+
import * as clack from "@clack/prompts";
|
|
1416
903
|
import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
|
|
1417
904
|
import { scan } from "@viberails/scanner";
|
|
1418
905
|
import chalk6 from "chalk";
|
|
@@ -1732,58 +1219,6 @@ function displayRulesPreview(config) {
|
|
|
1732
1219
|
);
|
|
1733
1220
|
console.log("");
|
|
1734
1221
|
}
|
|
1735
|
-
function displayInitSummary(config, exemptedPackages) {
|
|
1736
|
-
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1737
|
-
const isMonorepo = config.packages.length > 1;
|
|
1738
|
-
const ok = chalk5.green("\u2713");
|
|
1739
|
-
const off = chalk5.dim("\u25CB");
|
|
1740
|
-
console.log("");
|
|
1741
|
-
console.log(` ${chalk5.bold("Rules to apply:")}`);
|
|
1742
|
-
console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1743
|
-
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1744
|
-
if (config.rules.enforceNaming && fileNaming) {
|
|
1745
|
-
console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
|
|
1746
|
-
} else {
|
|
1747
|
-
console.log(` ${off} File naming: ${chalk5.dim("not enforced")}`);
|
|
1748
|
-
}
|
|
1749
|
-
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1750
|
-
if (config.rules.enforceMissingTests && testPattern) {
|
|
1751
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan(`enforced (${testPattern})`)}`);
|
|
1752
|
-
} else if (config.rules.enforceMissingTests) {
|
|
1753
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
|
|
1754
|
-
} else {
|
|
1755
|
-
console.log(` ${off} Missing tests: ${chalk5.dim("not enforced")}`);
|
|
1756
|
-
}
|
|
1757
|
-
if (config.rules.testCoverage > 0) {
|
|
1758
|
-
if (isMonorepo) {
|
|
1759
|
-
const withCoverage = config.packages.filter(
|
|
1760
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1761
|
-
);
|
|
1762
|
-
console.log(
|
|
1763
|
-
` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)} default ${chalk5.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1764
|
-
);
|
|
1765
|
-
} else {
|
|
1766
|
-
console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1767
|
-
}
|
|
1768
|
-
} else {
|
|
1769
|
-
console.log(` ${off} Coverage: ${chalk5.dim("disabled")}`);
|
|
1770
|
-
}
|
|
1771
|
-
if (exemptedPackages.length > 0) {
|
|
1772
|
-
console.log(
|
|
1773
|
-
` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
if (isMonorepo) {
|
|
1777
|
-
console.log(
|
|
1778
|
-
`
|
|
1779
|
-
${chalk5.dim(`${config.packages.length} packages scanned \xB7 warns on violation \xB7 use --enforce in CI`)}`
|
|
1780
|
-
);
|
|
1781
|
-
} else {
|
|
1782
|
-
console.log(`
|
|
1783
|
-
${chalk5.dim("warns on violation \xB7 use --enforce in CI to block")}`);
|
|
1784
|
-
}
|
|
1785
|
-
console.log("");
|
|
1786
|
-
}
|
|
1787
1222
|
|
|
1788
1223
|
// src/display-text.ts
|
|
1789
1224
|
function plainConfidenceLabel(convention) {
|
|
@@ -1902,7 +1337,9 @@ function formatScanResultsText(scanResult) {
|
|
|
1902
1337
|
// src/utils/apply-rule-overrides.ts
|
|
1903
1338
|
function applyRuleOverrides(config, overrides) {
|
|
1904
1339
|
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1340
|
+
const rootPkg = getRootPackage(config.packages);
|
|
1905
1341
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1342
|
+
config.rules.maxTestFileLines = overrides.maxTestFileLines;
|
|
1906
1343
|
config.rules.testCoverage = overrides.testCoverage;
|
|
1907
1344
|
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
1908
1345
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
@@ -1916,7 +1353,6 @@ function applyRuleOverrides(config, overrides) {
|
|
|
1916
1353
|
}
|
|
1917
1354
|
}
|
|
1918
1355
|
if (overrides.fileNamingValue) {
|
|
1919
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1920
1356
|
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
1921
1357
|
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
1922
1358
|
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
@@ -1928,6 +1364,18 @@ function applyRuleOverrides(config, overrides) {
|
|
|
1928
1364
|
}
|
|
1929
1365
|
}
|
|
1930
1366
|
}
|
|
1367
|
+
if (rootPkg) {
|
|
1368
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
1369
|
+
if (overrides.componentNaming !== void 0) {
|
|
1370
|
+
rootPkg.conventions.componentNaming = overrides.componentNaming || void 0;
|
|
1371
|
+
}
|
|
1372
|
+
if (overrides.hookNaming !== void 0) {
|
|
1373
|
+
rootPkg.conventions.hookNaming = overrides.hookNaming || void 0;
|
|
1374
|
+
}
|
|
1375
|
+
if (overrides.importAlias !== void 0) {
|
|
1376
|
+
rootPkg.conventions.importAlias = overrides.importAlias || void 0;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1931
1379
|
}
|
|
1932
1380
|
|
|
1933
1381
|
// src/utils/diff-configs.ts
|
|
@@ -2092,29 +1540,35 @@ async function configCommand(options, cwd) {
|
|
|
2092
1540
|
}
|
|
2093
1541
|
const configPath = path9.join(projectRoot, CONFIG_FILE3);
|
|
2094
1542
|
if (!fs10.existsSync(configPath)) {
|
|
2095
|
-
console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails
|
|
1543
|
+
console.log(`${chalk6.yellow("!")} No config found. Run ${chalk6.cyan("viberails")} first.`);
|
|
2096
1544
|
return;
|
|
2097
1545
|
}
|
|
2098
|
-
|
|
1546
|
+
if (!options.suppressIntro) {
|
|
1547
|
+
clack.intro("viberails config");
|
|
1548
|
+
}
|
|
2099
1549
|
const config = await loadConfig3(configPath);
|
|
2100
1550
|
let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
|
|
2101
|
-
|
|
1551
|
+
clack.note(formatRulesText(config).join("\n"), "Current rules");
|
|
2102
1552
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2103
1553
|
const overrides = await promptRuleMenu({
|
|
2104
1554
|
maxFileLines: config.rules.maxFileLines,
|
|
1555
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2105
1556
|
testCoverage: config.rules.testCoverage,
|
|
2106
1557
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2107
1558
|
enforceNaming: config.rules.enforceNaming,
|
|
2108
1559
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
1560
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
1561
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
1562
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
2109
1563
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
2110
1564
|
coverageCommand: config.defaults?.coverage?.command,
|
|
2111
1565
|
packageOverrides: config.packages
|
|
2112
1566
|
});
|
|
2113
1567
|
applyRuleOverrides(config, overrides);
|
|
2114
1568
|
if (options.rescan && config.packages.length > 1) {
|
|
2115
|
-
const shouldInfer = await
|
|
1569
|
+
const shouldInfer = await confirm("Re-infer boundary rules from import patterns?");
|
|
2116
1570
|
if (shouldInfer) {
|
|
2117
|
-
const bs =
|
|
1571
|
+
const bs = clack.spinner();
|
|
2118
1572
|
bs.start("Building import graph...");
|
|
2119
1573
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2120
1574
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -2130,31 +1584,31 @@ async function configCommand(options, cwd) {
|
|
|
2130
1584
|
}
|
|
2131
1585
|
}
|
|
2132
1586
|
}
|
|
2133
|
-
const shouldWrite = await
|
|
1587
|
+
const shouldWrite = await confirm("Save updated configuration?");
|
|
2134
1588
|
if (!shouldWrite) {
|
|
2135
|
-
|
|
1589
|
+
clack.outro("No changes written.");
|
|
2136
1590
|
return;
|
|
2137
1591
|
}
|
|
2138
1592
|
const compacted = compactConfig2(config);
|
|
2139
1593
|
fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2140
1594
|
`);
|
|
2141
1595
|
if (!scanResult) {
|
|
2142
|
-
const s =
|
|
1596
|
+
const s = clack.spinner();
|
|
2143
1597
|
s.start("Scanning for context generation...");
|
|
2144
1598
|
scanResult = await scan(projectRoot);
|
|
2145
1599
|
s.stop("Scan complete");
|
|
2146
1600
|
}
|
|
2147
1601
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2148
|
-
|
|
1602
|
+
clack.log.success(
|
|
2149
1603
|
`Updated:
|
|
2150
1604
|
${CONFIG_FILE3}
|
|
2151
1605
|
.viberails/context.md
|
|
2152
1606
|
.viberails/scan-result.json`
|
|
2153
1607
|
);
|
|
2154
|
-
|
|
1608
|
+
clack.outro("Done! Run viberails check to verify.");
|
|
2155
1609
|
}
|
|
2156
1610
|
async function rescanAndMerge(projectRoot, config) {
|
|
2157
|
-
const s =
|
|
1611
|
+
const s = clack.spinner();
|
|
2158
1612
|
s.start("Re-scanning project...");
|
|
2159
1613
|
const scanResult = await scan(projectRoot);
|
|
2160
1614
|
const merged = mergeConfig(config, scanResult);
|
|
@@ -2165,9 +1619,9 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
2165
1619
|
const icon = c.type === "removed" ? "-" : "+";
|
|
2166
1620
|
return `${icon} ${c.description}`;
|
|
2167
1621
|
}).join("\n");
|
|
2168
|
-
|
|
1622
|
+
clack.note(changeLines, "Changes detected");
|
|
2169
1623
|
} else {
|
|
2170
|
-
|
|
1624
|
+
clack.log.info("No new changes detected from scan.");
|
|
2171
1625
|
}
|
|
2172
1626
|
Object.assign(config, merged);
|
|
2173
1627
|
return scanResult;
|
|
@@ -2486,10 +1940,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2486
1940
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2487
1941
|
const testPattern = pkg?.structure?.testPattern;
|
|
2488
1942
|
if (!testPattern) return null;
|
|
2489
|
-
const
|
|
2490
|
-
const ext = path12.extname(
|
|
1943
|
+
const basename10 = path12.basename(sourceRelPath);
|
|
1944
|
+
const ext = path12.extname(basename10);
|
|
2491
1945
|
if (!ext) return null;
|
|
2492
|
-
const stem =
|
|
1946
|
+
const stem = basename10.slice(0, -ext.length);
|
|
2493
1947
|
const testSuffix = testPattern.replace("*", "");
|
|
2494
1948
|
const testFilename = `${stem}${testSuffix}`;
|
|
2495
1949
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2661,19 +2115,159 @@ ${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
|
2661
2115
|
}
|
|
2662
2116
|
|
|
2663
2117
|
// src/commands/init.ts
|
|
2664
|
-
import * as
|
|
2665
|
-
import * as
|
|
2666
|
-
import * as
|
|
2667
|
-
import { compactConfig as
|
|
2668
|
-
import { scan as
|
|
2669
|
-
import
|
|
2118
|
+
import * as fs20 from "fs";
|
|
2119
|
+
import * as path20 from "path";
|
|
2120
|
+
import * as clack4 from "@clack/prompts";
|
|
2121
|
+
import { compactConfig as compactConfig4, generateConfig as generateConfig2 } from "@viberails/config";
|
|
2122
|
+
import { scan as scan3 } from "@viberails/scanner";
|
|
2123
|
+
import chalk14 from "chalk";
|
|
2124
|
+
|
|
2125
|
+
// src/display-init.ts
|
|
2126
|
+
import { FRAMEWORK_NAMES as FRAMEWORK_NAMES5, STYLING_NAMES as STYLING_NAMES5 } from "@viberails/types";
|
|
2127
|
+
import chalk9 from "chalk";
|
|
2128
|
+
var INIT_OVERVIEW_NAMES = {
|
|
2129
|
+
typescript: "TypeScript",
|
|
2130
|
+
javascript: "JavaScript",
|
|
2131
|
+
eslint: "ESLint",
|
|
2132
|
+
prettier: "Prettier",
|
|
2133
|
+
jest: "Jest",
|
|
2134
|
+
vitest: "Vitest",
|
|
2135
|
+
biome: "Biome"
|
|
2136
|
+
};
|
|
2137
|
+
function formatDetectedOverview(scanResult) {
|
|
2138
|
+
const { stack } = scanResult;
|
|
2139
|
+
const primaryParts = [];
|
|
2140
|
+
const secondaryParts = [];
|
|
2141
|
+
const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
|
|
2142
|
+
if (scanResult.packages.length > 1) {
|
|
2143
|
+
primaryParts.push("monorepo");
|
|
2144
|
+
primaryParts.push(`${scanResult.packages.length} packages`);
|
|
2145
|
+
} else if (stack.framework) {
|
|
2146
|
+
primaryParts.push(formatItem(stack.framework, FRAMEWORK_NAMES5));
|
|
2147
|
+
} else {
|
|
2148
|
+
primaryParts.push("single package");
|
|
2149
|
+
}
|
|
2150
|
+
primaryParts.push(formatOverviewItem(stack.language));
|
|
2151
|
+
if (stack.styling) {
|
|
2152
|
+
primaryParts.push(formatOverviewItem(stack.styling, STYLING_NAMES5));
|
|
2153
|
+
}
|
|
2154
|
+
if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
|
|
2155
|
+
if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
|
|
2156
|
+
if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
|
|
2157
|
+
if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
|
|
2158
|
+
const primary = primaryParts.map((part) => chalk9.cyan(part)).join(chalk9.dim(" \xB7 "));
|
|
2159
|
+
const secondary = secondaryParts.join(chalk9.dim(" \xB7 "));
|
|
2160
|
+
return secondary ? `${primary}
|
|
2161
|
+
${chalk9.dim(secondary)}` : primary;
|
|
2162
|
+
}
|
|
2163
|
+
function displayInitOverview(scanResult, config, exemptedPackages) {
|
|
2164
|
+
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2165
|
+
const isMonorepo = config.packages.length > 1;
|
|
2166
|
+
const ok = chalk9.green("\u2713");
|
|
2167
|
+
const info = chalk9.yellow("~");
|
|
2168
|
+
console.log("");
|
|
2169
|
+
console.log(` ${chalk9.bold("Ready to initialize:")}`);
|
|
2170
|
+
console.log(` ${formatDetectedOverview(scanResult)}`);
|
|
2171
|
+
console.log("");
|
|
2172
|
+
console.log(` ${chalk9.bold("Rules to apply:")}`);
|
|
2173
|
+
console.log(` ${ok} Max file size: ${chalk9.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
2174
|
+
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
2175
|
+
if (config.rules.enforceNaming && fileNaming) {
|
|
2176
|
+
console.log(` ${ok} File naming: ${chalk9.cyan(fileNaming)}`);
|
|
2177
|
+
} else {
|
|
2178
|
+
console.log(` ${info} File naming: ${chalk9.dim("not enforced")}`);
|
|
2179
|
+
}
|
|
2180
|
+
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
2181
|
+
if (config.rules.enforceMissingTests && testPattern) {
|
|
2182
|
+
console.log(` ${ok} Missing tests: ${chalk9.cyan(`enforced (${testPattern})`)}`);
|
|
2183
|
+
} else if (config.rules.enforceMissingTests) {
|
|
2184
|
+
console.log(` ${ok} Missing tests: ${chalk9.cyan("enforced")}`);
|
|
2185
|
+
} else {
|
|
2186
|
+
console.log(` ${info} Missing tests: ${chalk9.dim("not enforced")}`);
|
|
2187
|
+
}
|
|
2188
|
+
if (config.rules.testCoverage > 0) {
|
|
2189
|
+
if (isMonorepo) {
|
|
2190
|
+
const withCoverage = config.packages.filter(
|
|
2191
|
+
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
2192
|
+
);
|
|
2193
|
+
console.log(
|
|
2194
|
+
` ${ok} Coverage: ${chalk9.cyan(`${config.rules.testCoverage}%`)} default ${chalk9.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
2195
|
+
);
|
|
2196
|
+
} else {
|
|
2197
|
+
console.log(` ${ok} Coverage: ${chalk9.cyan(`${config.rules.testCoverage}%`)}`);
|
|
2198
|
+
}
|
|
2199
|
+
} else {
|
|
2200
|
+
console.log(` ${info} Coverage: ${chalk9.dim("disabled")}`);
|
|
2201
|
+
}
|
|
2202
|
+
if (exemptedPackages.length > 0) {
|
|
2203
|
+
console.log(
|
|
2204
|
+
` ${chalk9.dim(" exempted:")} ${chalk9.dim(exemptedPackages.join(", "))} ${chalk9.dim("(types-only)")}`
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
console.log("");
|
|
2208
|
+
console.log(` ${chalk9.bold("Also available:")}`);
|
|
2209
|
+
if (isMonorepo) {
|
|
2210
|
+
console.log(` ${info} Infer boundaries from current imports`);
|
|
2211
|
+
}
|
|
2212
|
+
console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
|
|
2213
|
+
console.log(
|
|
2214
|
+
`
|
|
2215
|
+
${chalk9.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
|
|
2216
|
+
);
|
|
2217
|
+
console.log("");
|
|
2218
|
+
}
|
|
2219
|
+
function summarizeSelectedIntegrations(integrations, opts) {
|
|
2220
|
+
const lines = [];
|
|
2221
|
+
if (opts.hasBoundaries) {
|
|
2222
|
+
lines.push("\u2713 Boundary rules: inferred from current imports");
|
|
2223
|
+
} else {
|
|
2224
|
+
lines.push("~ Boundary rules: not enabled");
|
|
2225
|
+
}
|
|
2226
|
+
if (opts.hasCoverage) {
|
|
2227
|
+
lines.push("\u2713 Coverage checks: enabled");
|
|
2228
|
+
} else {
|
|
2229
|
+
lines.push("~ Coverage checks: disabled");
|
|
2230
|
+
}
|
|
2231
|
+
const selectedIntegrations = [
|
|
2232
|
+
integrations.preCommitHook ? "pre-commit hook" : void 0,
|
|
2233
|
+
integrations.typecheckHook ? "typecheck" : void 0,
|
|
2234
|
+
integrations.lintHook ? "lint check" : void 0,
|
|
2235
|
+
integrations.claudeCodeHook ? "Claude Code hook" : void 0,
|
|
2236
|
+
integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
|
|
2237
|
+
integrations.githubAction ? "GitHub Actions workflow" : void 0
|
|
2238
|
+
].filter(Boolean);
|
|
2239
|
+
if (selectedIntegrations.length > 0) {
|
|
2240
|
+
lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
|
|
2241
|
+
} else {
|
|
2242
|
+
lines.push("~ Integrations: none selected");
|
|
2243
|
+
}
|
|
2244
|
+
return lines;
|
|
2245
|
+
}
|
|
2246
|
+
function displaySetupPlan(config, integrations, opts = {}) {
|
|
2247
|
+
const configFile = opts.configFile ?? "viberails.config.json";
|
|
2248
|
+
const lines = summarizeSelectedIntegrations(integrations, {
|
|
2249
|
+
hasBoundaries: config.rules.enforceBoundaries,
|
|
2250
|
+
hasCoverage: config.rules.testCoverage > 0
|
|
2251
|
+
});
|
|
2252
|
+
console.log("");
|
|
2253
|
+
console.log(` ${chalk9.bold("Ready to write:")}`);
|
|
2254
|
+
console.log(
|
|
2255
|
+
` ${opts.replacingExistingConfig ? chalk9.yellow("!") : chalk9.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? chalk9.dim(" (replacing existing config)") : ""}`
|
|
2256
|
+
);
|
|
2257
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/context.md`);
|
|
2258
|
+
console.log(` ${chalk9.green("\u2713")} .viberails/scan-result.json`);
|
|
2259
|
+
for (const line of lines) {
|
|
2260
|
+
const icon = line.startsWith("\u2713") ? chalk9.green("\u2713") : chalk9.yellow("~");
|
|
2261
|
+
console.log(` ${icon} ${line.slice(2)}`);
|
|
2262
|
+
}
|
|
2263
|
+
console.log("");
|
|
2264
|
+
}
|
|
2670
2265
|
|
|
2671
2266
|
// src/utils/check-prerequisites.ts
|
|
2672
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
2673
2267
|
import * as fs14 from "fs";
|
|
2674
2268
|
import * as path14 from "path";
|
|
2675
|
-
import * as
|
|
2676
|
-
import
|
|
2269
|
+
import * as clack2 from "@clack/prompts";
|
|
2270
|
+
import chalk10 from "chalk";
|
|
2677
2271
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2678
2272
|
const pm = scanResult.stack.packageManager.name;
|
|
2679
2273
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -2704,9 +2298,9 @@ function displayMissingPrereqs(prereqs) {
|
|
|
2704
2298
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2705
2299
|
for (const m of missing) {
|
|
2706
2300
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2707
|
-
console.log(` ${
|
|
2301
|
+
console.log(` ${chalk10.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2708
2302
|
if (m.installCommand) {
|
|
2709
|
-
console.log(` Install: ${
|
|
2303
|
+
console.log(` Install: ${chalk10.cyan(m.installCommand)}`);
|
|
2710
2304
|
}
|
|
2711
2305
|
}
|
|
2712
2306
|
}
|
|
@@ -2718,24 +2312,24 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2718
2312
|
const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
|
|
2719
2313
|
return `\u2717 ${p.label} \u2014 ${detail}`;
|
|
2720
2314
|
}).join("\n");
|
|
2721
|
-
|
|
2315
|
+
clack2.note(prereqLines, "Coverage support");
|
|
2722
2316
|
let disableCoverage = false;
|
|
2723
2317
|
for (const m of missing) {
|
|
2724
2318
|
if (!m.installCommand) continue;
|
|
2725
2319
|
const pkgCount = m.affectedPackages?.length;
|
|
2726
2320
|
const message = pkgCount ? `${m.label} is not installed. Required for coverage in ${pkgCount} packages using vitest.` : `${m.label} is not installed. It is required for coverage percentage checks.`;
|
|
2727
|
-
const choice = await
|
|
2321
|
+
const choice = await clack2.select({
|
|
2728
2322
|
message,
|
|
2729
2323
|
options: [
|
|
2730
2324
|
{
|
|
2731
2325
|
value: "install",
|
|
2732
|
-
label:
|
|
2326
|
+
label: "Install now",
|
|
2733
2327
|
hint: m.installCommand
|
|
2734
2328
|
},
|
|
2735
2329
|
{
|
|
2736
2330
|
value: "disable",
|
|
2737
|
-
label: "
|
|
2738
|
-
hint: "missing-test checks still active"
|
|
2331
|
+
label: "Disable coverage checks",
|
|
2332
|
+
hint: "missing-test checks still stay active"
|
|
2739
2333
|
},
|
|
2740
2334
|
{
|
|
2741
2335
|
value: "skip",
|
|
@@ -2746,28 +2340,23 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2746
2340
|
});
|
|
2747
2341
|
assertNotCancelled(choice);
|
|
2748
2342
|
if (choice === "install") {
|
|
2749
|
-
const is =
|
|
2343
|
+
const is = clack2.spinner();
|
|
2750
2344
|
is.start(`Installing ${m.label}...`);
|
|
2751
|
-
const result =
|
|
2752
|
-
cwd: projectRoot,
|
|
2753
|
-
shell: true,
|
|
2754
|
-
encoding: "utf-8",
|
|
2755
|
-
stdio: "pipe"
|
|
2756
|
-
});
|
|
2345
|
+
const result = await spawnAsync(m.installCommand, projectRoot);
|
|
2757
2346
|
if (result.status === 0) {
|
|
2758
2347
|
is.stop(`Installed ${m.label}`);
|
|
2759
2348
|
} else {
|
|
2760
2349
|
is.stop(`Failed to install ${m.label}`);
|
|
2761
|
-
|
|
2350
|
+
clack2.log.warn(
|
|
2762
2351
|
`Install manually: ${m.installCommand}
|
|
2763
2352
|
Coverage percentage checks will not work until the dependency is installed.`
|
|
2764
2353
|
);
|
|
2765
2354
|
}
|
|
2766
2355
|
} else if (choice === "disable") {
|
|
2767
2356
|
disableCoverage = true;
|
|
2768
|
-
|
|
2357
|
+
clack2.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
|
|
2769
2358
|
} else {
|
|
2770
|
-
|
|
2359
|
+
clack2.log.info(
|
|
2771
2360
|
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2772
2361
|
Install later: ${m.installCommand}`
|
|
2773
2362
|
);
|
|
@@ -2785,20 +2374,6 @@ function hasDependency(projectRoot, name) {
|
|
|
2785
2374
|
}
|
|
2786
2375
|
}
|
|
2787
2376
|
|
|
2788
|
-
// src/utils/filter-confidence.ts
|
|
2789
|
-
function filterHighConfidence(conventions, meta) {
|
|
2790
|
-
if (!meta) return conventions;
|
|
2791
|
-
const filtered = {};
|
|
2792
|
-
for (const [key, value] of Object.entries(conventions)) {
|
|
2793
|
-
if (value === void 0) continue;
|
|
2794
|
-
const convMeta = meta[key];
|
|
2795
|
-
if (!convMeta || convMeta.confidence === "high") {
|
|
2796
|
-
filtered[key] = value;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
return filtered;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
2377
|
// src/utils/update-gitignore.ts
|
|
2803
2378
|
import * as fs15 from "fs";
|
|
2804
2379
|
import * as path15 from "path";
|
|
@@ -2819,7 +2394,7 @@ function updateGitignore(projectRoot) {
|
|
|
2819
2394
|
// src/commands/init-hooks.ts
|
|
2820
2395
|
import * as fs17 from "fs";
|
|
2821
2396
|
import * as path17 from "path";
|
|
2822
|
-
import
|
|
2397
|
+
import chalk11 from "chalk";
|
|
2823
2398
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2824
2399
|
|
|
2825
2400
|
// src/commands/resolve-typecheck.ts
|
|
@@ -2864,13 +2439,13 @@ function setupPreCommitHook(projectRoot) {
|
|
|
2864
2439
|
const lefthookPath = path17.join(projectRoot, "lefthook.yml");
|
|
2865
2440
|
if (fs17.existsSync(lefthookPath)) {
|
|
2866
2441
|
addLefthookPreCommit(lefthookPath);
|
|
2867
|
-
console.log(` ${
|
|
2442
|
+
console.log(` ${chalk11.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2868
2443
|
return "lefthook.yml";
|
|
2869
2444
|
}
|
|
2870
2445
|
const huskyDir = path17.join(projectRoot, ".husky");
|
|
2871
2446
|
if (fs17.existsSync(huskyDir)) {
|
|
2872
2447
|
writeHuskyPreCommit(huskyDir);
|
|
2873
|
-
console.log(` ${
|
|
2448
|
+
console.log(` ${chalk11.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2874
2449
|
return ".husky/pre-commit";
|
|
2875
2450
|
}
|
|
2876
2451
|
const gitDir = path17.join(projectRoot, ".git");
|
|
@@ -2880,7 +2455,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
2880
2455
|
fs17.mkdirSync(hooksDir, { recursive: true });
|
|
2881
2456
|
}
|
|
2882
2457
|
writeGitHookPreCommit(hooksDir);
|
|
2883
|
-
console.log(` ${
|
|
2458
|
+
console.log(` ${chalk11.green("\u2713")} .git/hooks/pre-commit`);
|
|
2884
2459
|
return ".git/hooks/pre-commit";
|
|
2885
2460
|
}
|
|
2886
2461
|
return void 0;
|
|
@@ -2941,9 +2516,9 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2941
2516
|
settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
|
|
2942
2517
|
} catch {
|
|
2943
2518
|
console.warn(
|
|
2944
|
-
` ${
|
|
2519
|
+
` ${chalk11.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
2945
2520
|
);
|
|
2946
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2521
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk11.cyan("viberails init --force")}`);
|
|
2947
2522
|
return;
|
|
2948
2523
|
}
|
|
2949
2524
|
}
|
|
@@ -2966,7 +2541,7 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2966
2541
|
settings.hooks = hooks;
|
|
2967
2542
|
fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2968
2543
|
`);
|
|
2969
|
-
console.log(` ${
|
|
2544
|
+
console.log(` ${chalk11.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
2970
2545
|
}
|
|
2971
2546
|
function setupClaudeMdReference(projectRoot) {
|
|
2972
2547
|
const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
|
|
@@ -2978,7 +2553,7 @@ function setupClaudeMdReference(projectRoot) {
|
|
|
2978
2553
|
const ref = "\n@.viberails/context.md\n";
|
|
2979
2554
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
2980
2555
|
fs17.writeFileSync(claudeMdPath, prefix + ref);
|
|
2981
|
-
console.log(` ${
|
|
2556
|
+
console.log(` ${chalk11.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2982
2557
|
}
|
|
2983
2558
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
2984
2559
|
const workflowDir = path17.join(projectRoot, ".github", "workflows");
|
|
@@ -3064,7 +2639,7 @@ ${cmd}
|
|
|
3064
2639
|
// src/commands/init-hooks-extra.ts
|
|
3065
2640
|
import * as fs18 from "fs";
|
|
3066
2641
|
import * as path18 from "path";
|
|
3067
|
-
import
|
|
2642
|
+
import chalk12 from "chalk";
|
|
3068
2643
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
3069
2644
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3070
2645
|
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
@@ -3124,12 +2699,12 @@ ${command}
|
|
|
3124
2699
|
function setupTypecheckHook(projectRoot, packageManager) {
|
|
3125
2700
|
const resolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3126
2701
|
if (!resolved.command) {
|
|
3127
|
-
console.log(` ${
|
|
2702
|
+
console.log(` ${chalk12.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
|
|
3128
2703
|
return void 0;
|
|
3129
2704
|
}
|
|
3130
2705
|
const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
|
|
3131
2706
|
if (target) {
|
|
3132
|
-
console.log(` ${
|
|
2707
|
+
console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
|
|
3133
2708
|
}
|
|
3134
2709
|
return target;
|
|
3135
2710
|
}
|
|
@@ -3150,7 +2725,7 @@ function setupLintHook(projectRoot, linter) {
|
|
|
3150
2725
|
}
|
|
3151
2726
|
const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
|
|
3152
2727
|
if (target) {
|
|
3153
|
-
console.log(` ${
|
|
2728
|
+
console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
3154
2729
|
}
|
|
3155
2730
|
return target;
|
|
3156
2731
|
}
|
|
@@ -3186,31 +2761,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3186
2761
|
return created;
|
|
3187
2762
|
}
|
|
3188
2763
|
|
|
3189
|
-
// src/commands/init.ts
|
|
3190
|
-
|
|
2764
|
+
// src/commands/init-non-interactive.ts
|
|
2765
|
+
import * as fs19 from "fs";
|
|
2766
|
+
import * as path19 from "path";
|
|
2767
|
+
import * as clack3 from "@clack/prompts";
|
|
2768
|
+
import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
|
|
2769
|
+
import { scan as scan2 } from "@viberails/scanner";
|
|
2770
|
+
import chalk13 from "chalk";
|
|
2771
|
+
|
|
2772
|
+
// src/utils/filter-confidence.ts
|
|
2773
|
+
function filterHighConfidence(conventions, meta) {
|
|
2774
|
+
if (!meta) return conventions;
|
|
2775
|
+
const filtered = {};
|
|
2776
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
2777
|
+
if (value === void 0) continue;
|
|
2778
|
+
const convMeta = meta[key];
|
|
2779
|
+
if (!convMeta || convMeta.confidence === "high") {
|
|
2780
|
+
filtered[key] = value;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return filtered;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/commands/init-non-interactive.ts
|
|
3191
2787
|
function getExemptedPackages(config) {
|
|
3192
2788
|
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3193
2789
|
}
|
|
3194
|
-
async function initCommand(options, cwd) {
|
|
3195
|
-
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
3196
|
-
if (!projectRoot) {
|
|
3197
|
-
throw new Error(
|
|
3198
|
-
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
3199
|
-
);
|
|
3200
|
-
}
|
|
3201
|
-
const configPath = path19.join(projectRoot, CONFIG_FILE5);
|
|
3202
|
-
if (fs19.existsSync(configPath) && !options.force) {
|
|
3203
|
-
console.log(
|
|
3204
|
-
`${chalk12.yellow("!")} viberails is already initialized.
|
|
3205
|
-
Run ${chalk12.cyan("viberails config")} to edit rules, ${chalk12.cyan("viberails sync")} to update, or ${chalk12.cyan("viberails init --force")} to start fresh.`
|
|
3206
|
-
);
|
|
3207
|
-
return;
|
|
3208
|
-
}
|
|
3209
|
-
if (options.yes) return initNonInteractive(projectRoot, configPath);
|
|
3210
|
-
await initInteractive(projectRoot, configPath, options);
|
|
3211
|
-
}
|
|
3212
2790
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3213
|
-
const s =
|
|
2791
|
+
const s = clack3.spinner();
|
|
3214
2792
|
s.start("Scanning project...");
|
|
3215
2793
|
const scanResult = await scan2(projectRoot);
|
|
3216
2794
|
const config = generateConfig(scanResult);
|
|
@@ -3225,11 +2803,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3225
2803
|
const exempted = getExemptedPackages(config);
|
|
3226
2804
|
if (exempted.length > 0) {
|
|
3227
2805
|
console.log(
|
|
3228
|
-
` ${
|
|
2806
|
+
` ${chalk13.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk13.dim("(types-only)")}`
|
|
3229
2807
|
);
|
|
3230
2808
|
}
|
|
3231
2809
|
if (config.packages.length > 1) {
|
|
3232
|
-
const bs =
|
|
2810
|
+
const bs = clack3.spinner();
|
|
3233
2811
|
bs.start("Building import graph...");
|
|
3234
2812
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3235
2813
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3262,14 +2840,14 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3262
2840
|
const hookManager = detectHookManager(projectRoot);
|
|
3263
2841
|
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
3264
2842
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3265
|
-
const ok =
|
|
2843
|
+
const ok = chalk13.green("\u2713");
|
|
3266
2844
|
const created = [
|
|
3267
2845
|
`${ok} ${path19.basename(configPath)}`,
|
|
3268
2846
|
`${ok} .viberails/context.md`,
|
|
3269
2847
|
`${ok} .viberails/scan-result.json`,
|
|
3270
2848
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
3271
2849
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
3272
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
2850
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk13.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
3273
2851
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
3274
2852
|
].filter(Boolean);
|
|
3275
2853
|
if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
|
|
@@ -3278,39 +2856,93 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3278
2856
|
Created:
|
|
3279
2857
|
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
3280
2858
|
}
|
|
2859
|
+
|
|
2860
|
+
// src/commands/init.ts
|
|
2861
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
2862
|
+
function getExemptedPackages2(config) {
|
|
2863
|
+
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
2864
|
+
}
|
|
2865
|
+
async function initCommand(options, cwd) {
|
|
2866
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
2867
|
+
if (!projectRoot) {
|
|
2868
|
+
throw new Error(
|
|
2869
|
+
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
const configPath = path20.join(projectRoot, CONFIG_FILE5);
|
|
2873
|
+
if (fs20.existsSync(configPath) && !options.force) {
|
|
2874
|
+
if (!options.yes) {
|
|
2875
|
+
return initInteractive(projectRoot, configPath, options);
|
|
2876
|
+
}
|
|
2877
|
+
console.log(
|
|
2878
|
+
`${chalk14.yellow("!")} viberails is already initialized.
|
|
2879
|
+
Run ${chalk14.cyan("viberails")} to review or edit the existing setup, ${chalk14.cyan("viberails sync")} to update generated files, or ${chalk14.cyan("viberails init --force")} to replace it.`
|
|
2880
|
+
);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (options.yes) return initNonInteractive(projectRoot, configPath);
|
|
2884
|
+
await initInteractive(projectRoot, configPath, options);
|
|
2885
|
+
}
|
|
3281
2886
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3282
|
-
|
|
3283
|
-
|
|
2887
|
+
clack4.intro("viberails");
|
|
2888
|
+
const replacingExistingConfig = fs20.existsSync(configPath);
|
|
2889
|
+
if (fs20.existsSync(configPath) && !options.force) {
|
|
2890
|
+
const action = await promptExistingConfigAction(path20.basename(configPath));
|
|
2891
|
+
if (action === "cancel") {
|
|
2892
|
+
clack4.outro("Aborted. No files were written.");
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (action === "edit") {
|
|
2896
|
+
await configCommand({ suppressIntro: true }, projectRoot);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
options.force = true;
|
|
2900
|
+
}
|
|
2901
|
+
if (fs20.existsSync(configPath) && options.force) {
|
|
3284
2902
|
const replace = await confirmDangerous(
|
|
3285
|
-
`${
|
|
2903
|
+
`${path20.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3286
2904
|
);
|
|
3287
2905
|
if (!replace) {
|
|
3288
|
-
|
|
2906
|
+
clack4.outro("Aborted. No files were written.");
|
|
3289
2907
|
return;
|
|
3290
2908
|
}
|
|
3291
2909
|
}
|
|
3292
|
-
const s =
|
|
2910
|
+
const s = clack4.spinner();
|
|
3293
2911
|
s.start("Scanning project...");
|
|
3294
|
-
const scanResult = await
|
|
3295
|
-
const config =
|
|
2912
|
+
const scanResult = await scan3(projectRoot);
|
|
2913
|
+
const config = generateConfig2(scanResult);
|
|
3296
2914
|
s.stop("Scan complete");
|
|
3297
2915
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3298
|
-
|
|
2916
|
+
clack4.log.warn(
|
|
3299
2917
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3300
2918
|
);
|
|
3301
2919
|
}
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
2920
|
+
const exemptedPkgs = getExemptedPackages2(config);
|
|
2921
|
+
let decision;
|
|
2922
|
+
while (true) {
|
|
2923
|
+
displayInitOverview(scanResult, config, exemptedPkgs);
|
|
2924
|
+
const nextDecision = await promptInitDecision();
|
|
2925
|
+
if (nextDecision === "review") {
|
|
2926
|
+
clack4.note(formatScanResultsText(scanResult), "Detected details");
|
|
2927
|
+
continue;
|
|
2928
|
+
}
|
|
2929
|
+
decision = nextDecision;
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
3306
2932
|
if (decision === "customize") {
|
|
2933
|
+
const { resolveNamingDefault } = await import("./prompt-naming-default-AH54HEBC.js");
|
|
2934
|
+
await resolveNamingDefault(config, scanResult);
|
|
3307
2935
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3308
2936
|
const overrides = await promptRuleMenu({
|
|
3309
2937
|
maxFileLines: config.rules.maxFileLines,
|
|
2938
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3310
2939
|
testCoverage: config.rules.testCoverage,
|
|
3311
2940
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3312
2941
|
enforceNaming: config.rules.enforceNaming,
|
|
3313
2942
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2943
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2944
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2945
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3314
2946
|
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
3315
2947
|
coverageCommand: config.defaults?.coverage?.command,
|
|
3316
2948
|
packageOverrides: config.packages
|
|
@@ -3318,13 +2950,13 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3318
2950
|
applyRuleOverrides(config, overrides);
|
|
3319
2951
|
}
|
|
3320
2952
|
if (config.packages.length > 1) {
|
|
3321
|
-
|
|
3322
|
-
"
|
|
2953
|
+
clack4.note(
|
|
2954
|
+
"Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
|
|
3323
2955
|
"Boundaries"
|
|
3324
2956
|
);
|
|
3325
|
-
const shouldInfer = await
|
|
2957
|
+
const shouldInfer = await confirm("Infer boundary rules from current import patterns?");
|
|
3326
2958
|
if (shouldInfer) {
|
|
3327
|
-
const bs =
|
|
2959
|
+
const bs = clack4.spinner();
|
|
3328
2960
|
bs.start("Building import graph...");
|
|
3329
2961
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3330
2962
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3345,7 +2977,7 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3345
2977
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3346
2978
|
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3347
2979
|
if (hasMissingPrereqs) {
|
|
3348
|
-
|
|
2980
|
+
clack4.log.info("Some dependencies are needed for full functionality.");
|
|
3349
2981
|
}
|
|
3350
2982
|
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3351
2983
|
if (prereqResult.disableCoverage) {
|
|
@@ -3358,46 +2990,50 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3358
2990
|
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3359
2991
|
isWorkspace: config.packages.length > 1
|
|
3360
2992
|
});
|
|
3361
|
-
|
|
2993
|
+
displaySetupPlan(config, integrations, {
|
|
2994
|
+
replacingExistingConfig,
|
|
2995
|
+
configFile: path20.basename(configPath)
|
|
2996
|
+
});
|
|
2997
|
+
const shouldWrite = await confirm("Apply this setup?");
|
|
3362
2998
|
if (!shouldWrite) {
|
|
3363
|
-
|
|
2999
|
+
clack4.outro("Aborted. No files were written.");
|
|
3364
3000
|
return;
|
|
3365
3001
|
}
|
|
3366
|
-
const ws =
|
|
3002
|
+
const ws = clack4.spinner();
|
|
3367
3003
|
ws.start("Writing configuration...");
|
|
3368
|
-
const compacted =
|
|
3369
|
-
|
|
3004
|
+
const compacted = compactConfig4(config);
|
|
3005
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3370
3006
|
`);
|
|
3371
3007
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3372
3008
|
updateGitignore(projectRoot);
|
|
3373
3009
|
ws.stop("Configuration written");
|
|
3374
|
-
const ok =
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3010
|
+
const ok = chalk14.green("\u2713");
|
|
3011
|
+
clack4.log.step(`${ok} ${path20.basename(configPath)}`);
|
|
3012
|
+
clack4.log.step(`${ok} .viberails/context.md`);
|
|
3013
|
+
clack4.log.step(`${ok} .viberails/scan-result.json`);
|
|
3378
3014
|
setupSelectedIntegrations(projectRoot, integrations, {
|
|
3379
3015
|
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3380
3016
|
packageManager: rootPkgStack?.packageManager?.split("@")[0]
|
|
3381
3017
|
});
|
|
3382
|
-
|
|
3018
|
+
clack4.outro(
|
|
3383
3019
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3384
|
-
${
|
|
3020
|
+
${chalk14.dim("Tip: use")} ${chalk14.cyan("viberails check --enforce")} ${chalk14.dim("in CI to block PRs on violations.")}`
|
|
3385
3021
|
);
|
|
3386
3022
|
}
|
|
3387
3023
|
|
|
3388
3024
|
// src/commands/sync.ts
|
|
3389
|
-
import * as
|
|
3390
|
-
import * as
|
|
3391
|
-
import * as
|
|
3392
|
-
import { compactConfig as
|
|
3393
|
-
import { scan as
|
|
3394
|
-
import
|
|
3025
|
+
import * as fs21 from "fs";
|
|
3026
|
+
import * as path21 from "path";
|
|
3027
|
+
import * as clack5 from "@clack/prompts";
|
|
3028
|
+
import { compactConfig as compactConfig5, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
|
|
3029
|
+
import { scan as scan4 } from "@viberails/scanner";
|
|
3030
|
+
import chalk15 from "chalk";
|
|
3395
3031
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3396
3032
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3397
3033
|
function loadPreviousStats(projectRoot) {
|
|
3398
|
-
const scanResultPath =
|
|
3034
|
+
const scanResultPath = path21.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3399
3035
|
try {
|
|
3400
|
-
const raw =
|
|
3036
|
+
const raw = fs21.readFileSync(scanResultPath, "utf-8");
|
|
3401
3037
|
const parsed = JSON.parse(raw);
|
|
3402
3038
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3403
3039
|
return parsed.statistics;
|
|
@@ -3414,17 +3050,17 @@ async function syncCommand(options, cwd) {
|
|
|
3414
3050
|
"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"
|
|
3415
3051
|
);
|
|
3416
3052
|
}
|
|
3417
|
-
const configPath =
|
|
3053
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE6);
|
|
3418
3054
|
const existing = await loadConfig5(configPath);
|
|
3419
3055
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3420
|
-
const s =
|
|
3056
|
+
const s = clack5.spinner();
|
|
3421
3057
|
s.start("Scanning project...");
|
|
3422
|
-
const scanResult = await
|
|
3058
|
+
const scanResult = await scan4(projectRoot);
|
|
3423
3059
|
s.stop("Scan complete");
|
|
3424
3060
|
const merged = mergeConfig2(existing, scanResult);
|
|
3425
|
-
const compacted =
|
|
3061
|
+
const compacted = compactConfig5(merged);
|
|
3426
3062
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3427
|
-
const rawDisk =
|
|
3063
|
+
const rawDisk = fs21.readFileSync(configPath, "utf-8").trim();
|
|
3428
3064
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3429
3065
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3430
3066
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3432,19 +3068,19 @@ async function syncCommand(options, cwd) {
|
|
|
3432
3068
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3433
3069
|
if (changes.length > 0 || statsDelta) {
|
|
3434
3070
|
console.log(`
|
|
3435
|
-
${
|
|
3071
|
+
${chalk15.bold("Changes:")}`);
|
|
3436
3072
|
for (const change of changes) {
|
|
3437
|
-
const icon = change.type === "removed" ?
|
|
3073
|
+
const icon = change.type === "removed" ? chalk15.red("-") : chalk15.green("+");
|
|
3438
3074
|
console.log(` ${icon} ${change.description}`);
|
|
3439
3075
|
}
|
|
3440
3076
|
if (statsDelta) {
|
|
3441
|
-
console.log(` ${
|
|
3077
|
+
console.log(` ${chalk15.dim(statsDelta)}`);
|
|
3442
3078
|
}
|
|
3443
3079
|
}
|
|
3444
3080
|
if (options?.interactive) {
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
const decision = await
|
|
3081
|
+
clack5.intro("viberails sync (interactive)");
|
|
3082
|
+
clack5.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
3083
|
+
const decision = await clack5.select({
|
|
3448
3084
|
message: "How would you like to proceed?",
|
|
3449
3085
|
options: [
|
|
3450
3086
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -3454,47 +3090,51 @@ ${chalk13.bold("Changes:")}`);
|
|
|
3454
3090
|
});
|
|
3455
3091
|
assertNotCancelled(decision);
|
|
3456
3092
|
if (decision === "cancel") {
|
|
3457
|
-
|
|
3093
|
+
clack5.outro("Sync cancelled. No files were written.");
|
|
3458
3094
|
return;
|
|
3459
3095
|
}
|
|
3460
3096
|
if (decision === "customize") {
|
|
3461
3097
|
const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
|
|
3462
3098
|
const overrides = await promptRuleMenu({
|
|
3463
3099
|
maxFileLines: merged.rules.maxFileLines,
|
|
3100
|
+
maxTestFileLines: merged.rules.maxTestFileLines,
|
|
3464
3101
|
testCoverage: merged.rules.testCoverage,
|
|
3465
3102
|
enforceMissingTests: merged.rules.enforceMissingTests,
|
|
3466
3103
|
enforceNaming: merged.rules.enforceNaming,
|
|
3467
3104
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3105
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3106
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3107
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3468
3108
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3469
3109
|
coverageCommand: merged.defaults?.coverage?.command,
|
|
3470
3110
|
packageOverrides: merged.packages
|
|
3471
3111
|
});
|
|
3472
3112
|
applyRuleOverrides(merged, overrides);
|
|
3473
|
-
const recompacted =
|
|
3474
|
-
|
|
3113
|
+
const recompacted = compactConfig5(merged);
|
|
3114
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3475
3115
|
`);
|
|
3476
3116
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3477
|
-
|
|
3478
|
-
|
|
3117
|
+
clack5.log.success("Updated config with your customizations.");
|
|
3118
|
+
clack5.outro("Done! Run viberails check to verify.");
|
|
3479
3119
|
return;
|
|
3480
3120
|
}
|
|
3481
3121
|
}
|
|
3482
|
-
|
|
3122
|
+
fs21.writeFileSync(configPath, `${compactedJson}
|
|
3483
3123
|
`);
|
|
3484
3124
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3485
3125
|
console.log(`
|
|
3486
|
-
${
|
|
3126
|
+
${chalk15.bold("Synced:")}`);
|
|
3487
3127
|
if (configChanged) {
|
|
3488
|
-
console.log(` ${
|
|
3128
|
+
console.log(` ${chalk15.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3489
3129
|
} else {
|
|
3490
|
-
console.log(` ${
|
|
3130
|
+
console.log(` ${chalk15.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3491
3131
|
}
|
|
3492
|
-
console.log(` ${
|
|
3493
|
-
console.log(` ${
|
|
3132
|
+
console.log(` ${chalk15.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
3133
|
+
console.log(` ${chalk15.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
3494
3134
|
}
|
|
3495
3135
|
|
|
3496
3136
|
// src/index.ts
|
|
3497
|
-
var VERSION = "0.6.
|
|
3137
|
+
var VERSION = "0.6.5";
|
|
3498
3138
|
var program = new Command();
|
|
3499
3139
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3500
3140
|
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) => {
|
|
@@ -3502,7 +3142,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3502
3142
|
await initCommand(options);
|
|
3503
3143
|
} catch (err) {
|
|
3504
3144
|
const message = err instanceof Error ? err.message : String(err);
|
|
3505
|
-
console.error(`${
|
|
3145
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3506
3146
|
process.exit(1);
|
|
3507
3147
|
}
|
|
3508
3148
|
});
|
|
@@ -3511,7 +3151,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3511
3151
|
await syncCommand(options);
|
|
3512
3152
|
} catch (err) {
|
|
3513
3153
|
const message = err instanceof Error ? err.message : String(err);
|
|
3514
|
-
console.error(`${
|
|
3154
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3515
3155
|
process.exit(1);
|
|
3516
3156
|
}
|
|
3517
3157
|
});
|
|
@@ -3520,7 +3160,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3520
3160
|
await configCommand(options);
|
|
3521
3161
|
} catch (err) {
|
|
3522
3162
|
const message = err instanceof Error ? err.message : String(err);
|
|
3523
|
-
console.error(`${
|
|
3163
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3524
3164
|
process.exit(1);
|
|
3525
3165
|
}
|
|
3526
3166
|
});
|
|
@@ -3541,7 +3181,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3541
3181
|
process.exit(exitCode);
|
|
3542
3182
|
} catch (err) {
|
|
3543
3183
|
const message = err instanceof Error ? err.message : String(err);
|
|
3544
|
-
console.error(`${
|
|
3184
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3545
3185
|
process.exit(1);
|
|
3546
3186
|
}
|
|
3547
3187
|
}
|
|
@@ -3552,7 +3192,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3552
3192
|
process.exit(exitCode);
|
|
3553
3193
|
} catch (err) {
|
|
3554
3194
|
const message = err instanceof Error ? err.message : String(err);
|
|
3555
|
-
console.error(`${
|
|
3195
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3556
3196
|
process.exit(1);
|
|
3557
3197
|
}
|
|
3558
3198
|
});
|
|
@@ -3561,7 +3201,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3561
3201
|
await boundariesCommand(options);
|
|
3562
3202
|
} catch (err) {
|
|
3563
3203
|
const message = err instanceof Error ? err.message : String(err);
|
|
3564
|
-
console.error(`${
|
|
3204
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3565
3205
|
process.exit(1);
|
|
3566
3206
|
}
|
|
3567
3207
|
});
|