viberails 0.6.4 → 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 +976 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +360 -880
- 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,581 +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 * as clack from "@clack/prompts";
|
|
35
|
-
|
|
36
|
-
// src/utils/spawn-async.ts
|
|
37
|
-
import { spawn } from "child_process";
|
|
38
|
-
function spawnAsync(command, cwd) {
|
|
39
|
-
return new Promise((resolve4) => {
|
|
40
|
-
const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
|
|
41
|
-
let stdout = "";
|
|
42
|
-
let stderr = "";
|
|
43
|
-
child.stdout.on("data", (d) => {
|
|
44
|
-
stdout += d.toString();
|
|
45
|
-
});
|
|
46
|
-
child.stderr.on("data", (d) => {
|
|
47
|
-
stderr += d.toString();
|
|
48
|
-
});
|
|
49
|
-
child.on("close", (status) => {
|
|
50
|
-
resolve4({ status, stdout, stderr });
|
|
51
|
-
});
|
|
52
|
-
child.on("error", () => {
|
|
53
|
-
resolve4({ status: 1, stdout, stderr });
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/utils/prompt-integrations.ts
|
|
59
|
-
async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
|
|
60
|
-
const choice = await clack.select({
|
|
61
|
-
message: "No shared git hook manager detected. Install Lefthook?",
|
|
62
|
-
options: [
|
|
63
|
-
{
|
|
64
|
-
value: "install",
|
|
65
|
-
label: "Yes, install Lefthook",
|
|
66
|
-
hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
value: "skip",
|
|
70
|
-
label: "No, skip",
|
|
71
|
-
hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
});
|
|
75
|
-
assertNotCancelled(choice);
|
|
76
|
-
if (choice !== "install") return void 0;
|
|
77
|
-
const pm = packageManager || "npm";
|
|
78
|
-
const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
|
|
79
|
-
const s = clack.spinner();
|
|
80
|
-
s.start("Installing Lefthook...");
|
|
81
|
-
const result = await spawnAsync(installCmd, projectRoot);
|
|
82
|
-
if (result.status === 0) {
|
|
83
|
-
const fs21 = await import("fs");
|
|
84
|
-
const path21 = await import("path");
|
|
85
|
-
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
86
|
-
if (!fs21.existsSync(lefthookPath)) {
|
|
87
|
-
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
88
|
-
}
|
|
89
|
-
s.stop("Installed Lefthook");
|
|
90
|
-
return "Lefthook";
|
|
91
|
-
}
|
|
92
|
-
s.stop("Failed to install Lefthook");
|
|
93
|
-
clack.log.warn(`Install manually: ${installCmd}`);
|
|
94
|
-
return void 0;
|
|
95
|
-
}
|
|
96
|
-
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
97
|
-
let resolvedHookManager = hookManager;
|
|
98
|
-
if (!resolvedHookManager) {
|
|
99
|
-
resolvedHookManager = await promptHookManagerInstall(
|
|
100
|
-
projectRoot,
|
|
101
|
-
tools?.packageManager ?? "npm",
|
|
102
|
-
tools?.isWorkspace
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
const isBareHook = !resolvedHookManager;
|
|
106
|
-
const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
|
|
107
|
-
const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
|
|
108
|
-
const options = [
|
|
109
|
-
{
|
|
110
|
-
value: "preCommit",
|
|
111
|
-
label: hookLabel,
|
|
112
|
-
hint: hookHint
|
|
113
|
-
}
|
|
114
|
-
];
|
|
115
|
-
if (tools?.isTypeScript) {
|
|
116
|
-
options.push({
|
|
117
|
-
value: "typecheck",
|
|
118
|
-
label: "Typecheck (tsc --noEmit)",
|
|
119
|
-
hint: "pre-commit hook + CI check"
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (tools?.linter) {
|
|
123
|
-
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
124
|
-
options.push({
|
|
125
|
-
value: "lint",
|
|
126
|
-
label: `Lint check (${linterName})`,
|
|
127
|
-
hint: "pre-commit hook + CI check"
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
options.push(
|
|
131
|
-
{
|
|
132
|
-
value: "claude",
|
|
133
|
-
label: "Claude Code hook",
|
|
134
|
-
hint: "checks files when Claude edits them"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
value: "claudeMd",
|
|
138
|
-
label: "CLAUDE.md reference",
|
|
139
|
-
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
value: "githubAction",
|
|
143
|
-
label: "GitHub Actions workflow",
|
|
144
|
-
hint: "blocks PRs that fail viberails check"
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
|
-
const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
|
|
148
|
-
const result = await clack.multiselect({
|
|
149
|
-
message: "Optional integrations",
|
|
150
|
-
options,
|
|
151
|
-
initialValues,
|
|
152
|
-
required: false
|
|
153
|
-
});
|
|
154
|
-
assertNotCancelled(result);
|
|
155
|
-
return {
|
|
156
|
-
preCommitHook: result.includes("preCommit"),
|
|
157
|
-
claudeCodeHook: result.includes("claude"),
|
|
158
|
-
claudeMdRef: result.includes("claudeMd"),
|
|
159
|
-
githubAction: result.includes("githubAction"),
|
|
160
|
-
typecheckHook: result.includes("typecheck"),
|
|
161
|
-
lintHook: result.includes("lint")
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// src/utils/prompt-rules.ts
|
|
166
|
-
import * as clack4 from "@clack/prompts";
|
|
167
|
-
|
|
168
|
-
// src/utils/prompt-menu-handlers.ts
|
|
169
|
-
import * as clack3 from "@clack/prompts";
|
|
170
|
-
|
|
171
|
-
// src/utils/prompt-package-overrides.ts
|
|
172
|
-
import * as clack2 from "@clack/prompts";
|
|
173
|
-
function normalizePackageOverrides(packages) {
|
|
174
|
-
for (const pkg of packages) {
|
|
175
|
-
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
176
|
-
delete pkg.rules;
|
|
177
|
-
}
|
|
178
|
-
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
179
|
-
delete pkg.coverage;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return packages;
|
|
183
|
-
}
|
|
184
|
-
function packageCoverageHint(pkg, defaults) {
|
|
185
|
-
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
186
|
-
const isExempt = coverage === 0;
|
|
187
|
-
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
188
|
-
const defaultCommand = defaults.coverageCommand ?? "";
|
|
189
|
-
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
190
|
-
const tags = [];
|
|
191
|
-
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
192
|
-
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
193
|
-
tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
|
|
194
|
-
if (hasSummaryOverride) tags.push("summary override");
|
|
195
|
-
if (hasCommandOverride) tags.push("command override");
|
|
196
|
-
return tags.join(", ");
|
|
197
|
-
}
|
|
198
|
-
async function promptPackageCoverageOverrides(packages, defaults) {
|
|
199
|
-
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
200
|
-
if (editablePackages.length === 0) return packages;
|
|
201
|
-
while (true) {
|
|
202
|
-
const selectedPath = await clack2.select({
|
|
203
|
-
message: "Select package to edit coverage overrides",
|
|
204
|
-
options: [
|
|
205
|
-
...editablePackages.map((pkg) => ({
|
|
206
|
-
value: pkg.path,
|
|
207
|
-
label: `${pkg.path} (${pkg.name})`,
|
|
208
|
-
hint: packageCoverageHint(pkg, defaults)
|
|
209
|
-
})),
|
|
210
|
-
{ value: "__done__", label: "Done" }
|
|
211
|
-
]
|
|
212
|
-
});
|
|
213
|
-
assertNotCancelled(selectedPath);
|
|
214
|
-
if (selectedPath === "__done__") break;
|
|
215
|
-
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
216
|
-
if (!target) continue;
|
|
217
|
-
while (true) {
|
|
218
|
-
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
219
|
-
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
220
|
-
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
|
|
221
|
-
const choice = await clack2.select({
|
|
222
|
-
message: `Edit coverage overrides for ${target.path}`,
|
|
223
|
-
options: [
|
|
224
|
-
{ value: "testCoverage", label: "testCoverage", hint: String(effectiveCoverage) },
|
|
225
|
-
{ value: "summaryPath", label: "coverage.summaryPath", hint: effectiveSummary },
|
|
226
|
-
{ value: "command", label: "coverage.command", hint: effectiveCommand },
|
|
227
|
-
{ value: "reset", label: "Reset this package to inherit defaults" },
|
|
228
|
-
{ value: "back", label: "Back to package list" }
|
|
229
|
-
]
|
|
230
|
-
});
|
|
231
|
-
assertNotCancelled(choice);
|
|
232
|
-
if (choice === "back") break;
|
|
233
|
-
if (choice === "testCoverage") {
|
|
234
|
-
const result = await clack2.text({
|
|
235
|
-
message: "Package testCoverage (0 to exempt package)?",
|
|
236
|
-
initialValue: String(effectiveCoverage),
|
|
237
|
-
validate: (v) => {
|
|
238
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
239
|
-
const n = Number.parseInt(v, 10);
|
|
240
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
assertNotCancelled(result);
|
|
244
|
-
const nextCoverage = Number.parseInt(result, 10);
|
|
245
|
-
if (nextCoverage === defaults.testCoverage) {
|
|
246
|
-
if (target.rules) {
|
|
247
|
-
delete target.rules.testCoverage;
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (choice === "summaryPath") {
|
|
254
|
-
const result = await clack2.text({
|
|
255
|
-
message: "Package coverage.summaryPath (blank to inherit default)?",
|
|
256
|
-
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
257
|
-
placeholder: defaults.coverageSummaryPath
|
|
258
|
-
});
|
|
259
|
-
assertNotCancelled(result);
|
|
260
|
-
const value = result.trim();
|
|
261
|
-
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
262
|
-
if (target.coverage) {
|
|
263
|
-
delete target.coverage.summaryPath;
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (choice === "command") {
|
|
270
|
-
const result = await clack2.text({
|
|
271
|
-
message: "Package coverage.command (blank to inherit default/auto)?",
|
|
272
|
-
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
273
|
-
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
274
|
-
});
|
|
275
|
-
assertNotCancelled(result);
|
|
276
|
-
const value = result.trim();
|
|
277
|
-
const defaultCommand = defaults.coverageCommand ?? "";
|
|
278
|
-
if (value.length === 0 || value === defaultCommand) {
|
|
279
|
-
if (target.coverage) {
|
|
280
|
-
delete target.coverage.command;
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (choice === "reset") {
|
|
287
|
-
if (target.rules) {
|
|
288
|
-
delete target.rules.testCoverage;
|
|
289
|
-
}
|
|
290
|
-
delete target.coverage;
|
|
291
|
-
}
|
|
292
|
-
normalizePackageOverrides(editablePackages);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return normalizePackageOverrides(packages);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// src/utils/prompt-menu-handlers.ts
|
|
299
|
-
function getPackageDiffs(pkg, root) {
|
|
300
|
-
const diffs = [];
|
|
301
|
-
const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
|
|
302
|
-
for (const key of convKeys) {
|
|
303
|
-
if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
|
|
304
|
-
diffs.push(`${key}: ${pkg.conventions[key]}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const stackKeys = [
|
|
308
|
-
"framework",
|
|
309
|
-
"language",
|
|
310
|
-
"styling",
|
|
311
|
-
"backend",
|
|
312
|
-
"orm",
|
|
313
|
-
"linter",
|
|
314
|
-
"formatter",
|
|
315
|
-
"testRunner",
|
|
316
|
-
"packageManager"
|
|
317
|
-
];
|
|
318
|
-
for (const key of stackKeys) {
|
|
319
|
-
if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
|
|
320
|
-
diffs.push(`${key}: ${pkg.stack[key]}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
324
|
-
diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
|
|
325
|
-
}
|
|
326
|
-
if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
|
|
327
|
-
diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
|
|
328
|
-
}
|
|
329
|
-
if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
|
|
330
|
-
diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
|
|
331
|
-
}
|
|
332
|
-
if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
|
|
333
|
-
diffs.push("coverage.command: (override)");
|
|
334
|
-
}
|
|
335
|
-
return diffs;
|
|
336
|
-
}
|
|
337
|
-
function buildMenuOptions(state, packageCount) {
|
|
338
|
-
const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
|
|
339
|
-
const options = [
|
|
340
|
-
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
341
|
-
{ value: "enforceNaming", label: "Enforce file naming", hint: namingHint }
|
|
342
|
-
];
|
|
343
|
-
if (state.fileNamingValue) {
|
|
344
|
-
options.push({
|
|
345
|
-
value: "fileNaming",
|
|
346
|
-
label: "File naming convention",
|
|
347
|
-
hint: state.fileNamingValue
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const isMonorepo = packageCount > 0;
|
|
351
|
-
const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
|
|
352
|
-
const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
|
|
353
|
-
options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
|
|
354
|
-
options.push({
|
|
355
|
-
value: "enforceMissingTests",
|
|
356
|
-
label: "Enforce missing tests",
|
|
357
|
-
hint: state.enforceMissingTests ? "yes" : "no"
|
|
358
|
-
});
|
|
359
|
-
if (state.testCoverage > 0) {
|
|
360
|
-
options.push(
|
|
361
|
-
{
|
|
362
|
-
value: "coverageSummaryPath",
|
|
363
|
-
label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
|
|
364
|
-
hint: state.coverageSummaryPath
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
value: "coverageCommand",
|
|
368
|
-
label: isMonorepo ? "Default coverage command" : "Coverage command",
|
|
369
|
-
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
370
|
-
}
|
|
371
|
-
);
|
|
372
|
-
if (isMonorepo) {
|
|
373
|
-
options.push({
|
|
374
|
-
value: "packageOverrides",
|
|
375
|
-
label: "Per-package coverage overrides",
|
|
376
|
-
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
options.push(
|
|
381
|
-
{ value: "reset", label: "Reset all to detected defaults" },
|
|
382
|
-
{ value: "done", label: "Done" }
|
|
383
|
-
);
|
|
384
|
-
return options;
|
|
385
|
-
}
|
|
386
|
-
function clonePackages(packages) {
|
|
387
|
-
return packages?.map((pkg) => ({
|
|
388
|
-
...pkg,
|
|
389
|
-
stack: pkg.stack ? { ...pkg.stack } : void 0,
|
|
390
|
-
structure: pkg.structure ? { ...pkg.structure } : void 0,
|
|
391
|
-
conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
|
|
392
|
-
rules: pkg.rules ? { ...pkg.rules } : void 0,
|
|
393
|
-
coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
|
|
394
|
-
ignore: pkg.ignore ? [...pkg.ignore] : void 0,
|
|
395
|
-
boundaries: pkg.boundaries ? {
|
|
396
|
-
deny: [...pkg.boundaries.deny],
|
|
397
|
-
ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
|
|
398
|
-
} : void 0
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
|
-
async function handleMenuChoice(choice, state, defaults, root) {
|
|
402
|
-
if (choice === "reset") {
|
|
403
|
-
state.maxFileLines = defaults.maxFileLines;
|
|
404
|
-
state.testCoverage = defaults.testCoverage;
|
|
405
|
-
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
406
|
-
state.enforceNaming = defaults.enforceNaming;
|
|
407
|
-
state.fileNamingValue = defaults.fileNamingValue;
|
|
408
|
-
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
409
|
-
state.coverageCommand = defaults.coverageCommand;
|
|
410
|
-
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
411
|
-
clack3.log.info("Reset all rules to detected defaults.");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
if (choice === "packageOverrides") {
|
|
415
|
-
if (state.packageOverrides) {
|
|
416
|
-
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
417
|
-
state.packageOverrides = await promptPackageCoverageOverrides(state.packageOverrides, {
|
|
418
|
-
testCoverage: state.testCoverage,
|
|
419
|
-
coverageSummaryPath: state.coverageSummaryPath,
|
|
420
|
-
coverageCommand: state.coverageCommand
|
|
421
|
-
});
|
|
422
|
-
const lines = packageDiffs.map((entry) => `${entry.pkg.path}
|
|
423
|
-
${entry.diffs.join(", ")}`);
|
|
424
|
-
if (lines.length > 0) {
|
|
425
|
-
clack3.note(lines.join("\n\n"), "Existing package differences");
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (choice === "maxFileLines") {
|
|
431
|
-
const result = await clack3.text({
|
|
432
|
-
message: "Maximum lines per source file?",
|
|
433
|
-
initialValue: String(state.maxFileLines),
|
|
434
|
-
validate: (v) => {
|
|
435
|
-
if (typeof v !== "string") return "Enter a positive number";
|
|
436
|
-
const n = Number.parseInt(v, 10);
|
|
437
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
assertNotCancelled(result);
|
|
441
|
-
state.maxFileLines = Number.parseInt(result, 10);
|
|
442
|
-
}
|
|
443
|
-
if (choice === "enforceMissingTests") {
|
|
444
|
-
const result = await clack3.confirm({
|
|
445
|
-
message: "Require every source file to have a corresponding test file?",
|
|
446
|
-
initialValue: state.enforceMissingTests
|
|
447
|
-
});
|
|
448
|
-
assertNotCancelled(result);
|
|
449
|
-
state.enforceMissingTests = result;
|
|
450
|
-
}
|
|
451
|
-
if (choice === "testCoverage") {
|
|
452
|
-
const result = await clack3.text({
|
|
453
|
-
message: "Test coverage target (0 disables coverage checks)?",
|
|
454
|
-
initialValue: String(state.testCoverage),
|
|
455
|
-
validate: (v) => {
|
|
456
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
457
|
-
const n = Number.parseInt(v, 10);
|
|
458
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
assertNotCancelled(result);
|
|
462
|
-
state.testCoverage = Number.parseInt(result, 10);
|
|
463
|
-
}
|
|
464
|
-
if (choice === "coverageSummaryPath") {
|
|
465
|
-
const result = await clack3.text({
|
|
466
|
-
message: "Coverage summary path (relative to package root)?",
|
|
467
|
-
initialValue: state.coverageSummaryPath,
|
|
468
|
-
validate: (v) => {
|
|
469
|
-
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
assertNotCancelled(result);
|
|
473
|
-
state.coverageSummaryPath = result.trim();
|
|
474
|
-
}
|
|
475
|
-
if (choice === "coverageCommand") {
|
|
476
|
-
const result = await clack3.text({
|
|
477
|
-
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
478
|
-
initialValue: state.coverageCommand ?? "",
|
|
479
|
-
placeholder: "(auto-detect from package.json test runner)"
|
|
480
|
-
});
|
|
481
|
-
assertNotCancelled(result);
|
|
482
|
-
const trimmed = result.trim();
|
|
483
|
-
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
484
|
-
}
|
|
485
|
-
if (choice === "enforceNaming") {
|
|
486
|
-
const result = await clack3.confirm({
|
|
487
|
-
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
488
|
-
initialValue: state.enforceNaming
|
|
489
|
-
});
|
|
490
|
-
assertNotCancelled(result);
|
|
491
|
-
state.enforceNaming = result;
|
|
492
|
-
}
|
|
493
|
-
if (choice === "fileNaming") {
|
|
494
|
-
const selected = await clack3.select({
|
|
495
|
-
message: "Which file naming convention should be enforced?",
|
|
496
|
-
options: [
|
|
497
|
-
{ value: "kebab-case", label: "kebab-case" },
|
|
498
|
-
{ value: "camelCase", label: "camelCase" },
|
|
499
|
-
{ value: "PascalCase", label: "PascalCase" },
|
|
500
|
-
{ value: "snake_case", label: "snake_case" }
|
|
501
|
-
],
|
|
502
|
-
initialValue: state.fileNamingValue
|
|
503
|
-
});
|
|
504
|
-
assertNotCancelled(selected);
|
|
505
|
-
state.fileNamingValue = selected;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// src/utils/prompt-rules.ts
|
|
510
|
-
function getRootPackage(packages) {
|
|
511
|
-
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
512
|
-
}
|
|
513
|
-
async function promptRuleMenu(defaults) {
|
|
514
|
-
const state = {
|
|
515
|
-
...defaults,
|
|
516
|
-
packageOverrides: clonePackages(defaults.packageOverrides)
|
|
517
|
-
};
|
|
518
|
-
const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
|
|
519
|
-
const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
|
|
520
|
-
while (true) {
|
|
521
|
-
const options = buildMenuOptions(state, packageCount);
|
|
522
|
-
const choice = await clack4.select({ message: "Customize rules", options });
|
|
523
|
-
assertNotCancelled(choice);
|
|
524
|
-
if (choice === "done") break;
|
|
525
|
-
await handleMenuChoice(choice, state, defaults, root);
|
|
526
|
-
}
|
|
527
|
-
return {
|
|
528
|
-
maxFileLines: state.maxFileLines,
|
|
529
|
-
testCoverage: state.testCoverage,
|
|
530
|
-
enforceMissingTests: state.enforceMissingTests,
|
|
531
|
-
enforceNaming: state.enforceNaming,
|
|
532
|
-
fileNamingValue: state.fileNamingValue,
|
|
533
|
-
coverageSummaryPath: state.coverageSummaryPath,
|
|
534
|
-
coverageCommand: state.coverageCommand,
|
|
535
|
-
packageOverrides: state.packageOverrides
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// src/utils/prompt.ts
|
|
540
|
-
function assertNotCancelled(value) {
|
|
541
|
-
if (clack5.isCancel(value)) {
|
|
542
|
-
clack5.cancel("Setup cancelled.");
|
|
543
|
-
process.exit(0);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
async function confirm3(message) {
|
|
547
|
-
const result = await clack5.confirm({ message, initialValue: true });
|
|
548
|
-
assertNotCancelled(result);
|
|
549
|
-
return result;
|
|
550
|
-
}
|
|
551
|
-
async function confirmDangerous(message) {
|
|
552
|
-
const result = await clack5.confirm({ message, initialValue: false });
|
|
553
|
-
assertNotCancelled(result);
|
|
554
|
-
return result;
|
|
555
|
-
}
|
|
556
|
-
async function promptExistingConfigAction(configFile) {
|
|
557
|
-
const result = await clack5.select({
|
|
558
|
-
message: `${configFile} already exists. What do you want to do?`,
|
|
559
|
-
options: [
|
|
560
|
-
{
|
|
561
|
-
value: "edit",
|
|
562
|
-
label: "Edit existing config",
|
|
563
|
-
hint: "open the current rules and save updates in place"
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
value: "replace",
|
|
567
|
-
label: "Replace with a fresh scan",
|
|
568
|
-
hint: "re-scan the project and overwrite the current config"
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
value: "cancel",
|
|
572
|
-
label: "Cancel",
|
|
573
|
-
hint: "leave the current setup unchanged"
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
});
|
|
577
|
-
assertNotCancelled(result);
|
|
578
|
-
return result;
|
|
579
|
-
}
|
|
580
|
-
async function promptInitDecision() {
|
|
581
|
-
const result = await clack5.select({
|
|
582
|
-
message: "How do you want to proceed?",
|
|
583
|
-
options: [
|
|
584
|
-
{
|
|
585
|
-
value: "accept",
|
|
586
|
-
label: "Accept defaults",
|
|
587
|
-
hint: "writes the config with these defaults; use --enforce in CI to block"
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
value: "customize",
|
|
591
|
-
label: "Customize rules",
|
|
592
|
-
hint: "edit limits, naming, test coverage, and package overrides"
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
value: "review",
|
|
596
|
-
label: "Review detected details",
|
|
597
|
-
hint: "show the full scan report with package and structure details"
|
|
598
|
-
}
|
|
599
|
-
]
|
|
600
|
-
});
|
|
601
|
-
assertNotCancelled(result);
|
|
602
|
-
return result;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
41
|
// src/utils/resolve-workspace-packages.ts
|
|
606
42
|
import * as fs2 from "fs";
|
|
607
43
|
import * as path2 from "path";
|
|
@@ -706,7 +142,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
706
142
|
console.log(`
|
|
707
143
|
${totalRules} denied`);
|
|
708
144
|
console.log("");
|
|
709
|
-
const shouldSave = await
|
|
145
|
+
const shouldSave = await confirm("Save to viberails.config.json?");
|
|
710
146
|
if (shouldSave) {
|
|
711
147
|
config.boundaries = inferred;
|
|
712
148
|
config.rules.enforceBoundaries = true;
|
|
@@ -1218,13 +654,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1218
654
|
const testSuffix = testPattern.replace("*", "");
|
|
1219
655
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1220
656
|
for (const relFile of sourceFiles) {
|
|
1221
|
-
const
|
|
1222
|
-
if (
|
|
657
|
+
const basename10 = path6.basename(relFile);
|
|
658
|
+
if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
|
|
1223
659
|
continue;
|
|
1224
660
|
}
|
|
1225
|
-
const ext = path6.extname(
|
|
661
|
+
const ext = path6.extname(basename10);
|
|
1226
662
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1227
|
-
const stem =
|
|
663
|
+
const stem = basename10.slice(0, -ext.length);
|
|
1228
664
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1229
665
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1230
666
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
@@ -1305,9 +741,9 @@ async function checkCommand(options, cwd) {
|
|
|
1305
741
|
}
|
|
1306
742
|
const violations = [];
|
|
1307
743
|
const severity = options.enforce ? "error" : "warn";
|
|
1308
|
-
const
|
|
744
|
+
const log5 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
|
|
1309
745
|
};
|
|
1310
|
-
|
|
746
|
+
log5(" Checking files...");
|
|
1311
747
|
for (const file of filesToCheck) {
|
|
1312
748
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1313
749
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1340,9 +776,9 @@ async function checkCommand(options, cwd) {
|
|
|
1340
776
|
}
|
|
1341
777
|
}
|
|
1342
778
|
}
|
|
1343
|
-
|
|
779
|
+
log5(" done\n");
|
|
1344
780
|
if (!options.files) {
|
|
1345
|
-
|
|
781
|
+
log5(" Checking missing tests...");
|
|
1346
782
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1347
783
|
if (options.staged) {
|
|
1348
784
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1355,14 +791,14 @@ async function checkCommand(options, cwd) {
|
|
|
1355
791
|
} else {
|
|
1356
792
|
violations.push(...testViolations);
|
|
1357
793
|
}
|
|
1358
|
-
|
|
794
|
+
log5(" done\n");
|
|
1359
795
|
}
|
|
1360
796
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1361
|
-
|
|
797
|
+
log5(" Running test coverage...\n");
|
|
1362
798
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1363
799
|
staged: options.staged,
|
|
1364
800
|
enforce: options.enforce,
|
|
1365
|
-
onProgress: (pkg) =>
|
|
801
|
+
onProgress: (pkg) => log5(` Coverage: ${pkg}...
|
|
1366
802
|
`)
|
|
1367
803
|
});
|
|
1368
804
|
violations.push(...coverageViolations);
|
|
@@ -1387,7 +823,7 @@ async function checkCommand(options, cwd) {
|
|
|
1387
823
|
severity
|
|
1388
824
|
});
|
|
1389
825
|
}
|
|
1390
|
-
|
|
826
|
+
log5(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1391
827
|
`);
|
|
1392
828
|
}
|
|
1393
829
|
if (options.format === "json") {
|
|
@@ -1463,7 +899,7 @@ async function hookCheckCommand(cwd) {
|
|
|
1463
899
|
// src/commands/config.ts
|
|
1464
900
|
import * as fs10 from "fs";
|
|
1465
901
|
import * as path9 from "path";
|
|
1466
|
-
import * as
|
|
902
|
+
import * as clack from "@clack/prompts";
|
|
1467
903
|
import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
|
|
1468
904
|
import { scan } from "@viberails/scanner";
|
|
1469
905
|
import chalk6 from "chalk";
|
|
@@ -1654,15 +1090,6 @@ function formatMonorepoResultsText(scanResult) {
|
|
|
1654
1090
|
}
|
|
1655
1091
|
|
|
1656
1092
|
// src/display.ts
|
|
1657
|
-
var INIT_OVERVIEW_NAMES = {
|
|
1658
|
-
typescript: "TypeScript",
|
|
1659
|
-
javascript: "JavaScript",
|
|
1660
|
-
eslint: "ESLint",
|
|
1661
|
-
prettier: "Prettier",
|
|
1662
|
-
jest: "Jest",
|
|
1663
|
-
vitest: "Vitest",
|
|
1664
|
-
biome: "Biome"
|
|
1665
|
-
};
|
|
1666
1093
|
function formatItem(item, nameMap) {
|
|
1667
1094
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1668
1095
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1792,134 +1219,6 @@ function displayRulesPreview(config) {
|
|
|
1792
1219
|
);
|
|
1793
1220
|
console.log("");
|
|
1794
1221
|
}
|
|
1795
|
-
function formatDetectedOverview(scanResult) {
|
|
1796
|
-
const { stack } = scanResult;
|
|
1797
|
-
const primaryParts = [];
|
|
1798
|
-
const secondaryParts = [];
|
|
1799
|
-
const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
|
|
1800
|
-
if (scanResult.packages.length > 1) {
|
|
1801
|
-
primaryParts.push("monorepo");
|
|
1802
|
-
primaryParts.push(`${scanResult.packages.length} packages`);
|
|
1803
|
-
} else if (stack.framework) {
|
|
1804
|
-
primaryParts.push(formatItem(stack.framework, FRAMEWORK_NAMES2));
|
|
1805
|
-
} else {
|
|
1806
|
-
primaryParts.push("single package");
|
|
1807
|
-
}
|
|
1808
|
-
primaryParts.push(formatOverviewItem(stack.language));
|
|
1809
|
-
if (stack.styling) {
|
|
1810
|
-
primaryParts.push(formatOverviewItem(stack.styling, STYLING_NAMES2));
|
|
1811
|
-
}
|
|
1812
|
-
if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
|
|
1813
|
-
if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
|
|
1814
|
-
if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
|
|
1815
|
-
if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
|
|
1816
|
-
const primary = primaryParts.map((part) => chalk5.cyan(part)).join(chalk5.dim(" \xB7 "));
|
|
1817
|
-
const secondary = secondaryParts.join(chalk5.dim(" \xB7 "));
|
|
1818
|
-
return secondary ? `${primary}
|
|
1819
|
-
${chalk5.dim(secondary)}` : primary;
|
|
1820
|
-
}
|
|
1821
|
-
function displayInitOverview(scanResult, config, exemptedPackages) {
|
|
1822
|
-
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1823
|
-
const isMonorepo = config.packages.length > 1;
|
|
1824
|
-
const ok = chalk5.green("\u2713");
|
|
1825
|
-
const info = chalk5.yellow("~");
|
|
1826
|
-
console.log("");
|
|
1827
|
-
console.log(` ${chalk5.bold("Ready to initialize:")}`);
|
|
1828
|
-
console.log(` ${formatDetectedOverview(scanResult)}`);
|
|
1829
|
-
console.log("");
|
|
1830
|
-
console.log(` ${chalk5.bold("Rules to apply:")}`);
|
|
1831
|
-
console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1832
|
-
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1833
|
-
if (config.rules.enforceNaming && fileNaming) {
|
|
1834
|
-
console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
|
|
1835
|
-
} else {
|
|
1836
|
-
console.log(` ${info} File naming: ${chalk5.dim("not enforced")}`);
|
|
1837
|
-
}
|
|
1838
|
-
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1839
|
-
if (config.rules.enforceMissingTests && testPattern) {
|
|
1840
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan(`enforced (${testPattern})`)}`);
|
|
1841
|
-
} else if (config.rules.enforceMissingTests) {
|
|
1842
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
|
|
1843
|
-
} else {
|
|
1844
|
-
console.log(` ${info} Missing tests: ${chalk5.dim("not enforced")}`);
|
|
1845
|
-
}
|
|
1846
|
-
if (config.rules.testCoverage > 0) {
|
|
1847
|
-
if (isMonorepo) {
|
|
1848
|
-
const withCoverage = config.packages.filter(
|
|
1849
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1850
|
-
);
|
|
1851
|
-
console.log(
|
|
1852
|
-
` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)} default ${chalk5.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1853
|
-
);
|
|
1854
|
-
} else {
|
|
1855
|
-
console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1856
|
-
}
|
|
1857
|
-
} else {
|
|
1858
|
-
console.log(` ${info} Coverage: ${chalk5.dim("disabled")}`);
|
|
1859
|
-
}
|
|
1860
|
-
if (exemptedPackages.length > 0) {
|
|
1861
|
-
console.log(
|
|
1862
|
-
` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
console.log("");
|
|
1866
|
-
console.log(` ${chalk5.bold("Also available:")}`);
|
|
1867
|
-
if (isMonorepo) {
|
|
1868
|
-
console.log(` ${info} Infer boundaries from current imports`);
|
|
1869
|
-
}
|
|
1870
|
-
console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
|
|
1871
|
-
console.log(
|
|
1872
|
-
`
|
|
1873
|
-
${chalk5.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
|
|
1874
|
-
);
|
|
1875
|
-
console.log("");
|
|
1876
|
-
}
|
|
1877
|
-
function summarizeSelectedIntegrations(integrations, opts) {
|
|
1878
|
-
const lines = [];
|
|
1879
|
-
if (opts.hasBoundaries) {
|
|
1880
|
-
lines.push("\u2713 Boundary rules: inferred from current imports");
|
|
1881
|
-
} else {
|
|
1882
|
-
lines.push("~ Boundary rules: not enabled");
|
|
1883
|
-
}
|
|
1884
|
-
if (opts.hasCoverage) {
|
|
1885
|
-
lines.push("\u2713 Coverage checks: enabled");
|
|
1886
|
-
} else {
|
|
1887
|
-
lines.push("~ Coverage checks: disabled");
|
|
1888
|
-
}
|
|
1889
|
-
const selectedIntegrations = [
|
|
1890
|
-
integrations.preCommitHook ? "pre-commit hook" : void 0,
|
|
1891
|
-
integrations.typecheckHook ? "typecheck" : void 0,
|
|
1892
|
-
integrations.lintHook ? "lint check" : void 0,
|
|
1893
|
-
integrations.claudeCodeHook ? "Claude Code hook" : void 0,
|
|
1894
|
-
integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
|
|
1895
|
-
integrations.githubAction ? "GitHub Actions workflow" : void 0
|
|
1896
|
-
].filter(Boolean);
|
|
1897
|
-
if (selectedIntegrations.length > 0) {
|
|
1898
|
-
lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
|
|
1899
|
-
} else {
|
|
1900
|
-
lines.push("~ Integrations: none selected");
|
|
1901
|
-
}
|
|
1902
|
-
return lines;
|
|
1903
|
-
}
|
|
1904
|
-
function displaySetupPlan(config, integrations, opts = {}) {
|
|
1905
|
-
const configFile = opts.configFile ?? "viberails.config.json";
|
|
1906
|
-
const lines = summarizeSelectedIntegrations(integrations, {
|
|
1907
|
-
hasBoundaries: config.rules.enforceBoundaries,
|
|
1908
|
-
hasCoverage: config.rules.testCoverage > 0
|
|
1909
|
-
});
|
|
1910
|
-
console.log("");
|
|
1911
|
-
console.log(` ${chalk5.bold("Ready to write:")}`);
|
|
1912
|
-
console.log(
|
|
1913
|
-
` ${opts.replacingExistingConfig ? chalk5.yellow("!") : chalk5.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? chalk5.dim(" (replacing existing config)") : ""}`
|
|
1914
|
-
);
|
|
1915
|
-
console.log(` ${chalk5.green("\u2713")} .viberails/context.md`);
|
|
1916
|
-
console.log(` ${chalk5.green("\u2713")} .viberails/scan-result.json`);
|
|
1917
|
-
for (const line of lines) {
|
|
1918
|
-
const icon = line.startsWith("\u2713") ? chalk5.green("\u2713") : chalk5.yellow("~");
|
|
1919
|
-
console.log(` ${icon} ${line.slice(2)}`);
|
|
1920
|
-
}
|
|
1921
|
-
console.log("");
|
|
1922
|
-
}
|
|
1923
1222
|
|
|
1924
1223
|
// src/display-text.ts
|
|
1925
1224
|
function plainConfidenceLabel(convention) {
|
|
@@ -2038,7 +1337,9 @@ function formatScanResultsText(scanResult) {
|
|
|
2038
1337
|
// src/utils/apply-rule-overrides.ts
|
|
2039
1338
|
function applyRuleOverrides(config, overrides) {
|
|
2040
1339
|
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1340
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2041
1341
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1342
|
+
config.rules.maxTestFileLines = overrides.maxTestFileLines;
|
|
2042
1343
|
config.rules.testCoverage = overrides.testCoverage;
|
|
2043
1344
|
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
2044
1345
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
@@ -2052,7 +1353,6 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2052
1353
|
}
|
|
2053
1354
|
}
|
|
2054
1355
|
if (overrides.fileNamingValue) {
|
|
2055
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2056
1356
|
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
2057
1357
|
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2058
1358
|
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
@@ -2064,6 +1364,18 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2064
1364
|
}
|
|
2065
1365
|
}
|
|
2066
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
|
+
}
|
|
2067
1379
|
}
|
|
2068
1380
|
|
|
2069
1381
|
// src/utils/diff-configs.ts
|
|
@@ -2232,27 +1544,31 @@ async function configCommand(options, cwd) {
|
|
|
2232
1544
|
return;
|
|
2233
1545
|
}
|
|
2234
1546
|
if (!options.suppressIntro) {
|
|
2235
|
-
|
|
1547
|
+
clack.intro("viberails config");
|
|
2236
1548
|
}
|
|
2237
1549
|
const config = await loadConfig3(configPath);
|
|
2238
1550
|
let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
|
|
2239
|
-
|
|
1551
|
+
clack.note(formatRulesText(config).join("\n"), "Current rules");
|
|
2240
1552
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2241
1553
|
const overrides = await promptRuleMenu({
|
|
2242
1554
|
maxFileLines: config.rules.maxFileLines,
|
|
1555
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2243
1556
|
testCoverage: config.rules.testCoverage,
|
|
2244
1557
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2245
1558
|
enforceNaming: config.rules.enforceNaming,
|
|
2246
1559
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
1560
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
1561
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
1562
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
2247
1563
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
2248
1564
|
coverageCommand: config.defaults?.coverage?.command,
|
|
2249
1565
|
packageOverrides: config.packages
|
|
2250
1566
|
});
|
|
2251
1567
|
applyRuleOverrides(config, overrides);
|
|
2252
1568
|
if (options.rescan && config.packages.length > 1) {
|
|
2253
|
-
const shouldInfer = await
|
|
1569
|
+
const shouldInfer = await confirm("Re-infer boundary rules from import patterns?");
|
|
2254
1570
|
if (shouldInfer) {
|
|
2255
|
-
const bs =
|
|
1571
|
+
const bs = clack.spinner();
|
|
2256
1572
|
bs.start("Building import graph...");
|
|
2257
1573
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2258
1574
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -2268,31 +1584,31 @@ async function configCommand(options, cwd) {
|
|
|
2268
1584
|
}
|
|
2269
1585
|
}
|
|
2270
1586
|
}
|
|
2271
|
-
const shouldWrite = await
|
|
1587
|
+
const shouldWrite = await confirm("Save updated configuration?");
|
|
2272
1588
|
if (!shouldWrite) {
|
|
2273
|
-
|
|
1589
|
+
clack.outro("No changes written.");
|
|
2274
1590
|
return;
|
|
2275
1591
|
}
|
|
2276
1592
|
const compacted = compactConfig2(config);
|
|
2277
1593
|
fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2278
1594
|
`);
|
|
2279
1595
|
if (!scanResult) {
|
|
2280
|
-
const s =
|
|
1596
|
+
const s = clack.spinner();
|
|
2281
1597
|
s.start("Scanning for context generation...");
|
|
2282
1598
|
scanResult = await scan(projectRoot);
|
|
2283
1599
|
s.stop("Scan complete");
|
|
2284
1600
|
}
|
|
2285
1601
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2286
|
-
|
|
1602
|
+
clack.log.success(
|
|
2287
1603
|
`Updated:
|
|
2288
1604
|
${CONFIG_FILE3}
|
|
2289
1605
|
.viberails/context.md
|
|
2290
1606
|
.viberails/scan-result.json`
|
|
2291
1607
|
);
|
|
2292
|
-
|
|
1608
|
+
clack.outro("Done! Run viberails check to verify.");
|
|
2293
1609
|
}
|
|
2294
1610
|
async function rescanAndMerge(projectRoot, config) {
|
|
2295
|
-
const s =
|
|
1611
|
+
const s = clack.spinner();
|
|
2296
1612
|
s.start("Re-scanning project...");
|
|
2297
1613
|
const scanResult = await scan(projectRoot);
|
|
2298
1614
|
const merged = mergeConfig(config, scanResult);
|
|
@@ -2303,9 +1619,9 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
2303
1619
|
const icon = c.type === "removed" ? "-" : "+";
|
|
2304
1620
|
return `${icon} ${c.description}`;
|
|
2305
1621
|
}).join("\n");
|
|
2306
|
-
|
|
1622
|
+
clack.note(changeLines, "Changes detected");
|
|
2307
1623
|
} else {
|
|
2308
|
-
|
|
1624
|
+
clack.log.info("No new changes detected from scan.");
|
|
2309
1625
|
}
|
|
2310
1626
|
Object.assign(config, merged);
|
|
2311
1627
|
return scanResult;
|
|
@@ -2624,10 +1940,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2624
1940
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2625
1941
|
const testPattern = pkg?.structure?.testPattern;
|
|
2626
1942
|
if (!testPattern) return null;
|
|
2627
|
-
const
|
|
2628
|
-
const ext = path12.extname(
|
|
1943
|
+
const basename10 = path12.basename(sourceRelPath);
|
|
1944
|
+
const ext = path12.extname(basename10);
|
|
2629
1945
|
if (!ext) return null;
|
|
2630
|
-
const stem =
|
|
1946
|
+
const stem = basename10.slice(0, -ext.length);
|
|
2631
1947
|
const testSuffix = testPattern.replace("*", "");
|
|
2632
1948
|
const testFilename = `${stem}${testSuffix}`;
|
|
2633
1949
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2799,18 +2115,159 @@ ${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
|
2799
2115
|
}
|
|
2800
2116
|
|
|
2801
2117
|
// src/commands/init.ts
|
|
2802
|
-
import * as
|
|
2803
|
-
import * as
|
|
2804
|
-
import * as
|
|
2805
|
-
import { compactConfig as
|
|
2806
|
-
import { scan as
|
|
2807
|
-
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
|
+
}
|
|
2808
2265
|
|
|
2809
2266
|
// src/utils/check-prerequisites.ts
|
|
2810
2267
|
import * as fs14 from "fs";
|
|
2811
2268
|
import * as path14 from "path";
|
|
2812
|
-
import * as
|
|
2813
|
-
import
|
|
2269
|
+
import * as clack2 from "@clack/prompts";
|
|
2270
|
+
import chalk10 from "chalk";
|
|
2814
2271
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2815
2272
|
const pm = scanResult.stack.packageManager.name;
|
|
2816
2273
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -2841,9 +2298,9 @@ function displayMissingPrereqs(prereqs) {
|
|
|
2841
2298
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2842
2299
|
for (const m of missing) {
|
|
2843
2300
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2844
|
-
console.log(` ${
|
|
2301
|
+
console.log(` ${chalk10.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2845
2302
|
if (m.installCommand) {
|
|
2846
|
-
console.log(` Install: ${
|
|
2303
|
+
console.log(` Install: ${chalk10.cyan(m.installCommand)}`);
|
|
2847
2304
|
}
|
|
2848
2305
|
}
|
|
2849
2306
|
}
|
|
@@ -2855,13 +2312,13 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2855
2312
|
const detail = p.affectedPackages ? `needed by: ${p.affectedPackages.join(", ")}` : p.reason;
|
|
2856
2313
|
return `\u2717 ${p.label} \u2014 ${detail}`;
|
|
2857
2314
|
}).join("\n");
|
|
2858
|
-
|
|
2315
|
+
clack2.note(prereqLines, "Coverage support");
|
|
2859
2316
|
let disableCoverage = false;
|
|
2860
2317
|
for (const m of missing) {
|
|
2861
2318
|
if (!m.installCommand) continue;
|
|
2862
2319
|
const pkgCount = m.affectedPackages?.length;
|
|
2863
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.`;
|
|
2864
|
-
const choice = await
|
|
2321
|
+
const choice = await clack2.select({
|
|
2865
2322
|
message,
|
|
2866
2323
|
options: [
|
|
2867
2324
|
{
|
|
@@ -2883,23 +2340,23 @@ async function promptMissingPrereqs(projectRoot, prereqs) {
|
|
|
2883
2340
|
});
|
|
2884
2341
|
assertNotCancelled(choice);
|
|
2885
2342
|
if (choice === "install") {
|
|
2886
|
-
const is =
|
|
2343
|
+
const is = clack2.spinner();
|
|
2887
2344
|
is.start(`Installing ${m.label}...`);
|
|
2888
2345
|
const result = await spawnAsync(m.installCommand, projectRoot);
|
|
2889
2346
|
if (result.status === 0) {
|
|
2890
2347
|
is.stop(`Installed ${m.label}`);
|
|
2891
2348
|
} else {
|
|
2892
2349
|
is.stop(`Failed to install ${m.label}`);
|
|
2893
|
-
|
|
2350
|
+
clack2.log.warn(
|
|
2894
2351
|
`Install manually: ${m.installCommand}
|
|
2895
2352
|
Coverage percentage checks will not work until the dependency is installed.`
|
|
2896
2353
|
);
|
|
2897
2354
|
}
|
|
2898
2355
|
} else if (choice === "disable") {
|
|
2899
2356
|
disableCoverage = true;
|
|
2900
|
-
|
|
2357
|
+
clack2.log.info("Coverage percentage checks disabled. Missing-test checks remain active.");
|
|
2901
2358
|
} else {
|
|
2902
|
-
|
|
2359
|
+
clack2.log.info(
|
|
2903
2360
|
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2904
2361
|
Install later: ${m.installCommand}`
|
|
2905
2362
|
);
|
|
@@ -2917,20 +2374,6 @@ function hasDependency(projectRoot, name) {
|
|
|
2917
2374
|
}
|
|
2918
2375
|
}
|
|
2919
2376
|
|
|
2920
|
-
// src/utils/filter-confidence.ts
|
|
2921
|
-
function filterHighConfidence(conventions, meta) {
|
|
2922
|
-
if (!meta) return conventions;
|
|
2923
|
-
const filtered = {};
|
|
2924
|
-
for (const [key, value] of Object.entries(conventions)) {
|
|
2925
|
-
if (value === void 0) continue;
|
|
2926
|
-
const convMeta = meta[key];
|
|
2927
|
-
if (!convMeta || convMeta.confidence === "high") {
|
|
2928
|
-
filtered[key] = value;
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
return filtered;
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
2377
|
// src/utils/update-gitignore.ts
|
|
2935
2378
|
import * as fs15 from "fs";
|
|
2936
2379
|
import * as path15 from "path";
|
|
@@ -2951,7 +2394,7 @@ function updateGitignore(projectRoot) {
|
|
|
2951
2394
|
// src/commands/init-hooks.ts
|
|
2952
2395
|
import * as fs17 from "fs";
|
|
2953
2396
|
import * as path17 from "path";
|
|
2954
|
-
import
|
|
2397
|
+
import chalk11 from "chalk";
|
|
2955
2398
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2956
2399
|
|
|
2957
2400
|
// src/commands/resolve-typecheck.ts
|
|
@@ -2996,13 +2439,13 @@ function setupPreCommitHook(projectRoot) {
|
|
|
2996
2439
|
const lefthookPath = path17.join(projectRoot, "lefthook.yml");
|
|
2997
2440
|
if (fs17.existsSync(lefthookPath)) {
|
|
2998
2441
|
addLefthookPreCommit(lefthookPath);
|
|
2999
|
-
console.log(` ${
|
|
2442
|
+
console.log(` ${chalk11.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
3000
2443
|
return "lefthook.yml";
|
|
3001
2444
|
}
|
|
3002
2445
|
const huskyDir = path17.join(projectRoot, ".husky");
|
|
3003
2446
|
if (fs17.existsSync(huskyDir)) {
|
|
3004
2447
|
writeHuskyPreCommit(huskyDir);
|
|
3005
|
-
console.log(` ${
|
|
2448
|
+
console.log(` ${chalk11.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
3006
2449
|
return ".husky/pre-commit";
|
|
3007
2450
|
}
|
|
3008
2451
|
const gitDir = path17.join(projectRoot, ".git");
|
|
@@ -3012,7 +2455,7 @@ function setupPreCommitHook(projectRoot) {
|
|
|
3012
2455
|
fs17.mkdirSync(hooksDir, { recursive: true });
|
|
3013
2456
|
}
|
|
3014
2457
|
writeGitHookPreCommit(hooksDir);
|
|
3015
|
-
console.log(` ${
|
|
2458
|
+
console.log(` ${chalk11.green("\u2713")} .git/hooks/pre-commit`);
|
|
3016
2459
|
return ".git/hooks/pre-commit";
|
|
3017
2460
|
}
|
|
3018
2461
|
return void 0;
|
|
@@ -3073,9 +2516,9 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3073
2516
|
settings = JSON.parse(fs17.readFileSync(settingsPath, "utf-8"));
|
|
3074
2517
|
} catch {
|
|
3075
2518
|
console.warn(
|
|
3076
|
-
` ${
|
|
2519
|
+
` ${chalk11.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
3077
2520
|
);
|
|
3078
|
-
console.warn(` Fix the JSON manually, then re-run ${
|
|
2521
|
+
console.warn(` Fix the JSON manually, then re-run ${chalk11.cyan("viberails init --force")}`);
|
|
3079
2522
|
return;
|
|
3080
2523
|
}
|
|
3081
2524
|
}
|
|
@@ -3098,7 +2541,7 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3098
2541
|
settings.hooks = hooks;
|
|
3099
2542
|
fs17.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3100
2543
|
`);
|
|
3101
|
-
console.log(` ${
|
|
2544
|
+
console.log(` ${chalk11.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
3102
2545
|
}
|
|
3103
2546
|
function setupClaudeMdReference(projectRoot) {
|
|
3104
2547
|
const claudeMdPath = path17.join(projectRoot, "CLAUDE.md");
|
|
@@ -3110,7 +2553,7 @@ function setupClaudeMdReference(projectRoot) {
|
|
|
3110
2553
|
const ref = "\n@.viberails/context.md\n";
|
|
3111
2554
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
3112
2555
|
fs17.writeFileSync(claudeMdPath, prefix + ref);
|
|
3113
|
-
console.log(` ${
|
|
2556
|
+
console.log(` ${chalk11.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
3114
2557
|
}
|
|
3115
2558
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
3116
2559
|
const workflowDir = path17.join(projectRoot, ".github", "workflows");
|
|
@@ -3196,7 +2639,7 @@ ${cmd}
|
|
|
3196
2639
|
// src/commands/init-hooks-extra.ts
|
|
3197
2640
|
import * as fs18 from "fs";
|
|
3198
2641
|
import * as path18 from "path";
|
|
3199
|
-
import
|
|
2642
|
+
import chalk12 from "chalk";
|
|
3200
2643
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
3201
2644
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3202
2645
|
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
@@ -3256,12 +2699,12 @@ ${command}
|
|
|
3256
2699
|
function setupTypecheckHook(projectRoot, packageManager) {
|
|
3257
2700
|
const resolved = resolveTypecheckCommand(projectRoot, packageManager);
|
|
3258
2701
|
if (!resolved.command) {
|
|
3259
|
-
console.log(` ${
|
|
2702
|
+
console.log(` ${chalk12.yellow("!")} Skipped typecheck hook: ${resolved.reason}`);
|
|
3260
2703
|
return void 0;
|
|
3261
2704
|
}
|
|
3262
2705
|
const target = addPreCommitStep(projectRoot, "typecheck", resolved.command, "typecheck");
|
|
3263
2706
|
if (target) {
|
|
3264
|
-
console.log(` ${
|
|
2707
|
+
console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added typecheck (${resolved.label})`);
|
|
3265
2708
|
}
|
|
3266
2709
|
return target;
|
|
3267
2710
|
}
|
|
@@ -3282,7 +2725,7 @@ function setupLintHook(projectRoot, linter) {
|
|
|
3282
2725
|
}
|
|
3283
2726
|
const target = addPreCommitStep(projectRoot, "lint", command, linter, lefthookExtra);
|
|
3284
2727
|
if (target) {
|
|
3285
|
-
console.log(` ${
|
|
2728
|
+
console.log(` ${chalk12.green("\u2713")} ${target} \u2014 added ${linterName} lint check`);
|
|
3286
2729
|
}
|
|
3287
2730
|
return target;
|
|
3288
2731
|
}
|
|
@@ -3318,34 +2761,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3318
2761
|
return created;
|
|
3319
2762
|
}
|
|
3320
2763
|
|
|
3321
|
-
// src/commands/init.ts
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
}
|
|
3333
|
-
const
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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;
|
|
3337
2781
|
}
|
|
3338
|
-
console.log(
|
|
3339
|
-
`${chalk12.yellow("!")} viberails is already initialized.
|
|
3340
|
-
Run ${chalk12.cyan("viberails")} to review or edit the existing setup, ${chalk12.cyan("viberails sync")} to update generated files, or ${chalk12.cyan("viberails init --force")} to replace it.`
|
|
3341
|
-
);
|
|
3342
|
-
return;
|
|
3343
2782
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
2783
|
+
return filtered;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/commands/init-non-interactive.ts
|
|
2787
|
+
function getExemptedPackages(config) {
|
|
2788
|
+
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3346
2789
|
}
|
|
3347
2790
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3348
|
-
const s =
|
|
2791
|
+
const s = clack3.spinner();
|
|
3349
2792
|
s.start("Scanning project...");
|
|
3350
2793
|
const scanResult = await scan2(projectRoot);
|
|
3351
2794
|
const config = generateConfig(scanResult);
|
|
@@ -3360,11 +2803,11 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3360
2803
|
const exempted = getExemptedPackages(config);
|
|
3361
2804
|
if (exempted.length > 0) {
|
|
3362
2805
|
console.log(
|
|
3363
|
-
` ${
|
|
2806
|
+
` ${chalk13.dim("Auto-exempted from coverage:")} ${exempted.join(", ")} ${chalk13.dim("(types-only)")}`
|
|
3364
2807
|
);
|
|
3365
2808
|
}
|
|
3366
2809
|
if (config.packages.length > 1) {
|
|
3367
|
-
const bs =
|
|
2810
|
+
const bs = clack3.spinner();
|
|
3368
2811
|
bs.start("Building import graph...");
|
|
3369
2812
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3370
2813
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3397,14 +2840,14 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3397
2840
|
const hookManager = detectHookManager(projectRoot);
|
|
3398
2841
|
const hasHookManager = hookManager === "Lefthook" || hookManager === "Husky";
|
|
3399
2842
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3400
|
-
const ok =
|
|
2843
|
+
const ok = chalk13.green("\u2713");
|
|
3401
2844
|
const created = [
|
|
3402
2845
|
`${ok} ${path19.basename(configPath)}`,
|
|
3403
2846
|
`${ok} .viberails/context.md`,
|
|
3404
2847
|
`${ok} .viberails/scan-result.json`,
|
|
3405
2848
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
3406
2849
|
`${ok} CLAUDE.md \u2014 added @.viberails/context.md reference`,
|
|
3407
|
-
preCommitTarget ? `${ok} ${preCommitTarget}` : `${
|
|
2850
|
+
preCommitTarget ? `${ok} ${preCommitTarget}` : `${chalk13.yellow("!")} pre-commit hook skipped (install lefthook or husky)`,
|
|
3408
2851
|
actionTarget ? `${ok} ${actionTarget} \u2014 blocks PRs on violations` : ""
|
|
3409
2852
|
].filter(Boolean);
|
|
3410
2853
|
if (hasHookManager && isTypeScript) setupTypecheckHook(projectRoot, rootPkgPm);
|
|
@@ -3413,13 +2856,40 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3413
2856
|
Created:
|
|
3414
2857
|
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
3415
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
|
+
}
|
|
3416
2886
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3417
|
-
|
|
3418
|
-
const replacingExistingConfig =
|
|
3419
|
-
if (
|
|
3420
|
-
const action = await promptExistingConfigAction(
|
|
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));
|
|
3421
2891
|
if (action === "cancel") {
|
|
3422
|
-
|
|
2892
|
+
clack4.outro("Aborted. No files were written.");
|
|
3423
2893
|
return;
|
|
3424
2894
|
}
|
|
3425
2895
|
if (action === "edit") {
|
|
@@ -3428,45 +2898,51 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3428
2898
|
}
|
|
3429
2899
|
options.force = true;
|
|
3430
2900
|
}
|
|
3431
|
-
if (
|
|
2901
|
+
if (fs20.existsSync(configPath) && options.force) {
|
|
3432
2902
|
const replace = await confirmDangerous(
|
|
3433
|
-
`${
|
|
2903
|
+
`${path20.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3434
2904
|
);
|
|
3435
2905
|
if (!replace) {
|
|
3436
|
-
|
|
2906
|
+
clack4.outro("Aborted. No files were written.");
|
|
3437
2907
|
return;
|
|
3438
2908
|
}
|
|
3439
2909
|
}
|
|
3440
|
-
const s =
|
|
2910
|
+
const s = clack4.spinner();
|
|
3441
2911
|
s.start("Scanning project...");
|
|
3442
|
-
const scanResult = await
|
|
3443
|
-
const config =
|
|
2912
|
+
const scanResult = await scan3(projectRoot);
|
|
2913
|
+
const config = generateConfig2(scanResult);
|
|
3444
2914
|
s.stop("Scan complete");
|
|
3445
2915
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3446
|
-
|
|
2916
|
+
clack4.log.warn(
|
|
3447
2917
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3448
2918
|
);
|
|
3449
2919
|
}
|
|
3450
|
-
const exemptedPkgs =
|
|
2920
|
+
const exemptedPkgs = getExemptedPackages2(config);
|
|
3451
2921
|
let decision;
|
|
3452
2922
|
while (true) {
|
|
3453
2923
|
displayInitOverview(scanResult, config, exemptedPkgs);
|
|
3454
2924
|
const nextDecision = await promptInitDecision();
|
|
3455
2925
|
if (nextDecision === "review") {
|
|
3456
|
-
|
|
2926
|
+
clack4.note(formatScanResultsText(scanResult), "Detected details");
|
|
3457
2927
|
continue;
|
|
3458
2928
|
}
|
|
3459
2929
|
decision = nextDecision;
|
|
3460
2930
|
break;
|
|
3461
2931
|
}
|
|
3462
2932
|
if (decision === "customize") {
|
|
2933
|
+
const { resolveNamingDefault } = await import("./prompt-naming-default-AH54HEBC.js");
|
|
2934
|
+
await resolveNamingDefault(config, scanResult);
|
|
3463
2935
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3464
2936
|
const overrides = await promptRuleMenu({
|
|
3465
2937
|
maxFileLines: config.rules.maxFileLines,
|
|
2938
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3466
2939
|
testCoverage: config.rules.testCoverage,
|
|
3467
2940
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3468
2941
|
enforceNaming: config.rules.enforceNaming,
|
|
3469
2942
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2943
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2944
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2945
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3470
2946
|
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
3471
2947
|
coverageCommand: config.defaults?.coverage?.command,
|
|
3472
2948
|
packageOverrides: config.packages
|
|
@@ -3474,13 +2950,13 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3474
2950
|
applyRuleOverrides(config, overrides);
|
|
3475
2951
|
}
|
|
3476
2952
|
if (config.packages.length > 1) {
|
|
3477
|
-
|
|
2953
|
+
clack4.note(
|
|
3478
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.",
|
|
3479
2955
|
"Boundaries"
|
|
3480
2956
|
);
|
|
3481
|
-
const shouldInfer = await
|
|
2957
|
+
const shouldInfer = await confirm("Infer boundary rules from current import patterns?");
|
|
3482
2958
|
if (shouldInfer) {
|
|
3483
|
-
const bs =
|
|
2959
|
+
const bs = clack4.spinner();
|
|
3484
2960
|
bs.start("Building import graph...");
|
|
3485
2961
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3486
2962
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3501,7 +2977,7 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3501
2977
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3502
2978
|
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3503
2979
|
if (hasMissingPrereqs) {
|
|
3504
|
-
|
|
2980
|
+
clack4.log.info("Some dependencies are needed for full functionality.");
|
|
3505
2981
|
}
|
|
3506
2982
|
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3507
2983
|
if (prereqResult.disableCoverage) {
|
|
@@ -3516,48 +2992,48 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3516
2992
|
});
|
|
3517
2993
|
displaySetupPlan(config, integrations, {
|
|
3518
2994
|
replacingExistingConfig,
|
|
3519
|
-
configFile:
|
|
2995
|
+
configFile: path20.basename(configPath)
|
|
3520
2996
|
});
|
|
3521
|
-
const shouldWrite = await
|
|
2997
|
+
const shouldWrite = await confirm("Apply this setup?");
|
|
3522
2998
|
if (!shouldWrite) {
|
|
3523
|
-
|
|
2999
|
+
clack4.outro("Aborted. No files were written.");
|
|
3524
3000
|
return;
|
|
3525
3001
|
}
|
|
3526
|
-
const ws =
|
|
3002
|
+
const ws = clack4.spinner();
|
|
3527
3003
|
ws.start("Writing configuration...");
|
|
3528
|
-
const compacted =
|
|
3529
|
-
|
|
3004
|
+
const compacted = compactConfig4(config);
|
|
3005
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3530
3006
|
`);
|
|
3531
3007
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3532
3008
|
updateGitignore(projectRoot);
|
|
3533
3009
|
ws.stop("Configuration written");
|
|
3534
|
-
const ok =
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
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`);
|
|
3538
3014
|
setupSelectedIntegrations(projectRoot, integrations, {
|
|
3539
3015
|
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3540
3016
|
packageManager: rootPkgStack?.packageManager?.split("@")[0]
|
|
3541
3017
|
});
|
|
3542
|
-
|
|
3018
|
+
clack4.outro(
|
|
3543
3019
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3544
|
-
${
|
|
3020
|
+
${chalk14.dim("Tip: use")} ${chalk14.cyan("viberails check --enforce")} ${chalk14.dim("in CI to block PRs on violations.")}`
|
|
3545
3021
|
);
|
|
3546
3022
|
}
|
|
3547
3023
|
|
|
3548
3024
|
// src/commands/sync.ts
|
|
3549
|
-
import * as
|
|
3550
|
-
import * as
|
|
3551
|
-
import * as
|
|
3552
|
-
import { compactConfig as
|
|
3553
|
-
import { scan as
|
|
3554
|
-
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";
|
|
3555
3031
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3556
3032
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3557
3033
|
function loadPreviousStats(projectRoot) {
|
|
3558
|
-
const scanResultPath =
|
|
3034
|
+
const scanResultPath = path21.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3559
3035
|
try {
|
|
3560
|
-
const raw =
|
|
3036
|
+
const raw = fs21.readFileSync(scanResultPath, "utf-8");
|
|
3561
3037
|
const parsed = JSON.parse(raw);
|
|
3562
3038
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3563
3039
|
return parsed.statistics;
|
|
@@ -3574,17 +3050,17 @@ async function syncCommand(options, cwd) {
|
|
|
3574
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"
|
|
3575
3051
|
);
|
|
3576
3052
|
}
|
|
3577
|
-
const configPath =
|
|
3053
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE6);
|
|
3578
3054
|
const existing = await loadConfig5(configPath);
|
|
3579
3055
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3580
|
-
const s =
|
|
3056
|
+
const s = clack5.spinner();
|
|
3581
3057
|
s.start("Scanning project...");
|
|
3582
|
-
const scanResult = await
|
|
3058
|
+
const scanResult = await scan4(projectRoot);
|
|
3583
3059
|
s.stop("Scan complete");
|
|
3584
3060
|
const merged = mergeConfig2(existing, scanResult);
|
|
3585
|
-
const compacted =
|
|
3061
|
+
const compacted = compactConfig5(merged);
|
|
3586
3062
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3587
|
-
const rawDisk =
|
|
3063
|
+
const rawDisk = fs21.readFileSync(configPath, "utf-8").trim();
|
|
3588
3064
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3589
3065
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3590
3066
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3592,19 +3068,19 @@ async function syncCommand(options, cwd) {
|
|
|
3592
3068
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3593
3069
|
if (changes.length > 0 || statsDelta) {
|
|
3594
3070
|
console.log(`
|
|
3595
|
-
${
|
|
3071
|
+
${chalk15.bold("Changes:")}`);
|
|
3596
3072
|
for (const change of changes) {
|
|
3597
|
-
const icon = change.type === "removed" ?
|
|
3073
|
+
const icon = change.type === "removed" ? chalk15.red("-") : chalk15.green("+");
|
|
3598
3074
|
console.log(` ${icon} ${change.description}`);
|
|
3599
3075
|
}
|
|
3600
3076
|
if (statsDelta) {
|
|
3601
|
-
console.log(` ${
|
|
3077
|
+
console.log(` ${chalk15.dim(statsDelta)}`);
|
|
3602
3078
|
}
|
|
3603
3079
|
}
|
|
3604
3080
|
if (options?.interactive) {
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
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({
|
|
3608
3084
|
message: "How would you like to proceed?",
|
|
3609
3085
|
options: [
|
|
3610
3086
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -3614,47 +3090,51 @@ ${chalk13.bold("Changes:")}`);
|
|
|
3614
3090
|
});
|
|
3615
3091
|
assertNotCancelled(decision);
|
|
3616
3092
|
if (decision === "cancel") {
|
|
3617
|
-
|
|
3093
|
+
clack5.outro("Sync cancelled. No files were written.");
|
|
3618
3094
|
return;
|
|
3619
3095
|
}
|
|
3620
3096
|
if (decision === "customize") {
|
|
3621
3097
|
const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
|
|
3622
3098
|
const overrides = await promptRuleMenu({
|
|
3623
3099
|
maxFileLines: merged.rules.maxFileLines,
|
|
3100
|
+
maxTestFileLines: merged.rules.maxTestFileLines,
|
|
3624
3101
|
testCoverage: merged.rules.testCoverage,
|
|
3625
3102
|
enforceMissingTests: merged.rules.enforceMissingTests,
|
|
3626
3103
|
enforceNaming: merged.rules.enforceNaming,
|
|
3627
3104
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3105
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3106
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3107
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3628
3108
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3629
3109
|
coverageCommand: merged.defaults?.coverage?.command,
|
|
3630
3110
|
packageOverrides: merged.packages
|
|
3631
3111
|
});
|
|
3632
3112
|
applyRuleOverrides(merged, overrides);
|
|
3633
|
-
const recompacted =
|
|
3634
|
-
|
|
3113
|
+
const recompacted = compactConfig5(merged);
|
|
3114
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3635
3115
|
`);
|
|
3636
3116
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3637
|
-
|
|
3638
|
-
|
|
3117
|
+
clack5.log.success("Updated config with your customizations.");
|
|
3118
|
+
clack5.outro("Done! Run viberails check to verify.");
|
|
3639
3119
|
return;
|
|
3640
3120
|
}
|
|
3641
3121
|
}
|
|
3642
|
-
|
|
3122
|
+
fs21.writeFileSync(configPath, `${compactedJson}
|
|
3643
3123
|
`);
|
|
3644
3124
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3645
3125
|
console.log(`
|
|
3646
|
-
${
|
|
3126
|
+
${chalk15.bold("Synced:")}`);
|
|
3647
3127
|
if (configChanged) {
|
|
3648
|
-
console.log(` ${
|
|
3128
|
+
console.log(` ${chalk15.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3649
3129
|
} else {
|
|
3650
|
-
console.log(` ${
|
|
3130
|
+
console.log(` ${chalk15.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3651
3131
|
}
|
|
3652
|
-
console.log(` ${
|
|
3653
|
-
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`);
|
|
3654
3134
|
}
|
|
3655
3135
|
|
|
3656
3136
|
// src/index.ts
|
|
3657
|
-
var VERSION = "0.6.
|
|
3137
|
+
var VERSION = "0.6.5";
|
|
3658
3138
|
var program = new Command();
|
|
3659
3139
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3660
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) => {
|
|
@@ -3662,7 +3142,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3662
3142
|
await initCommand(options);
|
|
3663
3143
|
} catch (err) {
|
|
3664
3144
|
const message = err instanceof Error ? err.message : String(err);
|
|
3665
|
-
console.error(`${
|
|
3145
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3666
3146
|
process.exit(1);
|
|
3667
3147
|
}
|
|
3668
3148
|
});
|
|
@@ -3671,7 +3151,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3671
3151
|
await syncCommand(options);
|
|
3672
3152
|
} catch (err) {
|
|
3673
3153
|
const message = err instanceof Error ? err.message : String(err);
|
|
3674
|
-
console.error(`${
|
|
3154
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3675
3155
|
process.exit(1);
|
|
3676
3156
|
}
|
|
3677
3157
|
});
|
|
@@ -3680,7 +3160,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3680
3160
|
await configCommand(options);
|
|
3681
3161
|
} catch (err) {
|
|
3682
3162
|
const message = err instanceof Error ? err.message : String(err);
|
|
3683
|
-
console.error(`${
|
|
3163
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3684
3164
|
process.exit(1);
|
|
3685
3165
|
}
|
|
3686
3166
|
});
|
|
@@ -3701,7 +3181,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3701
3181
|
process.exit(exitCode);
|
|
3702
3182
|
} catch (err) {
|
|
3703
3183
|
const message = err instanceof Error ? err.message : String(err);
|
|
3704
|
-
console.error(`${
|
|
3184
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3705
3185
|
process.exit(1);
|
|
3706
3186
|
}
|
|
3707
3187
|
}
|
|
@@ -3712,7 +3192,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3712
3192
|
process.exit(exitCode);
|
|
3713
3193
|
} catch (err) {
|
|
3714
3194
|
const message = err instanceof Error ? err.message : String(err);
|
|
3715
|
-
console.error(`${
|
|
3195
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3716
3196
|
process.exit(1);
|
|
3717
3197
|
}
|
|
3718
3198
|
});
|
|
@@ -3721,7 +3201,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3721
3201
|
await boundariesCommand(options);
|
|
3722
3202
|
} catch (err) {
|
|
3723
3203
|
const message = err instanceof Error ? err.message : String(err);
|
|
3724
|
-
console.error(`${
|
|
3204
|
+
console.error(`${chalk16.red("Error:")} ${message}`);
|
|
3725
3205
|
process.exit(1);
|
|
3726
3206
|
}
|
|
3727
3207
|
});
|