viberails 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +847 -844
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1363 -432
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-XQKOK3FU.js +0 -821
- package/dist/chunk-XQKOK3FU.js.map +0 -1
- package/dist/prompt-naming-default-AH54HEBC.js +0 -57
- package/dist/prompt-naming-default-AH54HEBC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
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";
|
|
13
2
|
|
|
14
3
|
// src/index.ts
|
|
15
4
|
import chalk16 from "chalk";
|
|
@@ -38,6 +27,656 @@ function findProjectRoot(startDir) {
|
|
|
38
27
|
}
|
|
39
28
|
}
|
|
40
29
|
|
|
30
|
+
// src/utils/prompt.ts
|
|
31
|
+
import * as clack5 from "@clack/prompts";
|
|
32
|
+
|
|
33
|
+
// src/utils/prompt-rules.ts
|
|
34
|
+
import * as clack4 from "@clack/prompts";
|
|
35
|
+
|
|
36
|
+
// src/utils/get-root-package.ts
|
|
37
|
+
function getRootPackage(packages) {
|
|
38
|
+
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/utils/prompt-menu-handlers.ts
|
|
42
|
+
import * as clack3 from "@clack/prompts";
|
|
43
|
+
|
|
44
|
+
// src/utils/prompt-package-overrides.ts
|
|
45
|
+
import * as clack2 from "@clack/prompts";
|
|
46
|
+
|
|
47
|
+
// src/utils/prompt-constants.ts
|
|
48
|
+
var SENTINEL_DONE = "__done__";
|
|
49
|
+
var SENTINEL_CLEAR = "__clear__";
|
|
50
|
+
var SENTINEL_CUSTOM = "__custom__";
|
|
51
|
+
var SENTINEL_NONE = "__none__";
|
|
52
|
+
var SENTINEL_INHERIT = "__inherit__";
|
|
53
|
+
var SENTINEL_SKIP = "__skip__";
|
|
54
|
+
var HINT_NOT_SET = "not set";
|
|
55
|
+
var HINT_NO_OVERRIDES = "no overrides";
|
|
56
|
+
var HINT_AUTO_DETECT = "auto-detect";
|
|
57
|
+
|
|
58
|
+
// src/utils/prompt-submenus.ts
|
|
59
|
+
import * as clack from "@clack/prompts";
|
|
60
|
+
var FILE_NAMING_OPTIONS = [
|
|
61
|
+
{ value: "kebab-case", label: "kebab-case" },
|
|
62
|
+
{ value: "camelCase", label: "camelCase" },
|
|
63
|
+
{ value: "PascalCase", label: "PascalCase" },
|
|
64
|
+
{ value: "snake_case", label: "snake_case" }
|
|
65
|
+
];
|
|
66
|
+
var COMPONENT_NAMING_OPTIONS = [
|
|
67
|
+
{ value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
|
|
68
|
+
{ value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
|
|
69
|
+
];
|
|
70
|
+
var HOOK_NAMING_OPTIONS = [
|
|
71
|
+
{ value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
|
|
72
|
+
{ value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
|
|
73
|
+
];
|
|
74
|
+
async function promptFileLimitsMenu(state) {
|
|
75
|
+
while (true) {
|
|
76
|
+
const choice = await clack.select({
|
|
77
|
+
message: "File limits",
|
|
78
|
+
options: [
|
|
79
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
80
|
+
{
|
|
81
|
+
value: "maxTestFileLines",
|
|
82
|
+
label: "Max test file lines",
|
|
83
|
+
hint: state.maxTestFileLines > 0 ? String(state.maxTestFileLines) : "0 (unlimited)"
|
|
84
|
+
},
|
|
85
|
+
{ value: "back", label: "Back" }
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
89
|
+
if (choice === "maxFileLines") {
|
|
90
|
+
const result = await clack.text({
|
|
91
|
+
message: "Maximum lines per source file?",
|
|
92
|
+
initialValue: String(state.maxFileLines),
|
|
93
|
+
validate: (v) => {
|
|
94
|
+
if (typeof v !== "string") return "Enter a positive number";
|
|
95
|
+
const n = Number.parseInt(v, 10);
|
|
96
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (isCancelled(result)) continue;
|
|
100
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
101
|
+
}
|
|
102
|
+
if (choice === "maxTestFileLines") {
|
|
103
|
+
const result = await clack.text({
|
|
104
|
+
message: "Maximum lines per test file (0 to disable)?",
|
|
105
|
+
initialValue: String(state.maxTestFileLines),
|
|
106
|
+
validate: (v) => {
|
|
107
|
+
if (typeof v !== "string") return "Enter a number (0 or positive)";
|
|
108
|
+
const n = Number.parseInt(v, 10);
|
|
109
|
+
if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (isCancelled(result)) continue;
|
|
113
|
+
state.maxTestFileLines = Number.parseInt(result, 10);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function promptNamingMenu(state) {
|
|
118
|
+
while (true) {
|
|
119
|
+
const options = [
|
|
120
|
+
{
|
|
121
|
+
value: "enforceNaming",
|
|
122
|
+
label: "Enforce file naming",
|
|
123
|
+
hint: state.enforceNaming ? "yes" : "no"
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
if (state.enforceNaming) {
|
|
127
|
+
options.push({
|
|
128
|
+
value: "fileNaming",
|
|
129
|
+
label: "File naming convention",
|
|
130
|
+
hint: state.fileNamingValue ?? HINT_NOT_SET
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
options.push(
|
|
134
|
+
{
|
|
135
|
+
value: "componentNaming",
|
|
136
|
+
label: "Component naming",
|
|
137
|
+
hint: state.componentNaming ?? HINT_NOT_SET
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
value: "hookNaming",
|
|
141
|
+
label: "Hook naming",
|
|
142
|
+
hint: state.hookNaming ?? HINT_NOT_SET
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
value: "importAlias",
|
|
146
|
+
label: "Import alias",
|
|
147
|
+
hint: state.importAlias ?? HINT_NOT_SET
|
|
148
|
+
},
|
|
149
|
+
{ value: "back", label: "Back" }
|
|
150
|
+
);
|
|
151
|
+
const choice = await clack.select({ message: "Naming & conventions", options });
|
|
152
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
153
|
+
if (choice === "enforceNaming") {
|
|
154
|
+
const result = await clack.confirm({
|
|
155
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
156
|
+
initialValue: state.enforceNaming
|
|
157
|
+
});
|
|
158
|
+
if (isCancelled(result)) continue;
|
|
159
|
+
if (result && !state.fileNamingValue) {
|
|
160
|
+
const selected = await clack.select({
|
|
161
|
+
message: "Which file naming convention should be enforced?",
|
|
162
|
+
options: [...FILE_NAMING_OPTIONS]
|
|
163
|
+
});
|
|
164
|
+
if (isCancelled(selected)) continue;
|
|
165
|
+
state.fileNamingValue = selected;
|
|
166
|
+
}
|
|
167
|
+
state.enforceNaming = result;
|
|
168
|
+
}
|
|
169
|
+
if (choice === "fileNaming") {
|
|
170
|
+
const selected = await clack.select({
|
|
171
|
+
message: "Which file naming convention should be enforced?",
|
|
172
|
+
options: [...FILE_NAMING_OPTIONS],
|
|
173
|
+
initialValue: state.fileNamingValue
|
|
174
|
+
});
|
|
175
|
+
if (isCancelled(selected)) continue;
|
|
176
|
+
state.fileNamingValue = selected;
|
|
177
|
+
}
|
|
178
|
+
if (choice === "componentNaming") {
|
|
179
|
+
const selected = await clack.select({
|
|
180
|
+
message: "Component naming convention",
|
|
181
|
+
options: [
|
|
182
|
+
...COMPONENT_NAMING_OPTIONS,
|
|
183
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
184
|
+
],
|
|
185
|
+
initialValue: state.componentNaming ?? SENTINEL_CLEAR
|
|
186
|
+
});
|
|
187
|
+
if (isCancelled(selected)) continue;
|
|
188
|
+
state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
189
|
+
}
|
|
190
|
+
if (choice === "hookNaming") {
|
|
191
|
+
const selected = await clack.select({
|
|
192
|
+
message: "Hook naming convention",
|
|
193
|
+
options: [
|
|
194
|
+
...HOOK_NAMING_OPTIONS,
|
|
195
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
196
|
+
],
|
|
197
|
+
initialValue: state.hookNaming ?? SENTINEL_CLEAR
|
|
198
|
+
});
|
|
199
|
+
if (isCancelled(selected)) continue;
|
|
200
|
+
state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
201
|
+
}
|
|
202
|
+
if (choice === "importAlias") {
|
|
203
|
+
const selected = await clack.select({
|
|
204
|
+
message: "Import alias pattern",
|
|
205
|
+
options: [
|
|
206
|
+
{ value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
|
|
207
|
+
{ value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
|
|
208
|
+
{ value: SENTINEL_CUSTOM, label: "Custom..." },
|
|
209
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no alias)" }
|
|
210
|
+
],
|
|
211
|
+
initialValue: state.importAlias ?? SENTINEL_CLEAR
|
|
212
|
+
});
|
|
213
|
+
if (isCancelled(selected)) continue;
|
|
214
|
+
if (selected === SENTINEL_CLEAR) {
|
|
215
|
+
state.importAlias = void 0;
|
|
216
|
+
} else if (selected === SENTINEL_CUSTOM) {
|
|
217
|
+
const result = await clack.text({
|
|
218
|
+
message: "Custom import alias (e.g. #/*)?",
|
|
219
|
+
initialValue: state.importAlias ?? "",
|
|
220
|
+
placeholder: "e.g. #/*",
|
|
221
|
+
validate: (v) => {
|
|
222
|
+
if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
|
|
223
|
+
if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
|
|
224
|
+
return "Must match pattern like @/*, ~/*, or #src/*";
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (isCancelled(result)) continue;
|
|
228
|
+
state.importAlias = result.trim();
|
|
229
|
+
} else {
|
|
230
|
+
state.importAlias = selected;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function promptTestingMenu(state) {
|
|
236
|
+
while (true) {
|
|
237
|
+
const options = [
|
|
238
|
+
{
|
|
239
|
+
value: "enforceMissingTests",
|
|
240
|
+
label: "Enforce missing tests",
|
|
241
|
+
hint: state.enforceMissingTests ? "yes" : "no"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
value: "testCoverage",
|
|
245
|
+
label: "Test coverage target",
|
|
246
|
+
hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
|
|
247
|
+
}
|
|
248
|
+
];
|
|
249
|
+
if (state.testCoverage > 0) {
|
|
250
|
+
options.push(
|
|
251
|
+
{
|
|
252
|
+
value: "coverageSummaryPath",
|
|
253
|
+
label: "Coverage summary path",
|
|
254
|
+
hint: state.coverageSummaryPath
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
value: "coverageCommand",
|
|
258
|
+
label: "Coverage command",
|
|
259
|
+
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
options.push({ value: "back", label: "Back" });
|
|
264
|
+
const choice = await clack.select({ message: "Testing & coverage", options });
|
|
265
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
266
|
+
if (choice === "enforceMissingTests") {
|
|
267
|
+
const result = await clack.confirm({
|
|
268
|
+
message: "Require every source file to have a corresponding test file?",
|
|
269
|
+
initialValue: state.enforceMissingTests
|
|
270
|
+
});
|
|
271
|
+
if (isCancelled(result)) continue;
|
|
272
|
+
state.enforceMissingTests = result;
|
|
273
|
+
}
|
|
274
|
+
if (choice === "testCoverage") {
|
|
275
|
+
const result = await clack.text({
|
|
276
|
+
message: "Test coverage target (0 disables coverage checks)?",
|
|
277
|
+
initialValue: String(state.testCoverage),
|
|
278
|
+
validate: (v) => {
|
|
279
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
280
|
+
const n = Number.parseInt(v, 10);
|
|
281
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (isCancelled(result)) continue;
|
|
285
|
+
state.testCoverage = Number.parseInt(result, 10);
|
|
286
|
+
}
|
|
287
|
+
if (choice === "coverageSummaryPath") {
|
|
288
|
+
const result = await clack.text({
|
|
289
|
+
message: "Coverage summary path (relative to package root)?",
|
|
290
|
+
initialValue: state.coverageSummaryPath,
|
|
291
|
+
validate: (v) => {
|
|
292
|
+
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
if (isCancelled(result)) continue;
|
|
296
|
+
state.coverageSummaryPath = result.trim();
|
|
297
|
+
}
|
|
298
|
+
if (choice === "coverageCommand") {
|
|
299
|
+
const result = await clack.text({
|
|
300
|
+
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
301
|
+
initialValue: state.coverageCommand ?? "",
|
|
302
|
+
placeholder: "(auto-detect from package.json test runner)"
|
|
303
|
+
});
|
|
304
|
+
if (isCancelled(result)) continue;
|
|
305
|
+
const trimmed = result.trim();
|
|
306
|
+
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/utils/prompt-package-overrides.ts
|
|
312
|
+
function normalizePackageOverrides(packages) {
|
|
313
|
+
for (const pkg of packages) {
|
|
314
|
+
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
315
|
+
delete pkg.rules;
|
|
316
|
+
}
|
|
317
|
+
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
318
|
+
delete pkg.coverage;
|
|
319
|
+
}
|
|
320
|
+
if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
|
|
321
|
+
delete pkg.conventions;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return packages;
|
|
325
|
+
}
|
|
326
|
+
function packageOverrideHint(pkg, defaults) {
|
|
327
|
+
const tags = [];
|
|
328
|
+
if (pkg.conventions?.fileNaming && pkg.conventions.fileNaming !== defaults.fileNamingValue) {
|
|
329
|
+
tags.push(pkg.conventions.fileNaming);
|
|
330
|
+
}
|
|
331
|
+
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== defaults.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
332
|
+
tags.push(`${pkg.rules.maxFileLines} lines`);
|
|
333
|
+
}
|
|
334
|
+
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
335
|
+
const isExempt = coverage === 0;
|
|
336
|
+
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
337
|
+
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
338
|
+
if (isExempt) {
|
|
339
|
+
tags.push(isTypesOnly ? "exempt (types-only)" : "exempt");
|
|
340
|
+
} else if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== defaults.testCoverage) {
|
|
341
|
+
tags.push(`${coverage}%`);
|
|
342
|
+
}
|
|
343
|
+
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
344
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
345
|
+
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
346
|
+
if (hasSummaryOverride) tags.push("summary override");
|
|
347
|
+
if (hasCommandOverride) tags.push("command override");
|
|
348
|
+
return tags.length > 0 ? tags.join(", ") : HINT_NO_OVERRIDES;
|
|
349
|
+
}
|
|
350
|
+
async function promptPackageOverrides(packages, defaults) {
|
|
351
|
+
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
352
|
+
if (editablePackages.length === 0) return packages;
|
|
353
|
+
while (true) {
|
|
354
|
+
const selectedPath = await clack2.select({
|
|
355
|
+
message: "Select package to edit overrides",
|
|
356
|
+
options: [
|
|
357
|
+
...editablePackages.map((pkg) => ({
|
|
358
|
+
value: pkg.path,
|
|
359
|
+
label: `${pkg.path} (${pkg.name})`,
|
|
360
|
+
hint: packageOverrideHint(pkg, defaults)
|
|
361
|
+
})),
|
|
362
|
+
{ value: SENTINEL_DONE, label: "Done" }
|
|
363
|
+
]
|
|
364
|
+
});
|
|
365
|
+
if (isCancelled(selectedPath) || selectedPath === SENTINEL_DONE) break;
|
|
366
|
+
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
367
|
+
if (!target) continue;
|
|
368
|
+
await promptSinglePackageOverrides(target, defaults);
|
|
369
|
+
normalizePackageOverrides(editablePackages);
|
|
370
|
+
}
|
|
371
|
+
return normalizePackageOverrides(packages);
|
|
372
|
+
}
|
|
373
|
+
async function promptSinglePackageOverrides(target, defaults) {
|
|
374
|
+
while (true) {
|
|
375
|
+
const effectiveNaming = target.conventions?.fileNaming ?? defaults.fileNamingValue;
|
|
376
|
+
const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
|
|
377
|
+
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
378
|
+
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
379
|
+
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? HINT_AUTO_DETECT;
|
|
380
|
+
const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
|
|
381
|
+
const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
|
|
382
|
+
const namingHint = hasNamingOverride ? String(effectiveNaming) : `inherits: ${effectiveNaming ?? "not set"}`;
|
|
383
|
+
const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `inherits: ${effectiveMaxLines}`;
|
|
384
|
+
const choice = await clack2.select({
|
|
385
|
+
message: `Edit overrides for ${target.path}`,
|
|
386
|
+
options: [
|
|
387
|
+
{ value: "fileNaming", label: "File naming", hint: namingHint },
|
|
388
|
+
{ value: "maxFileLines", label: "Max file lines", hint: maxLinesHint },
|
|
389
|
+
{ value: "testCoverage", label: "Test coverage", hint: String(effectiveCoverage) },
|
|
390
|
+
{ value: "summaryPath", label: "Coverage summary path", hint: effectiveSummary },
|
|
391
|
+
{ value: "command", label: "Coverage command", hint: effectiveCommand },
|
|
392
|
+
{ value: "reset", label: "Reset all overrides for this package" },
|
|
393
|
+
{ value: "back", label: "Back to package list" }
|
|
394
|
+
]
|
|
395
|
+
});
|
|
396
|
+
if (isCancelled(choice) || choice === "back") break;
|
|
397
|
+
if (choice === "fileNaming") {
|
|
398
|
+
const selected = await clack2.select({
|
|
399
|
+
message: `File naming for ${target.path}`,
|
|
400
|
+
options: [
|
|
401
|
+
...FILE_NAMING_OPTIONS,
|
|
402
|
+
{ value: SENTINEL_NONE, label: "(none \u2014 exempt from checks)" },
|
|
403
|
+
{
|
|
404
|
+
value: SENTINEL_INHERIT,
|
|
405
|
+
label: `Inherit default${defaults.fileNamingValue ? ` (${defaults.fileNamingValue})` : ""}`
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
|
|
409
|
+
});
|
|
410
|
+
if (isCancelled(selected)) continue;
|
|
411
|
+
if (selected === SENTINEL_INHERIT) {
|
|
412
|
+
if (target.conventions) delete target.conventions.fileNaming;
|
|
413
|
+
} else if (selected === SENTINEL_NONE) {
|
|
414
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: "" };
|
|
415
|
+
} else {
|
|
416
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: selected };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (choice === "maxFileLines") {
|
|
420
|
+
const result = await clack2.text({
|
|
421
|
+
message: `Max file lines for ${target.path} (blank to inherit default)?`,
|
|
422
|
+
initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
|
|
423
|
+
placeholder: String(defaults.maxFileLines)
|
|
424
|
+
});
|
|
425
|
+
if (isCancelled(result)) continue;
|
|
426
|
+
const value = result.trim();
|
|
427
|
+
if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
|
|
428
|
+
if (target.rules) delete target.rules.maxFileLines;
|
|
429
|
+
} else {
|
|
430
|
+
target.rules = { ...target.rules ?? {}, maxFileLines: Number.parseInt(value, 10) };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (choice === "testCoverage") {
|
|
434
|
+
const result = await clack2.text({
|
|
435
|
+
message: "Package testCoverage (0 to exempt package)?",
|
|
436
|
+
initialValue: String(effectiveCoverage),
|
|
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
|
+
if (isCancelled(result)) continue;
|
|
444
|
+
const nextCoverage = Number.parseInt(result, 10);
|
|
445
|
+
if (nextCoverage === defaults.testCoverage) {
|
|
446
|
+
if (target.rules) delete target.rules.testCoverage;
|
|
447
|
+
} else {
|
|
448
|
+
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (choice === "summaryPath") {
|
|
452
|
+
const result = await clack2.text({
|
|
453
|
+
message: "Path to coverage summary file (blank to inherit default)?",
|
|
454
|
+
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
455
|
+
placeholder: defaults.coverageSummaryPath
|
|
456
|
+
});
|
|
457
|
+
if (isCancelled(result)) continue;
|
|
458
|
+
const value = result.trim();
|
|
459
|
+
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
460
|
+
if (target.coverage) delete target.coverage.summaryPath;
|
|
461
|
+
} else {
|
|
462
|
+
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (choice === "command") {
|
|
466
|
+
const result = await clack2.text({
|
|
467
|
+
message: "Coverage command (blank to auto-detect)?",
|
|
468
|
+
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
469
|
+
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
470
|
+
});
|
|
471
|
+
if (isCancelled(result)) continue;
|
|
472
|
+
const value = result.trim();
|
|
473
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
474
|
+
if (value.length === 0 || value === defaultCommand) {
|
|
475
|
+
if (target.coverage) delete target.coverage.command;
|
|
476
|
+
} else {
|
|
477
|
+
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (choice === "reset") {
|
|
481
|
+
if (target.rules) {
|
|
482
|
+
delete target.rules.testCoverage;
|
|
483
|
+
delete target.rules.maxFileLines;
|
|
484
|
+
}
|
|
485
|
+
delete target.coverage;
|
|
486
|
+
delete target.conventions;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/utils/prompt-menu-handlers.ts
|
|
492
|
+
function getPackageDiffs(pkg, root) {
|
|
493
|
+
const diffs = [];
|
|
494
|
+
const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
|
|
495
|
+
for (const key of convKeys) {
|
|
496
|
+
if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
|
|
497
|
+
diffs.push(`${key}: ${pkg.conventions[key]}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const stackKeys = [
|
|
501
|
+
"framework",
|
|
502
|
+
"language",
|
|
503
|
+
"styling",
|
|
504
|
+
"backend",
|
|
505
|
+
"orm",
|
|
506
|
+
"linter",
|
|
507
|
+
"formatter",
|
|
508
|
+
"testRunner",
|
|
509
|
+
"packageManager"
|
|
510
|
+
];
|
|
511
|
+
for (const key of stackKeys) {
|
|
512
|
+
if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
|
|
513
|
+
diffs.push(`${key}: ${pkg.stack[key]}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
517
|
+
diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
|
|
518
|
+
}
|
|
519
|
+
if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
|
|
520
|
+
diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
|
|
521
|
+
}
|
|
522
|
+
if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
|
|
523
|
+
diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
|
|
524
|
+
}
|
|
525
|
+
if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
|
|
526
|
+
diffs.push("coverage.command: (override)");
|
|
527
|
+
}
|
|
528
|
+
return diffs;
|
|
529
|
+
}
|
|
530
|
+
function buildMenuOptions(state, packageCount) {
|
|
531
|
+
const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
|
|
532
|
+
const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
|
|
533
|
+
const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
|
|
534
|
+
const options = [
|
|
535
|
+
{ value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
|
|
536
|
+
{ value: "naming", label: "Naming & conventions", hint: namingHint },
|
|
537
|
+
{ value: "testing", label: "Testing & coverage", hint: testingHint }
|
|
538
|
+
];
|
|
539
|
+
if (packageCount > 0) {
|
|
540
|
+
options.push({
|
|
541
|
+
value: "packageOverrides",
|
|
542
|
+
label: "Per-package overrides",
|
|
543
|
+
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
options.push(
|
|
547
|
+
{ value: "reset", label: "Reset all to detected defaults" },
|
|
548
|
+
{ value: "done", label: "Done" }
|
|
549
|
+
);
|
|
550
|
+
return options;
|
|
551
|
+
}
|
|
552
|
+
function clonePackages(packages) {
|
|
553
|
+
return packages ? structuredClone(packages) : void 0;
|
|
554
|
+
}
|
|
555
|
+
async function handleMenuChoice(choice, state, defaults, root) {
|
|
556
|
+
if (choice === "reset") {
|
|
557
|
+
state.maxFileLines = defaults.maxFileLines;
|
|
558
|
+
state.maxTestFileLines = defaults.maxTestFileLines;
|
|
559
|
+
state.testCoverage = defaults.testCoverage;
|
|
560
|
+
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
561
|
+
state.enforceNaming = defaults.enforceNaming;
|
|
562
|
+
state.fileNamingValue = defaults.fileNamingValue;
|
|
563
|
+
state.componentNaming = defaults.componentNaming;
|
|
564
|
+
state.hookNaming = defaults.hookNaming;
|
|
565
|
+
state.importAlias = defaults.importAlias;
|
|
566
|
+
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
567
|
+
state.coverageCommand = defaults.coverageCommand;
|
|
568
|
+
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
569
|
+
clack3.log.info("Reset all rules to detected defaults.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (choice === "fileLimits") {
|
|
573
|
+
await promptFileLimitsMenu(state);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (choice === "naming") {
|
|
577
|
+
await promptNamingMenu(state);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (choice === "testing") {
|
|
581
|
+
await promptTestingMenu(state);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (choice === "packageOverrides") {
|
|
585
|
+
if (state.packageOverrides) {
|
|
586
|
+
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
587
|
+
state.packageOverrides = await promptPackageOverrides(state.packageOverrides, {
|
|
588
|
+
fileNamingValue: state.fileNamingValue,
|
|
589
|
+
maxFileLines: state.maxFileLines,
|
|
590
|
+
testCoverage: state.testCoverage,
|
|
591
|
+
coverageSummaryPath: state.coverageSummaryPath,
|
|
592
|
+
coverageCommand: state.coverageCommand
|
|
593
|
+
});
|
|
594
|
+
const lines = packageDiffs.map((entry) => `${entry.pkg.path}
|
|
595
|
+
${entry.diffs.join(", ")}`);
|
|
596
|
+
if (lines.length > 0) {
|
|
597
|
+
clack3.note(lines.join("\n\n"), "Existing package differences");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/utils/prompt-rules.ts
|
|
605
|
+
async function promptRuleMenu(defaults) {
|
|
606
|
+
const state = {
|
|
607
|
+
...defaults,
|
|
608
|
+
packageOverrides: clonePackages(defaults.packageOverrides)
|
|
609
|
+
};
|
|
610
|
+
const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
|
|
611
|
+
const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
|
|
612
|
+
while (true) {
|
|
613
|
+
const options = buildMenuOptions(state, packageCount);
|
|
614
|
+
const choice = await clack4.select({ message: "Customize rules", options });
|
|
615
|
+
assertNotCancelled(choice);
|
|
616
|
+
if (choice === "done") break;
|
|
617
|
+
await handleMenuChoice(choice, state, defaults, root);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
maxFileLines: state.maxFileLines,
|
|
621
|
+
maxTestFileLines: state.maxTestFileLines,
|
|
622
|
+
testCoverage: state.testCoverage,
|
|
623
|
+
enforceMissingTests: state.enforceMissingTests,
|
|
624
|
+
enforceNaming: state.enforceNaming,
|
|
625
|
+
fileNamingValue: state.fileNamingValue,
|
|
626
|
+
componentNaming: state.componentNaming,
|
|
627
|
+
hookNaming: state.hookNaming,
|
|
628
|
+
importAlias: state.importAlias,
|
|
629
|
+
coverageSummaryPath: state.coverageSummaryPath,
|
|
630
|
+
coverageCommand: state.coverageCommand,
|
|
631
|
+
packageOverrides: state.packageOverrides
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/utils/prompt.ts
|
|
636
|
+
function assertNotCancelled(value) {
|
|
637
|
+
if (clack5.isCancel(value)) {
|
|
638
|
+
clack5.cancel("Setup cancelled.");
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function isCancelled(value) {
|
|
643
|
+
return clack5.isCancel(value);
|
|
644
|
+
}
|
|
645
|
+
async function confirm3(message) {
|
|
646
|
+
const result = await clack5.confirm({ message, initialValue: true });
|
|
647
|
+
assertNotCancelled(result);
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
async function confirmDangerous(message) {
|
|
651
|
+
const result = await clack5.confirm({ message, initialValue: false });
|
|
652
|
+
assertNotCancelled(result);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
async function promptExistingConfigAction(configFile) {
|
|
656
|
+
const result = await clack5.select({
|
|
657
|
+
message: `${configFile} already exists. What do you want to do?`,
|
|
658
|
+
options: [
|
|
659
|
+
{
|
|
660
|
+
value: "edit",
|
|
661
|
+
label: "Edit existing config",
|
|
662
|
+
hint: "open the current rules and save updates in place"
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
value: "replace",
|
|
666
|
+
label: "Replace with a fresh scan",
|
|
667
|
+
hint: "re-scan the project and overwrite the current config"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
value: "cancel",
|
|
671
|
+
label: "Cancel",
|
|
672
|
+
hint: "leave the current setup unchanged"
|
|
673
|
+
}
|
|
674
|
+
]
|
|
675
|
+
});
|
|
676
|
+
assertNotCancelled(result);
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
|
|
41
680
|
// src/utils/resolve-workspace-packages.ts
|
|
42
681
|
import * as fs2 from "fs";
|
|
43
682
|
import * as path2 from "path";
|
|
@@ -142,7 +781,7 @@ ${chalk.bold("Inferred boundary rules:")}
|
|
|
142
781
|
console.log(`
|
|
143
782
|
${totalRules} denied`);
|
|
144
783
|
console.log("");
|
|
145
|
-
const shouldSave = await
|
|
784
|
+
const shouldSave = await confirm3("Save to viberails.config.json?");
|
|
146
785
|
if (shouldSave) {
|
|
147
786
|
config.boundaries = inferred;
|
|
148
787
|
config.rules.enforceBoundaries = true;
|
|
@@ -741,9 +1380,9 @@ async function checkCommand(options, cwd) {
|
|
|
741
1380
|
}
|
|
742
1381
|
const violations = [];
|
|
743
1382
|
const severity = options.enforce ? "error" : "warn";
|
|
744
|
-
const
|
|
1383
|
+
const log9 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
|
|
745
1384
|
};
|
|
746
|
-
|
|
1385
|
+
log9(" Checking files...");
|
|
747
1386
|
for (const file of filesToCheck) {
|
|
748
1387
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
749
1388
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -776,9 +1415,9 @@ async function checkCommand(options, cwd) {
|
|
|
776
1415
|
}
|
|
777
1416
|
}
|
|
778
1417
|
}
|
|
779
|
-
|
|
1418
|
+
log9(" done\n");
|
|
780
1419
|
if (!options.files) {
|
|
781
|
-
|
|
1420
|
+
log9(" Checking missing tests...");
|
|
782
1421
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
783
1422
|
if (options.staged) {
|
|
784
1423
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -791,14 +1430,14 @@ async function checkCommand(options, cwd) {
|
|
|
791
1430
|
} else {
|
|
792
1431
|
violations.push(...testViolations);
|
|
793
1432
|
}
|
|
794
|
-
|
|
1433
|
+
log9(" done\n");
|
|
795
1434
|
}
|
|
796
1435
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
797
|
-
|
|
1436
|
+
log9(" Running test coverage...\n");
|
|
798
1437
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
799
1438
|
staged: options.staged,
|
|
800
1439
|
enforce: options.enforce,
|
|
801
|
-
onProgress: (pkg) =>
|
|
1440
|
+
onProgress: (pkg) => log9(` Coverage: ${pkg}...
|
|
802
1441
|
`)
|
|
803
1442
|
});
|
|
804
1443
|
violations.push(...coverageViolations);
|
|
@@ -823,7 +1462,7 @@ async function checkCommand(options, cwd) {
|
|
|
823
1462
|
severity
|
|
824
1463
|
});
|
|
825
1464
|
}
|
|
826
|
-
|
|
1465
|
+
log9(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
827
1466
|
`);
|
|
828
1467
|
}
|
|
829
1468
|
if (options.format === "json") {
|
|
@@ -899,7 +1538,7 @@ async function hookCheckCommand(cwd) {
|
|
|
899
1538
|
// src/commands/config.ts
|
|
900
1539
|
import * as fs10 from "fs";
|
|
901
1540
|
import * as path9 from "path";
|
|
902
|
-
import * as
|
|
1541
|
+
import * as clack6 from "@clack/prompts";
|
|
903
1542
|
import { compactConfig as compactConfig2, loadConfig as loadConfig3, mergeConfig } from "@viberails/config";
|
|
904
1543
|
import { scan } from "@viberails/scanner";
|
|
905
1544
|
import chalk6 from "chalk";
|
|
@@ -1544,11 +2183,11 @@ async function configCommand(options, cwd) {
|
|
|
1544
2183
|
return;
|
|
1545
2184
|
}
|
|
1546
2185
|
if (!options.suppressIntro) {
|
|
1547
|
-
|
|
2186
|
+
clack6.intro("viberails config");
|
|
1548
2187
|
}
|
|
1549
2188
|
const config = await loadConfig3(configPath);
|
|
1550
2189
|
let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
|
|
1551
|
-
|
|
2190
|
+
clack6.note(formatRulesText(config).join("\n"), "Current rules");
|
|
1552
2191
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1553
2192
|
const overrides = await promptRuleMenu({
|
|
1554
2193
|
maxFileLines: config.rules.maxFileLines,
|
|
@@ -1566,9 +2205,9 @@ async function configCommand(options, cwd) {
|
|
|
1566
2205
|
});
|
|
1567
2206
|
applyRuleOverrides(config, overrides);
|
|
1568
2207
|
if (options.rescan && config.packages.length > 1) {
|
|
1569
|
-
const shouldInfer = await
|
|
2208
|
+
const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
|
|
1570
2209
|
if (shouldInfer) {
|
|
1571
|
-
const bs =
|
|
2210
|
+
const bs = clack6.spinner();
|
|
1572
2211
|
bs.start("Building import graph...");
|
|
1573
2212
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
1574
2213
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -1584,31 +2223,31 @@ async function configCommand(options, cwd) {
|
|
|
1584
2223
|
}
|
|
1585
2224
|
}
|
|
1586
2225
|
}
|
|
1587
|
-
const shouldWrite = await
|
|
2226
|
+
const shouldWrite = await confirm3("Save updated configuration?");
|
|
1588
2227
|
if (!shouldWrite) {
|
|
1589
|
-
|
|
2228
|
+
clack6.outro("No changes written.");
|
|
1590
2229
|
return;
|
|
1591
2230
|
}
|
|
1592
2231
|
const compacted = compactConfig2(config);
|
|
1593
2232
|
fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
1594
2233
|
`);
|
|
1595
2234
|
if (!scanResult) {
|
|
1596
|
-
const s =
|
|
2235
|
+
const s = clack6.spinner();
|
|
1597
2236
|
s.start("Scanning for context generation...");
|
|
1598
2237
|
scanResult = await scan(projectRoot);
|
|
1599
2238
|
s.stop("Scan complete");
|
|
1600
2239
|
}
|
|
1601
2240
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
1602
|
-
|
|
2241
|
+
clack6.log.success(
|
|
1603
2242
|
`Updated:
|
|
1604
2243
|
${CONFIG_FILE3}
|
|
1605
2244
|
.viberails/context.md
|
|
1606
2245
|
.viberails/scan-result.json`
|
|
1607
2246
|
);
|
|
1608
|
-
|
|
2247
|
+
clack6.outro("Done! Run viberails check to verify.");
|
|
1609
2248
|
}
|
|
1610
2249
|
async function rescanAndMerge(projectRoot, config) {
|
|
1611
|
-
const s =
|
|
2250
|
+
const s = clack6.spinner();
|
|
1612
2251
|
s.start("Re-scanning project...");
|
|
1613
2252
|
const scanResult = await scan(projectRoot);
|
|
1614
2253
|
const merged = mergeConfig(config, scanResult);
|
|
@@ -1619,9 +2258,9 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
1619
2258
|
const icon = c.type === "removed" ? "-" : "+";
|
|
1620
2259
|
return `${icon} ${c.description}`;
|
|
1621
2260
|
}).join("\n");
|
|
1622
|
-
|
|
2261
|
+
clack6.note(changeLines, "Changes detected");
|
|
1623
2262
|
} else {
|
|
1624
|
-
|
|
2263
|
+
clack6.log.info("No new changes detected from scan.");
|
|
1625
2264
|
}
|
|
1626
2265
|
Object.assign(config, merged);
|
|
1627
2266
|
return scanResult;
|
|
@@ -2115,159 +2754,42 @@ ${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
|
2115
2754
|
}
|
|
2116
2755
|
|
|
2117
2756
|
// src/commands/init.ts
|
|
2118
|
-
import * as
|
|
2119
|
-
import * as
|
|
2120
|
-
import * as
|
|
2757
|
+
import * as fs21 from "fs";
|
|
2758
|
+
import * as path21 from "path";
|
|
2759
|
+
import * as clack13 from "@clack/prompts";
|
|
2121
2760
|
import { compactConfig as compactConfig4, generateConfig as generateConfig2 } from "@viberails/config";
|
|
2122
2761
|
import { scan as scan3 } from "@viberails/scanner";
|
|
2123
2762
|
import chalk14 from "chalk";
|
|
2124
2763
|
|
|
2125
|
-
// src/
|
|
2126
|
-
import
|
|
2764
|
+
// src/utils/check-prerequisites.ts
|
|
2765
|
+
import * as fs14 from "fs";
|
|
2766
|
+
import * as path14 from "path";
|
|
2767
|
+
import * as clack7 from "@clack/prompts";
|
|
2127
2768
|
import chalk9 from "chalk";
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
|
2769
|
+
|
|
2770
|
+
// src/utils/spawn-async.ts
|
|
2771
|
+
import { spawn } from "child_process";
|
|
2772
|
+
function spawnAsync(command, cwd) {
|
|
2773
|
+
return new Promise((resolve4) => {
|
|
2774
|
+
const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
|
|
2775
|
+
let stdout = "";
|
|
2776
|
+
let stderr = "";
|
|
2777
|
+
child.stdout.on("data", (d) => {
|
|
2778
|
+
stdout += d.toString();
|
|
2779
|
+
});
|
|
2780
|
+
child.stderr.on("data", (d) => {
|
|
2781
|
+
stderr += d.toString();
|
|
2782
|
+
});
|
|
2783
|
+
child.on("close", (status) => {
|
|
2784
|
+
resolve4({ status, stdout, stderr });
|
|
2785
|
+
});
|
|
2786
|
+
child.on("error", () => {
|
|
2787
|
+
resolve4({ status: 1, stdout, stderr });
|
|
2788
|
+
});
|
|
2251
2789
|
});
|
|
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
2790
|
}
|
|
2265
2791
|
|
|
2266
2792
|
// src/utils/check-prerequisites.ts
|
|
2267
|
-
import * as fs14 from "fs";
|
|
2268
|
-
import * as path14 from "path";
|
|
2269
|
-
import * as clack2 from "@clack/prompts";
|
|
2270
|
-
import chalk10 from "chalk";
|
|
2271
2793
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2272
2794
|
const pm = scanResult.stack.packageManager.name;
|
|
2273
2795
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -2298,113 +2820,572 @@ function displayMissingPrereqs(prereqs) {
|
|
|
2298
2820
|
const missing = prereqs.filter((p) => !p.installed);
|
|
2299
2821
|
for (const m of missing) {
|
|
2300
2822
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2301
|
-
console.log(` ${
|
|
2823
|
+
console.log(` ${chalk9.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2302
2824
|
if (m.installCommand) {
|
|
2303
|
-
console.log(` Install: ${
|
|
2825
|
+
console.log(` Install: ${chalk9.cyan(m.installCommand)}`);
|
|
2304
2826
|
}
|
|
2305
2827
|
}
|
|
2306
2828
|
}
|
|
2307
|
-
|
|
2308
|
-
const missing = prereqs.
|
|
2309
|
-
if (missing
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2829
|
+
function planCoverageInstall(prereqs) {
|
|
2830
|
+
const missing = prereqs.find((p) => !p.installed && p.installCommand);
|
|
2831
|
+
if (!missing?.installCommand) return void 0;
|
|
2832
|
+
return {
|
|
2833
|
+
label: missing.label,
|
|
2834
|
+
command: missing.installCommand
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
function hasDependency(projectRoot, name) {
|
|
2838
|
+
try {
|
|
2839
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
2840
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
2841
|
+
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2842
|
+
} catch {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// src/utils/deferred-install.ts
|
|
2848
|
+
import * as clack8 from "@clack/prompts";
|
|
2849
|
+
async function executeDeferredInstalls(projectRoot, installs) {
|
|
2850
|
+
if (installs.length === 0) return 0;
|
|
2851
|
+
let successCount = 0;
|
|
2852
|
+
for (const install of installs) {
|
|
2853
|
+
const s = clack8.spinner();
|
|
2854
|
+
s.start(`Installing ${install.label}...`);
|
|
2855
|
+
const result = await spawnAsync(install.command, projectRoot);
|
|
2856
|
+
if (result.status === 0) {
|
|
2857
|
+
s.stop(`Installed ${install.label}`);
|
|
2858
|
+
install.onSuccess?.();
|
|
2859
|
+
successCount++;
|
|
2860
|
+
} else {
|
|
2861
|
+
s.stop(`Failed to install ${install.label}`);
|
|
2862
|
+
clack8.log.warn(`Install manually: ${install.command}`);
|
|
2863
|
+
install.onFailure?.();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return successCount;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// src/utils/prompt-main-menu.ts
|
|
2870
|
+
import * as clack11 from "@clack/prompts";
|
|
2871
|
+
|
|
2872
|
+
// src/utils/prompt-main-menu-handlers.ts
|
|
2873
|
+
import * as clack10 from "@clack/prompts";
|
|
2874
|
+
|
|
2875
|
+
// src/utils/prompt-integrations.ts
|
|
2876
|
+
import * as fs15 from "fs";
|
|
2877
|
+
import * as path15 from "path";
|
|
2878
|
+
import * as clack9 from "@clack/prompts";
|
|
2879
|
+
function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
2880
|
+
if (pm === "yarn") return "yarn add -D lefthook";
|
|
2881
|
+
if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
|
|
2882
|
+
if (pm === "npm") return "npm install -D lefthook";
|
|
2883
|
+
return `${pm} add -D lefthook`;
|
|
2884
|
+
}
|
|
2885
|
+
async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
|
|
2886
|
+
const options = [];
|
|
2887
|
+
const needsLefthook = !hookManager;
|
|
2888
|
+
if (needsLefthook) {
|
|
2889
|
+
const pm = packageManager ?? "npm";
|
|
2890
|
+
options.push({
|
|
2891
|
+
value: "installLefthook",
|
|
2892
|
+
label: "Install Lefthook",
|
|
2893
|
+
hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2897
|
+
const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
|
|
2898
|
+
options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
|
|
2899
|
+
if (tools?.isTypeScript) {
|
|
2900
|
+
options.push({
|
|
2901
|
+
value: "typecheck",
|
|
2902
|
+
label: "Typecheck (tsc --noEmit)",
|
|
2903
|
+
hint: "pre-commit hook + CI check"
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
if (tools?.linter) {
|
|
2907
|
+
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
2908
|
+
options.push({
|
|
2909
|
+
value: "lint",
|
|
2910
|
+
label: `Lint check (${linterName})`,
|
|
2911
|
+
hint: "pre-commit hook + CI check"
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
options.push(
|
|
2915
|
+
{
|
|
2916
|
+
value: "claude",
|
|
2917
|
+
label: "Claude Code hook",
|
|
2918
|
+
hint: "checks files when Claude edits them"
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
value: "claudeMd",
|
|
2922
|
+
label: "CLAUDE.md reference",
|
|
2923
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
value: "githubAction",
|
|
2927
|
+
label: "GitHub Actions workflow",
|
|
2928
|
+
hint: "blocks PRs that fail viberails check"
|
|
2929
|
+
}
|
|
2930
|
+
);
|
|
2931
|
+
const initialValues = options.map((o) => o.value);
|
|
2932
|
+
const result = await clack9.multiselect({
|
|
2933
|
+
message: "Integrations",
|
|
2934
|
+
options,
|
|
2935
|
+
initialValues,
|
|
2936
|
+
required: false
|
|
2937
|
+
});
|
|
2938
|
+
assertNotCancelled(result);
|
|
2939
|
+
let lefthookInstall;
|
|
2940
|
+
if (needsLefthook && result.includes("installLefthook")) {
|
|
2941
|
+
const pm = packageManager ?? "npm";
|
|
2942
|
+
lefthookInstall = {
|
|
2943
|
+
label: "Lefthook",
|
|
2944
|
+
command: buildLefthookInstallCommand(pm, isWorkspace),
|
|
2945
|
+
onSuccess: projectRoot ? () => {
|
|
2946
|
+
const ymlPath = path15.join(projectRoot, "lefthook.yml");
|
|
2947
|
+
if (!fs15.existsSync(ymlPath)) {
|
|
2948
|
+
fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
|
|
2949
|
+
}
|
|
2950
|
+
} : void 0
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
return {
|
|
2954
|
+
choice: {
|
|
2955
|
+
preCommitHook: result.includes("preCommit"),
|
|
2956
|
+
claudeCodeHook: result.includes("claude"),
|
|
2957
|
+
claudeMdRef: result.includes("claudeMd"),
|
|
2958
|
+
githubAction: result.includes("githubAction"),
|
|
2959
|
+
typecheckHook: result.includes("typecheck"),
|
|
2960
|
+
lintHook: result.includes("lint")
|
|
2961
|
+
},
|
|
2962
|
+
lefthookInstall
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/utils/prompt-main-menu-handlers.ts
|
|
2967
|
+
async function handleAdvancedNaming(config) {
|
|
2968
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2969
|
+
const state = {
|
|
2970
|
+
maxFileLines: config.rules.maxFileLines,
|
|
2971
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2972
|
+
testCoverage: config.rules.testCoverage,
|
|
2973
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2974
|
+
enforceNaming: config.rules.enforceNaming,
|
|
2975
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2976
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2977
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2978
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
2979
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
2980
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
2981
|
+
};
|
|
2982
|
+
await promptNamingMenu(state);
|
|
2983
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2984
|
+
config.rules.enforceNaming = state.enforceNaming;
|
|
2985
|
+
if (state.fileNamingValue) {
|
|
2986
|
+
rootPkg.conventions.fileNaming = state.fileNamingValue;
|
|
2987
|
+
} else {
|
|
2988
|
+
delete rootPkg.conventions.fileNaming;
|
|
2989
|
+
}
|
|
2990
|
+
rootPkg.conventions.componentNaming = state.componentNaming || void 0;
|
|
2991
|
+
rootPkg.conventions.hookNaming = state.hookNaming || void 0;
|
|
2992
|
+
rootPkg.conventions.importAlias = state.importAlias || void 0;
|
|
2993
|
+
}
|
|
2994
|
+
async function handleFileNaming(config, scanResult) {
|
|
2995
|
+
const isMonorepo = config.packages.length > 1;
|
|
2996
|
+
if (isMonorepo) {
|
|
2997
|
+
const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
|
|
2998
|
+
path: p.relativePath,
|
|
2999
|
+
naming: p.conventions.fileNaming
|
|
3000
|
+
}));
|
|
3001
|
+
if (pkgData.length > 0) {
|
|
3002
|
+
const lines = pkgData.map(
|
|
3003
|
+
(p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
|
|
3004
|
+
);
|
|
3005
|
+
clack10.note(lines.join("\n"), "Per-package file naming detected");
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
|
|
3009
|
+
if (isMonorepo) {
|
|
3010
|
+
const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
|
|
3011
|
+
const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
|
|
3012
|
+
return { value: opt.value, label: opt.label, hint };
|
|
3013
|
+
}
|
|
3014
|
+
return { value: opt.value, label: opt.label };
|
|
3015
|
+
});
|
|
3016
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3017
|
+
const selected = await clack10.select({
|
|
3018
|
+
message: isMonorepo ? "Default file naming convention" : "File naming convention",
|
|
3019
|
+
options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
|
|
3020
|
+
initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
|
|
3021
|
+
});
|
|
3022
|
+
if (isCancelled(selected)) return;
|
|
3023
|
+
if (selected === SENTINEL_SKIP) {
|
|
3024
|
+
config.rules.enforceNaming = false;
|
|
3025
|
+
if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
|
|
3026
|
+
} else {
|
|
3027
|
+
config.rules.enforceNaming = true;
|
|
3028
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3029
|
+
rootPkg.conventions.fileNaming = selected;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
async function handleMissingTests(config) {
|
|
3033
|
+
const result = await clack10.confirm({
|
|
3034
|
+
message: "Require every source file to have a test file?",
|
|
3035
|
+
initialValue: config.rules.enforceMissingTests
|
|
3036
|
+
});
|
|
3037
|
+
if (isCancelled(result)) return;
|
|
3038
|
+
config.rules.enforceMissingTests = result;
|
|
3039
|
+
}
|
|
3040
|
+
async function handleCoverage(config, state, opts) {
|
|
3041
|
+
if (!opts.hasTestRunner) {
|
|
3042
|
+
clack10.note(
|
|
3043
|
+
"No test runner (vitest, jest, etc.) was detected.\nInstall one, then re-run viberails init to configure coverage.",
|
|
3044
|
+
"Coverage inactive"
|
|
3045
|
+
);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
const planned = planCoverageInstall(opts.coveragePrereqs);
|
|
3049
|
+
if (planned) {
|
|
3050
|
+
const choice = await clack10.select({
|
|
3051
|
+
message: `${planned.label} is not installed. Needed for coverage checks.`,
|
|
2323
3052
|
options: [
|
|
2324
3053
|
{
|
|
2325
3054
|
value: "install",
|
|
2326
|
-
label: "Install
|
|
2327
|
-
hint:
|
|
2328
|
-
},
|
|
2329
|
-
{
|
|
2330
|
-
value: "disable",
|
|
2331
|
-
label: "Disable coverage checks",
|
|
2332
|
-
hint: "missing-test checks still stay active"
|
|
3055
|
+
label: "Install (after final confirmation)",
|
|
3056
|
+
hint: planned.command
|
|
2333
3057
|
},
|
|
3058
|
+
{ value: "disable", label: "Disable coverage checks" },
|
|
2334
3059
|
{
|
|
2335
3060
|
value: "skip",
|
|
2336
3061
|
label: "Skip for now",
|
|
2337
|
-
hint: `install later: ${
|
|
3062
|
+
hint: `install later: ${planned.command}`
|
|
2338
3063
|
}
|
|
2339
3064
|
]
|
|
2340
3065
|
});
|
|
2341
|
-
|
|
3066
|
+
if (isCancelled(choice)) return;
|
|
3067
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
|
|
2342
3068
|
if (choice === "install") {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
is.stop(`Installed ${m.label}`);
|
|
2348
|
-
} else {
|
|
2349
|
-
is.stop(`Failed to install ${m.label}`);
|
|
2350
|
-
clack2.log.warn(
|
|
2351
|
-
`Install manually: ${m.installCommand}
|
|
2352
|
-
Coverage percentage checks will not work until the dependency is installed.`
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
3069
|
+
planned.onFailure = () => {
|
|
3070
|
+
config.rules.testCoverage = 0;
|
|
3071
|
+
};
|
|
3072
|
+
state.deferredInstalls.push(planned);
|
|
2355
3073
|
} else if (choice === "disable") {
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
} else {
|
|
2359
|
-
clack2.log.info(
|
|
2360
|
-
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2361
|
-
Install later: ${m.installCommand}`
|
|
2362
|
-
);
|
|
3074
|
+
config.rules.testCoverage = 0;
|
|
3075
|
+
return;
|
|
2363
3076
|
}
|
|
2364
3077
|
}
|
|
2365
|
-
|
|
3078
|
+
const result = await clack10.text({
|
|
3079
|
+
message: "Test coverage target (0 = disable)?",
|
|
3080
|
+
initialValue: String(config.rules.testCoverage),
|
|
3081
|
+
validate: (v) => {
|
|
3082
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
3083
|
+
const n = Number.parseInt(v, 10);
|
|
3084
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
3085
|
+
}
|
|
3086
|
+
});
|
|
3087
|
+
if (isCancelled(result)) return;
|
|
3088
|
+
config.rules.testCoverage = Number.parseInt(result, 10);
|
|
2366
3089
|
}
|
|
2367
|
-
function
|
|
3090
|
+
async function handlePackageOverrides(config) {
|
|
3091
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3092
|
+
config.packages = await promptPackageOverrides(config.packages, {
|
|
3093
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3094
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3095
|
+
testCoverage: config.rules.testCoverage,
|
|
3096
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3097
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3098
|
+
});
|
|
3099
|
+
normalizePackageOverrides(config.packages);
|
|
3100
|
+
}
|
|
3101
|
+
async function handleBoundaries(config, state, opts) {
|
|
3102
|
+
const shouldInfer = await clack10.confirm({
|
|
3103
|
+
message: "Infer boundary rules from current import patterns?",
|
|
3104
|
+
initialValue: false
|
|
3105
|
+
});
|
|
3106
|
+
if (isCancelled(shouldInfer)) return;
|
|
3107
|
+
state.visited.boundaries = true;
|
|
3108
|
+
if (!shouldInfer) {
|
|
3109
|
+
config.rules.enforceBoundaries = false;
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
const bs = clack10.spinner();
|
|
3113
|
+
bs.start("Building import graph...");
|
|
2368
3114
|
try {
|
|
2369
|
-
const
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
3115
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3116
|
+
const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
|
|
3117
|
+
const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
|
|
3118
|
+
const inferred = inferBoundaries(graph);
|
|
3119
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3120
|
+
if (denyCount > 0) {
|
|
3121
|
+
config.boundaries = inferred;
|
|
3122
|
+
config.rules.enforceBoundaries = true;
|
|
3123
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3124
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3125
|
+
} else {
|
|
3126
|
+
bs.stop("No boundary rules inferred");
|
|
3127
|
+
}
|
|
3128
|
+
} catch (err) {
|
|
3129
|
+
bs.stop("Failed to build import graph");
|
|
3130
|
+
clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
async function handleIntegrations(state, opts) {
|
|
3134
|
+
const result = await promptIntegrationsDeferred(
|
|
3135
|
+
state.hookManager,
|
|
3136
|
+
opts.tools,
|
|
3137
|
+
opts.tools.packageManager,
|
|
3138
|
+
opts.tools.isWorkspace,
|
|
3139
|
+
opts.projectRoot
|
|
3140
|
+
);
|
|
3141
|
+
state.visited.integrations = true;
|
|
3142
|
+
state.integrations = result.choice;
|
|
3143
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
|
|
3144
|
+
if (result.lefthookInstall) {
|
|
3145
|
+
state.deferredInstalls.push(result.lefthookInstall);
|
|
2374
3146
|
}
|
|
2375
3147
|
}
|
|
2376
3148
|
|
|
3149
|
+
// src/utils/prompt-main-menu-hints.ts
|
|
3150
|
+
import chalk10 from "chalk";
|
|
3151
|
+
function fileLimitsHint(config) {
|
|
3152
|
+
const max = config.rules.maxFileLines;
|
|
3153
|
+
const test = config.rules.maxTestFileLines;
|
|
3154
|
+
return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
|
|
3155
|
+
}
|
|
3156
|
+
function fileNamingHint(config, scanResult) {
|
|
3157
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3158
|
+
const naming = rootPkg.conventions?.fileNaming;
|
|
3159
|
+
if (!config.rules.enforceNaming) return "not enforced";
|
|
3160
|
+
if (naming) {
|
|
3161
|
+
const detected = scanResult.packages.some(
|
|
3162
|
+
(p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
|
|
3163
|
+
);
|
|
3164
|
+
return detected ? `${naming} (detected)` : naming;
|
|
3165
|
+
}
|
|
3166
|
+
return "mixed \u2014 will not enforce if skipped";
|
|
3167
|
+
}
|
|
3168
|
+
function fileNamingStatus(config) {
|
|
3169
|
+
if (!config.rules.enforceNaming) return "disabled";
|
|
3170
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3171
|
+
return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
|
|
3172
|
+
}
|
|
3173
|
+
function missingTestsHint(config) {
|
|
3174
|
+
if (!config.rules.enforceMissingTests) return "not enforced";
|
|
3175
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3176
|
+
const pattern = rootPkg.structure?.testPattern;
|
|
3177
|
+
return pattern ? `enforced (${pattern})` : "enforced";
|
|
3178
|
+
}
|
|
3179
|
+
function coverageHint(config, hasTestRunner) {
|
|
3180
|
+
if (config.rules.testCoverage === 0) return "disabled";
|
|
3181
|
+
if (!hasTestRunner)
|
|
3182
|
+
return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
|
|
3183
|
+
const isMonorepo = config.packages.length > 1;
|
|
3184
|
+
if (isMonorepo) {
|
|
3185
|
+
const withCov = config.packages.filter(
|
|
3186
|
+
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
3187
|
+
);
|
|
3188
|
+
const exempt = config.packages.length - withCov.length;
|
|
3189
|
+
return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
|
|
3190
|
+
}
|
|
3191
|
+
return `${config.rules.testCoverage}%`;
|
|
3192
|
+
}
|
|
3193
|
+
function advancedNamingHint(config) {
|
|
3194
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3195
|
+
const parts = [];
|
|
3196
|
+
if (rootPkg.conventions?.componentNaming)
|
|
3197
|
+
parts.push(`${rootPkg.conventions.componentNaming} components`);
|
|
3198
|
+
if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
|
|
3199
|
+
if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
|
|
3200
|
+
return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
|
|
3201
|
+
}
|
|
3202
|
+
function integrationsHint(state) {
|
|
3203
|
+
if (!state.visited.integrations || !state.integrations)
|
|
3204
|
+
return "not configured \u2014 select to set up";
|
|
3205
|
+
const items = [];
|
|
3206
|
+
if (state.integrations.preCommitHook) items.push("pre-commit");
|
|
3207
|
+
if (state.integrations.typecheckHook) items.push("typecheck");
|
|
3208
|
+
if (state.integrations.lintHook) items.push("lint");
|
|
3209
|
+
if (state.integrations.claudeCodeHook) items.push("Claude");
|
|
3210
|
+
if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
|
|
3211
|
+
if (state.integrations.githubAction) items.push("CI");
|
|
3212
|
+
return items.length > 0 ? items.join(" \xB7 ") : "none selected";
|
|
3213
|
+
}
|
|
3214
|
+
function packageOverridesHint(config) {
|
|
3215
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3216
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3217
|
+
const customized = editable.filter(
|
|
3218
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3219
|
+
).length;
|
|
3220
|
+
return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
|
|
3221
|
+
}
|
|
3222
|
+
function boundariesHint(config, state) {
|
|
3223
|
+
if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
|
|
3224
|
+
const deny = config.boundaries?.deny;
|
|
3225
|
+
if (!deny) return "enabled";
|
|
3226
|
+
const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
|
|
3227
|
+
const pkgCount = Object.keys(deny).length;
|
|
3228
|
+
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3229
|
+
}
|
|
3230
|
+
function advancedNamingStatus(config) {
|
|
3231
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3232
|
+
const hasAny = !!rootPkg.conventions?.componentNaming || !!rootPkg.conventions?.hookNaming || !!rootPkg.conventions?.importAlias;
|
|
3233
|
+
return hasAny ? "ok" : "unconfigured";
|
|
3234
|
+
}
|
|
3235
|
+
function packageOverridesStatus(config) {
|
|
3236
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3237
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3238
|
+
const customized = editable.some(
|
|
3239
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3240
|
+
);
|
|
3241
|
+
return customized ? "ok" : "unconfigured";
|
|
3242
|
+
}
|
|
3243
|
+
function statusIcon(status) {
|
|
3244
|
+
if (status === "ok") return chalk10.green("\u2713");
|
|
3245
|
+
if (status === "needs-input") return chalk10.yellow("?");
|
|
3246
|
+
if (status === "unconfigured") return chalk10.dim("-");
|
|
3247
|
+
return chalk10.yellow("~");
|
|
3248
|
+
}
|
|
3249
|
+
function buildMainMenuOptions(config, scanResult, state) {
|
|
3250
|
+
const namingStatus = fileNamingStatus(config);
|
|
3251
|
+
const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
|
|
3252
|
+
const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
|
|
3253
|
+
const options = [
|
|
3254
|
+
{
|
|
3255
|
+
value: "fileLimits",
|
|
3256
|
+
label: `${statusIcon("ok")} Max file size`,
|
|
3257
|
+
hint: fileLimitsHint(config)
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
value: "fileNaming",
|
|
3261
|
+
label: `${statusIcon(namingStatus)} Default file naming`,
|
|
3262
|
+
hint: fileNamingHint(config, scanResult)
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
value: "missingTests",
|
|
3266
|
+
label: `${statusIcon(missingTestsStatus)} Missing tests`,
|
|
3267
|
+
hint: missingTestsHint(config)
|
|
3268
|
+
},
|
|
3269
|
+
{
|
|
3270
|
+
value: "coverage",
|
|
3271
|
+
label: `${statusIcon(coverageStatus)} Coverage`,
|
|
3272
|
+
hint: coverageHint(config, state.hasTestRunner)
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
value: "advancedNaming",
|
|
3276
|
+
label: `${statusIcon(advancedNamingStatus(config))} Advanced naming`,
|
|
3277
|
+
hint: advancedNamingHint(config)
|
|
3278
|
+
}
|
|
3279
|
+
];
|
|
3280
|
+
if (config.packages.length > 1) {
|
|
3281
|
+
const bIcon = statusIcon(
|
|
3282
|
+
state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
|
|
3283
|
+
);
|
|
3284
|
+
const poIcon = statusIcon(packageOverridesStatus(config));
|
|
3285
|
+
options.push(
|
|
3286
|
+
{
|
|
3287
|
+
value: "packageOverrides",
|
|
3288
|
+
label: `${poIcon} Per-package overrides`,
|
|
3289
|
+
hint: packageOverridesHint(config)
|
|
3290
|
+
},
|
|
3291
|
+
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3292
|
+
);
|
|
3293
|
+
}
|
|
3294
|
+
const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
|
|
3295
|
+
options.push(
|
|
3296
|
+
{ value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
|
|
3297
|
+
{ value: "reset", label: " Reset all to defaults" },
|
|
3298
|
+
{ value: "review", label: " Review scan details" },
|
|
3299
|
+
{ value: "done", label: " Done \u2014 write config" }
|
|
3300
|
+
);
|
|
3301
|
+
return options;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/utils/prompt-main-menu.ts
|
|
3305
|
+
async function promptMainMenu(config, scanResult, opts) {
|
|
3306
|
+
const originalConfig = structuredClone(config);
|
|
3307
|
+
const state = {
|
|
3308
|
+
visited: { integrations: false, boundaries: false },
|
|
3309
|
+
deferredInstalls: [],
|
|
3310
|
+
hasTestRunner: opts.hasTestRunner,
|
|
3311
|
+
hookManager: opts.hookManager
|
|
3312
|
+
};
|
|
3313
|
+
while (true) {
|
|
3314
|
+
const options = buildMainMenuOptions(config, scanResult, state);
|
|
3315
|
+
const choice = await clack11.select({ message: "Configure viberails", options });
|
|
3316
|
+
assertNotCancelled(choice);
|
|
3317
|
+
if (choice === "done") {
|
|
3318
|
+
if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
|
|
3319
|
+
config.rules.enforceNaming = false;
|
|
3320
|
+
}
|
|
3321
|
+
break;
|
|
3322
|
+
}
|
|
3323
|
+
if (choice === "fileLimits") {
|
|
3324
|
+
const s = {
|
|
3325
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3326
|
+
maxTestFileLines: config.rules.maxTestFileLines
|
|
3327
|
+
};
|
|
3328
|
+
await promptFileLimitsMenu(s);
|
|
3329
|
+
config.rules.maxFileLines = s.maxFileLines;
|
|
3330
|
+
config.rules.maxTestFileLines = s.maxTestFileLines;
|
|
3331
|
+
}
|
|
3332
|
+
if (choice === "fileNaming") await handleFileNaming(config, scanResult);
|
|
3333
|
+
if (choice === "missingTests") await handleMissingTests(config);
|
|
3334
|
+
if (choice === "coverage") await handleCoverage(config, state, opts);
|
|
3335
|
+
if (choice === "advancedNaming") await handleAdvancedNaming(config);
|
|
3336
|
+
if (choice === "packageOverrides") await handlePackageOverrides(config);
|
|
3337
|
+
if (choice === "boundaries") await handleBoundaries(config, state, opts);
|
|
3338
|
+
if (choice === "integrations") await handleIntegrations(state, opts);
|
|
3339
|
+
if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
|
|
3340
|
+
if (choice === "reset") {
|
|
3341
|
+
const confirmed = await clack11.confirm({
|
|
3342
|
+
message: "Reset all settings to scan-detected defaults?",
|
|
3343
|
+
initialValue: false
|
|
3344
|
+
});
|
|
3345
|
+
assertNotCancelled(confirmed);
|
|
3346
|
+
if (confirmed) {
|
|
3347
|
+
Object.assign(config, structuredClone(originalConfig));
|
|
3348
|
+
state.deferredInstalls = [];
|
|
3349
|
+
state.visited = { integrations: false, boundaries: false };
|
|
3350
|
+
state.integrations = void 0;
|
|
3351
|
+
clack11.log.info("Reset all settings to scan-detected defaults.");
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
return state;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
2377
3358
|
// src/utils/update-gitignore.ts
|
|
2378
|
-
import * as
|
|
2379
|
-
import * as
|
|
3359
|
+
import * as fs16 from "fs";
|
|
3360
|
+
import * as path16 from "path";
|
|
2380
3361
|
function updateGitignore(projectRoot) {
|
|
2381
|
-
const gitignorePath =
|
|
3362
|
+
const gitignorePath = path16.join(projectRoot, ".gitignore");
|
|
2382
3363
|
let content = "";
|
|
2383
|
-
if (
|
|
2384
|
-
content =
|
|
3364
|
+
if (fs16.existsSync(gitignorePath)) {
|
|
3365
|
+
content = fs16.readFileSync(gitignorePath, "utf-8");
|
|
2385
3366
|
}
|
|
2386
3367
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
2387
3368
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
2388
3369
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
2389
3370
|
`;
|
|
2390
|
-
|
|
3371
|
+
fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
2391
3372
|
}
|
|
2392
3373
|
}
|
|
2393
3374
|
|
|
2394
3375
|
// src/commands/init-hooks.ts
|
|
2395
|
-
import * as
|
|
2396
|
-
import * as
|
|
3376
|
+
import * as fs18 from "fs";
|
|
3377
|
+
import * as path18 from "path";
|
|
2397
3378
|
import chalk11 from "chalk";
|
|
2398
3379
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2399
3380
|
|
|
2400
3381
|
// src/commands/resolve-typecheck.ts
|
|
2401
|
-
import * as
|
|
2402
|
-
import * as
|
|
3382
|
+
import * as fs17 from "fs";
|
|
3383
|
+
import * as path17 from "path";
|
|
2403
3384
|
function hasTurboTask(projectRoot, taskName) {
|
|
2404
|
-
const turboPath =
|
|
2405
|
-
if (!
|
|
3385
|
+
const turboPath = path17.join(projectRoot, "turbo.json");
|
|
3386
|
+
if (!fs17.existsSync(turboPath)) return false;
|
|
2406
3387
|
try {
|
|
2407
|
-
const turbo = JSON.parse(
|
|
3388
|
+
const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
|
|
2408
3389
|
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
2409
3390
|
return taskName in tasks;
|
|
2410
3391
|
} catch {
|
|
@@ -2415,10 +3396,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2415
3396
|
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
2416
3397
|
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
2417
3398
|
}
|
|
2418
|
-
const pkgJsonPath =
|
|
2419
|
-
if (
|
|
3399
|
+
const pkgJsonPath = path17.join(projectRoot, "package.json");
|
|
3400
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
2420
3401
|
try {
|
|
2421
|
-
const pkg = JSON.parse(
|
|
3402
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
2422
3403
|
if (pkg.scripts?.typecheck) {
|
|
2423
3404
|
const pm = packageManager ?? "npm";
|
|
2424
3405
|
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
@@ -2426,7 +3407,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2426
3407
|
} catch {
|
|
2427
3408
|
}
|
|
2428
3409
|
}
|
|
2429
|
-
if (
|
|
3410
|
+
if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
|
|
2430
3411
|
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
2431
3412
|
}
|
|
2432
3413
|
return {
|
|
@@ -2436,23 +3417,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2436
3417
|
|
|
2437
3418
|
// src/commands/init-hooks.ts
|
|
2438
3419
|
function setupPreCommitHook(projectRoot) {
|
|
2439
|
-
const lefthookPath =
|
|
2440
|
-
if (
|
|
3420
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3421
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
2441
3422
|
addLefthookPreCommit(lefthookPath);
|
|
2442
3423
|
console.log(` ${chalk11.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
2443
3424
|
return "lefthook.yml";
|
|
2444
3425
|
}
|
|
2445
|
-
const huskyDir =
|
|
2446
|
-
if (
|
|
3426
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3427
|
+
if (fs18.existsSync(huskyDir)) {
|
|
2447
3428
|
writeHuskyPreCommit(huskyDir);
|
|
2448
3429
|
console.log(` ${chalk11.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
2449
3430
|
return ".husky/pre-commit";
|
|
2450
3431
|
}
|
|
2451
|
-
const gitDir =
|
|
2452
|
-
if (
|
|
2453
|
-
const hooksDir =
|
|
2454
|
-
if (!
|
|
2455
|
-
|
|
3432
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3433
|
+
if (fs18.existsSync(gitDir)) {
|
|
3434
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3435
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
3436
|
+
fs18.mkdirSync(hooksDir, { recursive: true });
|
|
2456
3437
|
}
|
|
2457
3438
|
writeGitHookPreCommit(hooksDir);
|
|
2458
3439
|
console.log(` ${chalk11.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -2461,11 +3442,11 @@ function setupPreCommitHook(projectRoot) {
|
|
|
2461
3442
|
return void 0;
|
|
2462
3443
|
}
|
|
2463
3444
|
function writeGitHookPreCommit(hooksDir) {
|
|
2464
|
-
const hookPath =
|
|
2465
|
-
if (
|
|
2466
|
-
const existing =
|
|
3445
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3446
|
+
if (fs18.existsSync(hookPath)) {
|
|
3447
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2467
3448
|
if (existing.includes("viberails")) return;
|
|
2468
|
-
|
|
3449
|
+
fs18.writeFileSync(
|
|
2469
3450
|
hookPath,
|
|
2470
3451
|
`${existing.trimEnd()}
|
|
2471
3452
|
|
|
@@ -2482,10 +3463,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
|
|
|
2482
3463
|
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
2483
3464
|
""
|
|
2484
3465
|
].join("\n");
|
|
2485
|
-
|
|
3466
|
+
fs18.writeFileSync(hookPath, script, { mode: 493 });
|
|
2486
3467
|
}
|
|
2487
3468
|
function addLefthookPreCommit(lefthookPath) {
|
|
2488
|
-
const content =
|
|
3469
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
2489
3470
|
if (content.includes("viberails")) return;
|
|
2490
3471
|
const doc = parseYaml(content) ?? {};
|
|
2491
3472
|
if (!doc["pre-commit"]) {
|
|
@@ -2497,23 +3478,23 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
2497
3478
|
doc["pre-commit"].commands.viberails = {
|
|
2498
3479
|
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
2499
3480
|
};
|
|
2500
|
-
|
|
3481
|
+
fs18.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
2501
3482
|
}
|
|
2502
3483
|
function detectHookManager(projectRoot) {
|
|
2503
|
-
if (
|
|
2504
|
-
if (
|
|
3484
|
+
if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
3485
|
+
if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
|
|
2505
3486
|
return void 0;
|
|
2506
3487
|
}
|
|
2507
3488
|
function setupClaudeCodeHook(projectRoot) {
|
|
2508
|
-
const claudeDir =
|
|
2509
|
-
if (!
|
|
2510
|
-
|
|
3489
|
+
const claudeDir = path18.join(projectRoot, ".claude");
|
|
3490
|
+
if (!fs18.existsSync(claudeDir)) {
|
|
3491
|
+
fs18.mkdirSync(claudeDir, { recursive: true });
|
|
2511
3492
|
}
|
|
2512
|
-
const settingsPath =
|
|
3493
|
+
const settingsPath = path18.join(claudeDir, "settings.json");
|
|
2513
3494
|
let settings = {};
|
|
2514
|
-
if (
|
|
3495
|
+
if (fs18.existsSync(settingsPath)) {
|
|
2515
3496
|
try {
|
|
2516
|
-
settings = JSON.parse(
|
|
3497
|
+
settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
|
|
2517
3498
|
} catch {
|
|
2518
3499
|
console.warn(
|
|
2519
3500
|
` ${chalk11.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
@@ -2539,30 +3520,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
2539
3520
|
}
|
|
2540
3521
|
];
|
|
2541
3522
|
settings.hooks = hooks;
|
|
2542
|
-
|
|
3523
|
+
fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2543
3524
|
`);
|
|
2544
3525
|
console.log(` ${chalk11.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
2545
3526
|
}
|
|
2546
3527
|
function setupClaudeMdReference(projectRoot) {
|
|
2547
|
-
const claudeMdPath =
|
|
3528
|
+
const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
|
|
2548
3529
|
let content = "";
|
|
2549
|
-
if (
|
|
2550
|
-
content =
|
|
3530
|
+
if (fs18.existsSync(claudeMdPath)) {
|
|
3531
|
+
content = fs18.readFileSync(claudeMdPath, "utf-8");
|
|
2551
3532
|
}
|
|
2552
3533
|
if (content.includes("@.viberails/context.md")) return;
|
|
2553
3534
|
const ref = "\n@.viberails/context.md\n";
|
|
2554
3535
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
2555
|
-
|
|
3536
|
+
fs18.writeFileSync(claudeMdPath, prefix + ref);
|
|
2556
3537
|
console.log(` ${chalk11.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
2557
3538
|
}
|
|
2558
3539
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
2559
|
-
const workflowDir =
|
|
2560
|
-
const workflowPath =
|
|
2561
|
-
if (
|
|
2562
|
-
const existing =
|
|
3540
|
+
const workflowDir = path18.join(projectRoot, ".github", "workflows");
|
|
3541
|
+
const workflowPath = path18.join(workflowDir, "viberails.yml");
|
|
3542
|
+
if (fs18.existsSync(workflowPath)) {
|
|
3543
|
+
const existing = fs18.readFileSync(workflowPath, "utf-8");
|
|
2563
3544
|
if (existing.includes("viberails")) return void 0;
|
|
2564
3545
|
}
|
|
2565
|
-
|
|
3546
|
+
fs18.mkdirSync(workflowDir, { recursive: true });
|
|
2566
3547
|
const pm = packageManager || "npm";
|
|
2567
3548
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
2568
3549
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -2616,74 +3597,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
2616
3597
|
""
|
|
2617
3598
|
);
|
|
2618
3599
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
2619
|
-
|
|
3600
|
+
fs18.writeFileSync(workflowPath, content);
|
|
2620
3601
|
return ".github/workflows/viberails.yml";
|
|
2621
3602
|
}
|
|
2622
3603
|
function writeHuskyPreCommit(huskyDir) {
|
|
2623
|
-
const hookPath =
|
|
3604
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
2624
3605
|
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
2625
|
-
if (
|
|
2626
|
-
const existing =
|
|
3606
|
+
if (fs18.existsSync(hookPath)) {
|
|
3607
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
2627
3608
|
if (!existing.includes("viberails")) {
|
|
2628
|
-
|
|
3609
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2629
3610
|
${cmd}
|
|
2630
3611
|
`);
|
|
2631
3612
|
}
|
|
2632
3613
|
return;
|
|
2633
3614
|
}
|
|
2634
|
-
|
|
3615
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
2635
3616
|
${cmd}
|
|
2636
3617
|
`, { mode: 493 });
|
|
2637
3618
|
}
|
|
2638
3619
|
|
|
2639
3620
|
// src/commands/init-hooks-extra.ts
|
|
2640
|
-
import * as
|
|
2641
|
-
import * as
|
|
3621
|
+
import * as fs19 from "fs";
|
|
3622
|
+
import * as path19 from "path";
|
|
2642
3623
|
import chalk12 from "chalk";
|
|
2643
3624
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
2644
3625
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
2645
|
-
const lefthookPath =
|
|
2646
|
-
if (
|
|
2647
|
-
const content =
|
|
3626
|
+
const lefthookPath = path19.join(projectRoot, "lefthook.yml");
|
|
3627
|
+
if (fs19.existsSync(lefthookPath)) {
|
|
3628
|
+
const content = fs19.readFileSync(lefthookPath, "utf-8");
|
|
2648
3629
|
if (content.includes(marker)) return void 0;
|
|
2649
3630
|
const doc = parseYaml2(content) ?? {};
|
|
2650
3631
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
2651
3632
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
2652
3633
|
doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
|
|
2653
|
-
|
|
3634
|
+
fs19.writeFileSync(lefthookPath, stringifyYaml2(doc));
|
|
2654
3635
|
return "lefthook.yml";
|
|
2655
3636
|
}
|
|
2656
|
-
const huskyDir =
|
|
2657
|
-
if (
|
|
2658
|
-
const hookPath =
|
|
2659
|
-
if (
|
|
2660
|
-
const existing =
|
|
3637
|
+
const huskyDir = path19.join(projectRoot, ".husky");
|
|
3638
|
+
if (fs19.existsSync(huskyDir)) {
|
|
3639
|
+
const hookPath = path19.join(huskyDir, "pre-commit");
|
|
3640
|
+
if (fs19.existsSync(hookPath)) {
|
|
3641
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
2661
3642
|
if (existing.includes(marker)) return void 0;
|
|
2662
|
-
|
|
3643
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2663
3644
|
${command}
|
|
2664
3645
|
`);
|
|
2665
3646
|
} else {
|
|
2666
|
-
|
|
3647
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
2667
3648
|
${command}
|
|
2668
3649
|
`, { mode: 493 });
|
|
2669
3650
|
}
|
|
2670
3651
|
return ".husky/pre-commit";
|
|
2671
3652
|
}
|
|
2672
|
-
const gitDir =
|
|
2673
|
-
if (
|
|
2674
|
-
const hooksDir =
|
|
2675
|
-
if (!
|
|
2676
|
-
const hookPath =
|
|
2677
|
-
if (
|
|
2678
|
-
const existing =
|
|
3653
|
+
const gitDir = path19.join(projectRoot, ".git");
|
|
3654
|
+
if (fs19.existsSync(gitDir)) {
|
|
3655
|
+
const hooksDir = path19.join(gitDir, "hooks");
|
|
3656
|
+
if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
|
|
3657
|
+
const hookPath = path19.join(hooksDir, "pre-commit");
|
|
3658
|
+
if (fs19.existsSync(hookPath)) {
|
|
3659
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
2679
3660
|
if (existing.includes(marker)) return void 0;
|
|
2680
|
-
|
|
3661
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
2681
3662
|
|
|
2682
3663
|
# ${name}
|
|
2683
3664
|
${command}
|
|
2684
3665
|
`);
|
|
2685
3666
|
} else {
|
|
2686
|
-
|
|
3667
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
2687
3668
|
# Generated by viberails
|
|
2688
3669
|
|
|
2689
3670
|
# ${name}
|
|
@@ -2709,7 +3690,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
|
|
|
2709
3690
|
return target;
|
|
2710
3691
|
}
|
|
2711
3692
|
function setupLintHook(projectRoot, linter) {
|
|
2712
|
-
const isLefthook =
|
|
3693
|
+
const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
|
|
2713
3694
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
2714
3695
|
let command;
|
|
2715
3696
|
let lefthookExtra;
|
|
@@ -2733,6 +3714,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
2733
3714
|
const created = [];
|
|
2734
3715
|
if (integrations.preCommitHook) {
|
|
2735
3716
|
const t = setupPreCommitHook(projectRoot);
|
|
3717
|
+
if (t && opts.lefthookExpected && !t.includes("lefthook")) {
|
|
3718
|
+
console.log(` ${chalk12.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
|
|
3719
|
+
}
|
|
2736
3720
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
2737
3721
|
}
|
|
2738
3722
|
if (integrations.typecheckHook) {
|
|
@@ -2762,9 +3746,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
2762
3746
|
}
|
|
2763
3747
|
|
|
2764
3748
|
// src/commands/init-non-interactive.ts
|
|
2765
|
-
import * as
|
|
2766
|
-
import * as
|
|
2767
|
-
import * as
|
|
3749
|
+
import * as fs20 from "fs";
|
|
3750
|
+
import * as path20 from "path";
|
|
3751
|
+
import * as clack12 from "@clack/prompts";
|
|
2768
3752
|
import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
|
|
2769
3753
|
import { scan as scan2 } from "@viberails/scanner";
|
|
2770
3754
|
import chalk13 from "chalk";
|
|
@@ -2788,7 +3772,7 @@ function getExemptedPackages(config) {
|
|
|
2788
3772
|
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
2789
3773
|
}
|
|
2790
3774
|
async function initNonInteractive(projectRoot, configPath) {
|
|
2791
|
-
const s =
|
|
3775
|
+
const s = clack12.spinner();
|
|
2792
3776
|
s.start("Scanning project...");
|
|
2793
3777
|
const scanResult = await scan2(projectRoot);
|
|
2794
3778
|
const config = generateConfig(scanResult);
|
|
@@ -2807,7 +3791,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2807
3791
|
);
|
|
2808
3792
|
}
|
|
2809
3793
|
if (config.packages.length > 1) {
|
|
2810
|
-
const bs =
|
|
3794
|
+
const bs = clack12.spinner();
|
|
2811
3795
|
bs.start("Building import graph...");
|
|
2812
3796
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2813
3797
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -2823,7 +3807,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2823
3807
|
}
|
|
2824
3808
|
}
|
|
2825
3809
|
const compacted = compactConfig3(config);
|
|
2826
|
-
|
|
3810
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2827
3811
|
`);
|
|
2828
3812
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2829
3813
|
updateGitignore(projectRoot);
|
|
@@ -2842,7 +3826,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
2842
3826
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
2843
3827
|
const ok = chalk13.green("\u2713");
|
|
2844
3828
|
const created = [
|
|
2845
|
-
`${ok} ${
|
|
3829
|
+
`${ok} ${path20.basename(configPath)}`,
|
|
2846
3830
|
`${ok} .viberails/context.md`,
|
|
2847
3831
|
`${ok} .viberails/scan-result.json`,
|
|
2848
3832
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
@@ -2859,9 +3843,6 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
|
|
|
2859
3843
|
|
|
2860
3844
|
// src/commands/init.ts
|
|
2861
3845
|
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
3846
|
async function initCommand(options, cwd) {
|
|
2866
3847
|
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
2867
3848
|
if (!projectRoot) {
|
|
@@ -2869,8 +3850,8 @@ async function initCommand(options, cwd) {
|
|
|
2869
3850
|
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
2870
3851
|
);
|
|
2871
3852
|
}
|
|
2872
|
-
const configPath =
|
|
2873
|
-
if (
|
|
3853
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE5);
|
|
3854
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
2874
3855
|
if (!options.yes) {
|
|
2875
3856
|
return initInteractive(projectRoot, configPath, options);
|
|
2876
3857
|
}
|
|
@@ -2884,12 +3865,11 @@ async function initCommand(options, cwd) {
|
|
|
2884
3865
|
await initInteractive(projectRoot, configPath, options);
|
|
2885
3866
|
}
|
|
2886
3867
|
async function initInteractive(projectRoot, configPath, options) {
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
const action = await promptExistingConfigAction(path20.basename(configPath));
|
|
3868
|
+
clack13.intro("viberails");
|
|
3869
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3870
|
+
const action = await promptExistingConfigAction(path21.basename(configPath));
|
|
2891
3871
|
if (action === "cancel") {
|
|
2892
|
-
|
|
3872
|
+
clack13.outro("Aborted. No files were written.");
|
|
2893
3873
|
return;
|
|
2894
3874
|
}
|
|
2895
3875
|
if (action === "edit") {
|
|
@@ -2898,142 +3878,93 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
2898
3878
|
}
|
|
2899
3879
|
options.force = true;
|
|
2900
3880
|
}
|
|
2901
|
-
if (
|
|
3881
|
+
if (fs21.existsSync(configPath) && options.force) {
|
|
2902
3882
|
const replace = await confirmDangerous(
|
|
2903
|
-
`${
|
|
3883
|
+
`${path21.basename(configPath)} already exists and will be replaced. Continue?`
|
|
2904
3884
|
);
|
|
2905
3885
|
if (!replace) {
|
|
2906
|
-
|
|
3886
|
+
clack13.outro("Aborted. No files were written.");
|
|
2907
3887
|
return;
|
|
2908
3888
|
}
|
|
2909
3889
|
}
|
|
2910
|
-
const s =
|
|
3890
|
+
const s = clack13.spinner();
|
|
2911
3891
|
s.start("Scanning project...");
|
|
2912
3892
|
const scanResult = await scan3(projectRoot);
|
|
2913
3893
|
const config = generateConfig2(scanResult);
|
|
2914
3894
|
s.stop("Scan complete");
|
|
2915
3895
|
if (scanResult.statistics.totalFiles === 0) {
|
|
2916
|
-
|
|
3896
|
+
clack13.log.warn(
|
|
2917
3897
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
2918
3898
|
);
|
|
2919
3899
|
}
|
|
2920
|
-
const
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
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
|
-
}
|
|
2932
|
-
if (decision === "customize") {
|
|
2933
|
-
const { resolveNamingDefault } = await import("./prompt-naming-default-AH54HEBC.js");
|
|
2934
|
-
await resolveNamingDefault(config, scanResult);
|
|
2935
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2936
|
-
const overrides = await promptRuleMenu({
|
|
2937
|
-
maxFileLines: config.rules.maxFileLines,
|
|
2938
|
-
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2939
|
-
testCoverage: config.rules.testCoverage,
|
|
2940
|
-
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2941
|
-
enforceNaming: config.rules.enforceNaming,
|
|
2942
|
-
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2943
|
-
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2944
|
-
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2945
|
-
importAlias: rootPkg.conventions?.importAlias,
|
|
2946
|
-
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
2947
|
-
coverageCommand: config.defaults?.coverage?.command,
|
|
2948
|
-
packageOverrides: config.packages
|
|
2949
|
-
});
|
|
2950
|
-
applyRuleOverrides(config, overrides);
|
|
2951
|
-
}
|
|
2952
|
-
if (config.packages.length > 1) {
|
|
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.",
|
|
2955
|
-
"Boundaries"
|
|
3900
|
+
const hasTestRunner = !!scanResult.stack.testRunner;
|
|
3901
|
+
if (!hasTestRunner) {
|
|
3902
|
+
clack13.log.info(
|
|
3903
|
+
"No test runner detected. Coverage checks are inactive until a test runner is installed.\nInstall a test runner (e.g. vitest) and re-run viberails init."
|
|
2956
3904
|
);
|
|
2957
|
-
const shouldInfer = await confirm("Infer boundary rules from current import patterns?");
|
|
2958
|
-
if (shouldInfer) {
|
|
2959
|
-
const bs = clack4.spinner();
|
|
2960
|
-
bs.start("Building import graph...");
|
|
2961
|
-
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2962
|
-
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
2963
|
-
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
2964
|
-
const inferred = inferBoundaries(graph);
|
|
2965
|
-
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
2966
|
-
if (denyCount > 0) {
|
|
2967
|
-
config.boundaries = inferred;
|
|
2968
|
-
config.rules.enforceBoundaries = true;
|
|
2969
|
-
const pkgCount = Object.keys(inferred.deny).length;
|
|
2970
|
-
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
2971
|
-
} else {
|
|
2972
|
-
bs.stop("No boundary rules inferred");
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
3905
|
}
|
|
2976
3906
|
const hookManager = detectHookManager(projectRoot);
|
|
2977
3907
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
2978
|
-
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
2979
|
-
if (hasMissingPrereqs) {
|
|
2980
|
-
clack4.log.info("Some dependencies are needed for full functionality.");
|
|
2981
|
-
}
|
|
2982
|
-
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
2983
|
-
if (prereqResult.disableCoverage) {
|
|
2984
|
-
config.rules.testCoverage = 0;
|
|
2985
|
-
}
|
|
2986
3908
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
2987
|
-
const
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3909
|
+
const state = await promptMainMenu(config, scanResult, {
|
|
3910
|
+
hasTestRunner,
|
|
3911
|
+
hookManager,
|
|
3912
|
+
coveragePrereqs,
|
|
3913
|
+
projectRoot,
|
|
3914
|
+
tools: {
|
|
3915
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3916
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3917
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3918
|
+
isWorkspace: config.packages.length > 1
|
|
3919
|
+
}
|
|
2996
3920
|
});
|
|
2997
|
-
const shouldWrite = await
|
|
3921
|
+
const shouldWrite = await confirm3("Apply this setup?");
|
|
2998
3922
|
if (!shouldWrite) {
|
|
2999
|
-
|
|
3923
|
+
clack13.outro("Aborted. No files were written.");
|
|
3000
3924
|
return;
|
|
3001
3925
|
}
|
|
3002
|
-
|
|
3926
|
+
if (state.deferredInstalls.length > 0) {
|
|
3927
|
+
await executeDeferredInstalls(projectRoot, state.deferredInstalls);
|
|
3928
|
+
}
|
|
3929
|
+
const ws = clack13.spinner();
|
|
3003
3930
|
ws.start("Writing configuration...");
|
|
3004
3931
|
const compacted = compactConfig4(config);
|
|
3005
|
-
|
|
3932
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3006
3933
|
`);
|
|
3007
3934
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3008
3935
|
updateGitignore(projectRoot);
|
|
3009
3936
|
ws.stop("Configuration written");
|
|
3010
3937
|
const ok = chalk14.green("\u2713");
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3938
|
+
clack13.log.step(`${ok} ${path21.basename(configPath)}`);
|
|
3939
|
+
clack13.log.step(`${ok} .viberails/context.md`);
|
|
3940
|
+
clack13.log.step(`${ok} .viberails/scan-result.json`);
|
|
3941
|
+
if (state.visited.integrations && state.integrations) {
|
|
3942
|
+
const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
|
|
3943
|
+
setupSelectedIntegrations(projectRoot, state.integrations, {
|
|
3944
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3945
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3946
|
+
lefthookExpected
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
clack13.outro(
|
|
3019
3950
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3020
3951
|
${chalk14.dim("Tip: use")} ${chalk14.cyan("viberails check --enforce")} ${chalk14.dim("in CI to block PRs on violations.")}`
|
|
3021
3952
|
);
|
|
3022
3953
|
}
|
|
3023
3954
|
|
|
3024
3955
|
// src/commands/sync.ts
|
|
3025
|
-
import * as
|
|
3026
|
-
import * as
|
|
3027
|
-
import * as
|
|
3956
|
+
import * as fs22 from "fs";
|
|
3957
|
+
import * as path22 from "path";
|
|
3958
|
+
import * as clack14 from "@clack/prompts";
|
|
3028
3959
|
import { compactConfig as compactConfig5, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
|
|
3029
3960
|
import { scan as scan4 } from "@viberails/scanner";
|
|
3030
3961
|
import chalk15 from "chalk";
|
|
3031
3962
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3032
3963
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3033
3964
|
function loadPreviousStats(projectRoot) {
|
|
3034
|
-
const scanResultPath =
|
|
3965
|
+
const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3035
3966
|
try {
|
|
3036
|
-
const raw =
|
|
3967
|
+
const raw = fs22.readFileSync(scanResultPath, "utf-8");
|
|
3037
3968
|
const parsed = JSON.parse(raw);
|
|
3038
3969
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3039
3970
|
return parsed.statistics;
|
|
@@ -3050,17 +3981,17 @@ async function syncCommand(options, cwd) {
|
|
|
3050
3981
|
"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"
|
|
3051
3982
|
);
|
|
3052
3983
|
}
|
|
3053
|
-
const configPath =
|
|
3984
|
+
const configPath = path22.join(projectRoot, CONFIG_FILE6);
|
|
3054
3985
|
const existing = await loadConfig5(configPath);
|
|
3055
3986
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3056
|
-
const s =
|
|
3987
|
+
const s = clack14.spinner();
|
|
3057
3988
|
s.start("Scanning project...");
|
|
3058
3989
|
const scanResult = await scan4(projectRoot);
|
|
3059
3990
|
s.stop("Scan complete");
|
|
3060
3991
|
const merged = mergeConfig2(existing, scanResult);
|
|
3061
3992
|
const compacted = compactConfig5(merged);
|
|
3062
3993
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3063
|
-
const rawDisk =
|
|
3994
|
+
const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
|
|
3064
3995
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3065
3996
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3066
3997
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3078,9 +4009,9 @@ ${chalk15.bold("Changes:")}`);
|
|
|
3078
4009
|
}
|
|
3079
4010
|
}
|
|
3080
4011
|
if (options?.interactive) {
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
const decision = await
|
|
4012
|
+
clack14.intro("viberails sync (interactive)");
|
|
4013
|
+
clack14.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
4014
|
+
const decision = await clack14.select({
|
|
3084
4015
|
message: "How would you like to proceed?",
|
|
3085
4016
|
options: [
|
|
3086
4017
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -3090,7 +4021,7 @@ ${chalk15.bold("Changes:")}`);
|
|
|
3090
4021
|
});
|
|
3091
4022
|
assertNotCancelled(decision);
|
|
3092
4023
|
if (decision === "cancel") {
|
|
3093
|
-
|
|
4024
|
+
clack14.outro("Sync cancelled. No files were written.");
|
|
3094
4025
|
return;
|
|
3095
4026
|
}
|
|
3096
4027
|
if (decision === "customize") {
|
|
@@ -3111,15 +4042,15 @@ ${chalk15.bold("Changes:")}`);
|
|
|
3111
4042
|
});
|
|
3112
4043
|
applyRuleOverrides(merged, overrides);
|
|
3113
4044
|
const recompacted = compactConfig5(merged);
|
|
3114
|
-
|
|
4045
|
+
fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3115
4046
|
`);
|
|
3116
4047
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3117
|
-
|
|
3118
|
-
|
|
4048
|
+
clack14.log.success("Updated config with your customizations.");
|
|
4049
|
+
clack14.outro("Done! Run viberails check to verify.");
|
|
3119
4050
|
return;
|
|
3120
4051
|
}
|
|
3121
4052
|
}
|
|
3122
|
-
|
|
4053
|
+
fs22.writeFileSync(configPath, `${compactedJson}
|
|
3123
4054
|
`);
|
|
3124
4055
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3125
4056
|
console.log(`
|
|
@@ -3134,7 +4065,7 @@ ${chalk15.bold("Synced:")}`);
|
|
|
3134
4065
|
}
|
|
3135
4066
|
|
|
3136
4067
|
// src/index.ts
|
|
3137
|
-
var VERSION = "0.6.
|
|
4068
|
+
var VERSION = "0.6.7";
|
|
3138
4069
|
var program = new Command();
|
|
3139
4070
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3140
4071
|
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) => {
|