viberails 0.6.4 → 0.6.6
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 +1239 -866
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1227 -854
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk15 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -30,146 +30,285 @@ function findProjectRoot(startDir) {
|
|
|
30
30
|
// src/utils/prompt.ts
|
|
31
31
|
import * as clack5 from "@clack/prompts";
|
|
32
32
|
|
|
33
|
-
// src/utils/prompt-
|
|
34
|
-
import * as
|
|
33
|
+
// src/utils/prompt-rules.ts
|
|
34
|
+
import * as clack4 from "@clack/prompts";
|
|
35
35
|
|
|
36
|
-
// src/utils/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return new Promise((resolve4) => {
|
|
40
|
-
const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
|
|
41
|
-
let stdout = "";
|
|
42
|
-
let stderr = "";
|
|
43
|
-
child.stdout.on("data", (d) => {
|
|
44
|
-
stdout += d.toString();
|
|
45
|
-
});
|
|
46
|
-
child.stderr.on("data", (d) => {
|
|
47
|
-
stderr += d.toString();
|
|
48
|
-
});
|
|
49
|
-
child.on("close", (status) => {
|
|
50
|
-
resolve4({ status, stdout, stderr });
|
|
51
|
-
});
|
|
52
|
-
child.on("error", () => {
|
|
53
|
-
resolve4({ status: 1, stdout, stderr });
|
|
54
|
-
});
|
|
55
|
-
});
|
|
36
|
+
// src/utils/get-root-package.ts
|
|
37
|
+
function getRootPackage(packages) {
|
|
38
|
+
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
56
39
|
}
|
|
57
40
|
|
|
58
|
-
// src/utils/prompt-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
|
|
55
|
+
// src/utils/prompt-submenus.ts
|
|
56
|
+
import * as clack from "@clack/prompts";
|
|
57
|
+
var FILE_NAMING_OPTIONS = [
|
|
58
|
+
{ value: "kebab-case", label: "kebab-case" },
|
|
59
|
+
{ value: "camelCase", label: "camelCase" },
|
|
60
|
+
{ value: "PascalCase", label: "PascalCase" },
|
|
61
|
+
{ value: "snake_case", label: "snake_case" }
|
|
62
|
+
];
|
|
63
|
+
var COMPONENT_NAMING_OPTIONS = [
|
|
64
|
+
{ value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
|
|
65
|
+
{ value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
|
|
66
|
+
];
|
|
67
|
+
var HOOK_NAMING_OPTIONS = [
|
|
68
|
+
{ value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
|
|
69
|
+
{ value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
|
|
70
|
+
];
|
|
71
|
+
async function promptFileLimitsMenu(state) {
|
|
72
|
+
while (true) {
|
|
73
|
+
const choice = await clack.select({
|
|
74
|
+
message: "File limits",
|
|
75
|
+
options: [
|
|
76
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
77
|
+
{
|
|
78
|
+
value: "maxTestFileLines",
|
|
79
|
+
label: "Max test file lines",
|
|
80
|
+
hint: state.maxTestFileLines > 0 ? String(state.maxTestFileLines) : "0 (unlimited)"
|
|
81
|
+
},
|
|
82
|
+
{ value: "back", label: "Back" }
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
assertNotCancelled(choice);
|
|
86
|
+
if (choice === "back") return;
|
|
87
|
+
if (choice === "maxFileLines") {
|
|
88
|
+
const result = await clack.text({
|
|
89
|
+
message: "Maximum lines per source file?",
|
|
90
|
+
initialValue: String(state.maxFileLines),
|
|
91
|
+
validate: (v) => {
|
|
92
|
+
if (typeof v !== "string") return "Enter a positive number";
|
|
93
|
+
const n = Number.parseInt(v, 10);
|
|
94
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
assertNotCancelled(result);
|
|
98
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
99
|
+
}
|
|
100
|
+
if (choice === "maxTestFileLines") {
|
|
101
|
+
const result = await clack.text({
|
|
102
|
+
message: "Maximum lines per test file (0 to disable)?",
|
|
103
|
+
initialValue: String(state.maxTestFileLines),
|
|
104
|
+
validate: (v) => {
|
|
105
|
+
if (typeof v !== "string") return "Enter a number (0 or positive)";
|
|
106
|
+
const n = Number.parseInt(v, 10);
|
|
107
|
+
if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
assertNotCancelled(result);
|
|
111
|
+
state.maxTestFileLines = Number.parseInt(result, 10);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function promptNamingMenu(state) {
|
|
116
|
+
while (true) {
|
|
117
|
+
const options = [
|
|
118
|
+
{
|
|
119
|
+
value: "enforceNaming",
|
|
120
|
+
label: "Enforce file naming",
|
|
121
|
+
hint: state.enforceNaming ? "yes" : "no"
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
if (state.enforceNaming) {
|
|
125
|
+
options.push({
|
|
126
|
+
value: "fileNaming",
|
|
127
|
+
label: "File naming convention",
|
|
128
|
+
hint: state.fileNamingValue ?? "(not set)"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
options.push(
|
|
63
132
|
{
|
|
64
|
-
value: "
|
|
65
|
-
label: "
|
|
66
|
-
hint:
|
|
133
|
+
value: "componentNaming",
|
|
134
|
+
label: "Component naming",
|
|
135
|
+
hint: state.componentNaming ?? "(not set)"
|
|
67
136
|
},
|
|
68
137
|
{
|
|
69
|
-
value: "
|
|
70
|
-
label: "
|
|
71
|
-
hint:
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const s = clack.spinner();
|
|
80
|
-
s.start("Installing Lefthook...");
|
|
81
|
-
const result = await spawnAsync(installCmd, projectRoot);
|
|
82
|
-
if (result.status === 0) {
|
|
83
|
-
const fs21 = await import("fs");
|
|
84
|
-
const path21 = await import("path");
|
|
85
|
-
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
86
|
-
if (!fs21.existsSync(lefthookPath)) {
|
|
87
|
-
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
88
|
-
}
|
|
89
|
-
s.stop("Installed Lefthook");
|
|
90
|
-
return "Lefthook";
|
|
91
|
-
}
|
|
92
|
-
s.stop("Failed to install Lefthook");
|
|
93
|
-
clack.log.warn(`Install manually: ${installCmd}`);
|
|
94
|
-
return void 0;
|
|
95
|
-
}
|
|
96
|
-
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
97
|
-
let resolvedHookManager = hookManager;
|
|
98
|
-
if (!resolvedHookManager) {
|
|
99
|
-
resolvedHookManager = await promptHookManagerInstall(
|
|
100
|
-
projectRoot,
|
|
101
|
-
tools?.packageManager ?? "npm",
|
|
102
|
-
tools?.isWorkspace
|
|
138
|
+
value: "hookNaming",
|
|
139
|
+
label: "Hook naming",
|
|
140
|
+
hint: state.hookNaming ?? "(not set)"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
value: "importAlias",
|
|
144
|
+
label: "Import alias",
|
|
145
|
+
hint: state.importAlias ?? "(not set)"
|
|
146
|
+
},
|
|
147
|
+
{ value: "back", label: "Back" }
|
|
103
148
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
const choice = await clack.select({ message: "Naming & conventions", options });
|
|
150
|
+
assertNotCancelled(choice);
|
|
151
|
+
if (choice === "back") return;
|
|
152
|
+
if (choice === "enforceNaming") {
|
|
153
|
+
const result = await clack.confirm({
|
|
154
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
155
|
+
initialValue: state.enforceNaming
|
|
156
|
+
});
|
|
157
|
+
assertNotCancelled(result);
|
|
158
|
+
if (result && !state.fileNamingValue) {
|
|
159
|
+
const selected = await clack.select({
|
|
160
|
+
message: "Which file naming convention should be enforced?",
|
|
161
|
+
options: [...FILE_NAMING_OPTIONS]
|
|
162
|
+
});
|
|
163
|
+
assertNotCancelled(selected);
|
|
164
|
+
state.fileNamingValue = selected;
|
|
165
|
+
}
|
|
166
|
+
state.enforceNaming = result;
|
|
167
|
+
}
|
|
168
|
+
if (choice === "fileNaming") {
|
|
169
|
+
const selected = await clack.select({
|
|
170
|
+
message: "Which file naming convention should be enforced?",
|
|
171
|
+
options: [...FILE_NAMING_OPTIONS],
|
|
172
|
+
initialValue: state.fileNamingValue
|
|
173
|
+
});
|
|
174
|
+
assertNotCancelled(selected);
|
|
175
|
+
state.fileNamingValue = selected;
|
|
176
|
+
}
|
|
177
|
+
if (choice === "componentNaming") {
|
|
178
|
+
const selected = await clack.select({
|
|
179
|
+
message: "Component naming convention",
|
|
180
|
+
options: [
|
|
181
|
+
...COMPONENT_NAMING_OPTIONS,
|
|
182
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
183
|
+
],
|
|
184
|
+
initialValue: state.componentNaming ?? SENTINEL_CLEAR
|
|
185
|
+
});
|
|
186
|
+
assertNotCancelled(selected);
|
|
187
|
+
state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
188
|
+
}
|
|
189
|
+
if (choice === "hookNaming") {
|
|
190
|
+
const selected = await clack.select({
|
|
191
|
+
message: "Hook naming convention",
|
|
192
|
+
options: [
|
|
193
|
+
...HOOK_NAMING_OPTIONS,
|
|
194
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
195
|
+
],
|
|
196
|
+
initialValue: state.hookNaming ?? SENTINEL_CLEAR
|
|
197
|
+
});
|
|
198
|
+
assertNotCancelled(selected);
|
|
199
|
+
state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
200
|
+
}
|
|
201
|
+
if (choice === "importAlias") {
|
|
202
|
+
const selected = await clack.select({
|
|
203
|
+
message: "Import alias pattern",
|
|
204
|
+
options: [
|
|
205
|
+
{ value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
|
|
206
|
+
{ value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
|
|
207
|
+
{ value: SENTINEL_CUSTOM, label: "Custom..." },
|
|
208
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no alias)" }
|
|
209
|
+
],
|
|
210
|
+
initialValue: state.importAlias ?? SENTINEL_CLEAR
|
|
211
|
+
});
|
|
212
|
+
assertNotCancelled(selected);
|
|
213
|
+
if (selected === SENTINEL_CLEAR) {
|
|
214
|
+
state.importAlias = void 0;
|
|
215
|
+
} else if (selected === SENTINEL_CUSTOM) {
|
|
216
|
+
const result = await clack.text({
|
|
217
|
+
message: "Custom import alias (e.g. #/*)?",
|
|
218
|
+
initialValue: state.importAlias ?? "",
|
|
219
|
+
placeholder: "e.g. #/*",
|
|
220
|
+
validate: (v) => {
|
|
221
|
+
if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
|
|
222
|
+
if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
|
|
223
|
+
return "Must match pattern like @/*, ~/*, or #src/*";
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
assertNotCancelled(result);
|
|
227
|
+
state.importAlias = result.trim();
|
|
228
|
+
} else {
|
|
229
|
+
state.importAlias = selected;
|
|
230
|
+
}
|
|
113
231
|
}
|
|
114
|
-
];
|
|
115
|
-
if (tools?.isTypeScript) {
|
|
116
|
-
options.push({
|
|
117
|
-
value: "typecheck",
|
|
118
|
-
label: "Typecheck (tsc --noEmit)",
|
|
119
|
-
hint: "pre-commit hook + CI check"
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (tools?.linter) {
|
|
123
|
-
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
124
|
-
options.push({
|
|
125
|
-
value: "lint",
|
|
126
|
-
label: `Lint check (${linterName})`,
|
|
127
|
-
hint: "pre-commit hook + CI check"
|
|
128
|
-
});
|
|
129
232
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
233
|
+
}
|
|
234
|
+
async function promptTestingMenu(state) {
|
|
235
|
+
while (true) {
|
|
236
|
+
const options = [
|
|
237
|
+
{
|
|
238
|
+
value: "enforceMissingTests",
|
|
239
|
+
label: "Enforce missing tests",
|
|
240
|
+
hint: state.enforceMissingTests ? "yes" : "no"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
value: "testCoverage",
|
|
244
|
+
label: "Test coverage target",
|
|
245
|
+
hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
|
|
246
|
+
}
|
|
247
|
+
];
|
|
248
|
+
if (state.testCoverage > 0) {
|
|
249
|
+
options.push(
|
|
250
|
+
{
|
|
251
|
+
value: "coverageSummaryPath",
|
|
252
|
+
label: "Coverage summary path",
|
|
253
|
+
hint: state.coverageSummaryPath
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
value: "coverageCommand",
|
|
257
|
+
label: "Coverage command",
|
|
258
|
+
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
259
|
+
}
|
|
260
|
+
);
|
|
145
261
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
262
|
+
options.push({ value: "back", label: "Back" });
|
|
263
|
+
const choice = await clack.select({ message: "Testing & coverage", options });
|
|
264
|
+
assertNotCancelled(choice);
|
|
265
|
+
if (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
|
+
assertNotCancelled(result);
|
|
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
|
+
assertNotCancelled(result);
|
|
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
|
+
assertNotCancelled(result);
|
|
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
|
+
assertNotCancelled(result);
|
|
305
|
+
const trimmed = result.trim();
|
|
306
|
+
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
163
309
|
}
|
|
164
310
|
|
|
165
|
-
// src/utils/prompt-rules.ts
|
|
166
|
-
import * as clack4 from "@clack/prompts";
|
|
167
|
-
|
|
168
|
-
// src/utils/prompt-menu-handlers.ts
|
|
169
|
-
import * as clack3 from "@clack/prompts";
|
|
170
|
-
|
|
171
311
|
// src/utils/prompt-package-overrides.ts
|
|
172
|
-
import * as clack2 from "@clack/prompts";
|
|
173
312
|
function normalizePackageOverrides(packages) {
|
|
174
313
|
for (const pkg of packages) {
|
|
175
314
|
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
@@ -178,121 +317,177 @@ function normalizePackageOverrides(packages) {
|
|
|
178
317
|
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
179
318
|
delete pkg.coverage;
|
|
180
319
|
}
|
|
320
|
+
if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
|
|
321
|
+
delete pkg.conventions;
|
|
322
|
+
}
|
|
181
323
|
}
|
|
182
324
|
return packages;
|
|
183
325
|
}
|
|
184
|
-
function
|
|
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
|
+
}
|
|
185
334
|
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
186
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
|
+
}
|
|
187
343
|
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
188
344
|
const defaultCommand = defaults.coverageCommand ?? "";
|
|
189
345
|
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
190
|
-
const tags = [];
|
|
191
|
-
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
192
|
-
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
193
|
-
tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
|
|
194
346
|
if (hasSummaryOverride) tags.push("summary override");
|
|
195
347
|
if (hasCommandOverride) tags.push("command override");
|
|
196
|
-
return tags.join(", ");
|
|
348
|
+
return tags.length > 0 ? tags.join(", ") : "(no overrides)";
|
|
197
349
|
}
|
|
198
|
-
async function
|
|
350
|
+
async function promptPackageOverrides(packages, defaults) {
|
|
199
351
|
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
200
352
|
if (editablePackages.length === 0) return packages;
|
|
201
353
|
while (true) {
|
|
202
354
|
const selectedPath = await clack2.select({
|
|
203
|
-
message: "Select package to edit
|
|
355
|
+
message: "Select package to edit overrides",
|
|
204
356
|
options: [
|
|
205
357
|
...editablePackages.map((pkg) => ({
|
|
206
358
|
value: pkg.path,
|
|
207
359
|
label: `${pkg.path} (${pkg.name})`,
|
|
208
|
-
hint:
|
|
360
|
+
hint: packageOverrideHint(pkg, defaults)
|
|
209
361
|
})),
|
|
210
|
-
{ value:
|
|
362
|
+
{ value: SENTINEL_DONE, label: "Done" }
|
|
211
363
|
]
|
|
212
364
|
});
|
|
213
365
|
assertNotCancelled(selectedPath);
|
|
214
|
-
if (selectedPath ===
|
|
366
|
+
if (selectedPath === SENTINEL_DONE) break;
|
|
215
367
|
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
216
368
|
if (!target) continue;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
369
|
+
await promptSinglePackageOverrides(target, defaults);
|
|
370
|
+
normalizePackageOverrides(editablePackages);
|
|
371
|
+
}
|
|
372
|
+
return normalizePackageOverrides(packages);
|
|
373
|
+
}
|
|
374
|
+
async function promptSinglePackageOverrides(target, defaults) {
|
|
375
|
+
while (true) {
|
|
376
|
+
const effectiveNaming = target.conventions?.fileNaming ?? defaults.fileNamingValue;
|
|
377
|
+
const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
|
|
378
|
+
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
379
|
+
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
380
|
+
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
|
|
381
|
+
const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
|
|
382
|
+
const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
|
|
383
|
+
const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
|
|
384
|
+
const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
|
|
385
|
+
const choice = await clack2.select({
|
|
386
|
+
message: `Edit overrides for ${target.path}`,
|
|
387
|
+
options: [
|
|
388
|
+
{ value: "fileNaming", label: "File naming", hint: namingHint },
|
|
389
|
+
{ value: "maxFileLines", label: "Max file lines", hint: maxLinesHint },
|
|
390
|
+
{ value: "testCoverage", label: "Test coverage", hint: String(effectiveCoverage) },
|
|
391
|
+
{ value: "summaryPath", label: "Coverage summary path", hint: effectiveSummary },
|
|
392
|
+
{ value: "command", label: "Coverage command", hint: effectiveCommand },
|
|
393
|
+
{ value: "reset", label: "Reset all overrides for this package" },
|
|
394
|
+
{ value: "back", label: "Back to package list" }
|
|
395
|
+
]
|
|
396
|
+
});
|
|
397
|
+
assertNotCancelled(choice);
|
|
398
|
+
if (choice === "back") break;
|
|
399
|
+
if (choice === "fileNaming") {
|
|
400
|
+
const selected = await clack2.select({
|
|
401
|
+
message: `File naming for ${target.path}`,
|
|
223
402
|
options: [
|
|
224
|
-
|
|
225
|
-
{ value:
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
]
|
|
230
|
-
});
|
|
231
|
-
assertNotCancelled(choice);
|
|
232
|
-
if (choice === "back") break;
|
|
233
|
-
if (choice === "testCoverage") {
|
|
234
|
-
const result = await clack2.text({
|
|
235
|
-
message: "Package testCoverage (0 to exempt package)?",
|
|
236
|
-
initialValue: String(effectiveCoverage),
|
|
237
|
-
validate: (v) => {
|
|
238
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
239
|
-
const n = Number.parseInt(v, 10);
|
|
240
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
assertNotCancelled(result);
|
|
244
|
-
const nextCoverage = Number.parseInt(result, 10);
|
|
245
|
-
if (nextCoverage === defaults.testCoverage) {
|
|
246
|
-
if (target.rules) {
|
|
247
|
-
delete target.rules.testCoverage;
|
|
403
|
+
...FILE_NAMING_OPTIONS,
|
|
404
|
+
{ value: SENTINEL_NONE, label: "(none \u2014 exempt from checks)" },
|
|
405
|
+
{
|
|
406
|
+
value: SENTINEL_INHERIT,
|
|
407
|
+
label: `Inherit default${defaults.fileNamingValue ? ` (${defaults.fileNamingValue})` : ""}`
|
|
248
408
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
409
|
+
],
|
|
410
|
+
initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
|
|
411
|
+
});
|
|
412
|
+
assertNotCancelled(selected);
|
|
413
|
+
if (selected === SENTINEL_INHERIT) {
|
|
414
|
+
if (target.conventions) delete target.conventions.fileNaming;
|
|
415
|
+
} else if (selected === SENTINEL_NONE) {
|
|
416
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: "" };
|
|
417
|
+
} else {
|
|
418
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: selected };
|
|
252
419
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
267
|
-
}
|
|
420
|
+
}
|
|
421
|
+
if (choice === "maxFileLines") {
|
|
422
|
+
const result = await clack2.text({
|
|
423
|
+
message: `Max file lines for ${target.path} (blank to inherit default)?`,
|
|
424
|
+
initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
|
|
425
|
+
placeholder: String(defaults.maxFileLines)
|
|
426
|
+
});
|
|
427
|
+
assertNotCancelled(result);
|
|
428
|
+
const value = result.trim();
|
|
429
|
+
if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
|
|
430
|
+
if (target.rules) delete target.rules.maxFileLines;
|
|
431
|
+
} else {
|
|
432
|
+
target.rules = { ...target.rules ?? {}, maxFileLines: Number.parseInt(value, 10) };
|
|
268
433
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (value.length === 0 || value === defaultCommand) {
|
|
279
|
-
if (target.coverage) {
|
|
280
|
-
delete target.coverage.command;
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
434
|
+
}
|
|
435
|
+
if (choice === "testCoverage") {
|
|
436
|
+
const result = await clack2.text({
|
|
437
|
+
message: "Package testCoverage (0 to exempt package)?",
|
|
438
|
+
initialValue: String(effectiveCoverage),
|
|
439
|
+
validate: (v) => {
|
|
440
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
441
|
+
const n = Number.parseInt(v, 10);
|
|
442
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
284
443
|
}
|
|
444
|
+
});
|
|
445
|
+
assertNotCancelled(result);
|
|
446
|
+
const nextCoverage = Number.parseInt(result, 10);
|
|
447
|
+
if (nextCoverage === defaults.testCoverage) {
|
|
448
|
+
if (target.rules) delete target.rules.testCoverage;
|
|
449
|
+
} else {
|
|
450
|
+
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
285
451
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
452
|
+
}
|
|
453
|
+
if (choice === "summaryPath") {
|
|
454
|
+
const result = await clack2.text({
|
|
455
|
+
message: "Path to coverage summary file (blank to inherit default)?",
|
|
456
|
+
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
457
|
+
placeholder: defaults.coverageSummaryPath
|
|
458
|
+
});
|
|
459
|
+
assertNotCancelled(result);
|
|
460
|
+
const value = result.trim();
|
|
461
|
+
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
462
|
+
if (target.coverage) delete target.coverage.summaryPath;
|
|
463
|
+
} else {
|
|
464
|
+
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (choice === "command") {
|
|
468
|
+
const result = await clack2.text({
|
|
469
|
+
message: "Coverage command (blank to auto-detect)?",
|
|
470
|
+
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
471
|
+
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
472
|
+
});
|
|
473
|
+
assertNotCancelled(result);
|
|
474
|
+
const value = result.trim();
|
|
475
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
476
|
+
if (value.length === 0 || value === defaultCommand) {
|
|
477
|
+
if (target.coverage) delete target.coverage.command;
|
|
478
|
+
} else {
|
|
479
|
+
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
291
480
|
}
|
|
292
|
-
|
|
481
|
+
}
|
|
482
|
+
if (choice === "reset") {
|
|
483
|
+
if (target.rules) {
|
|
484
|
+
delete target.rules.testCoverage;
|
|
485
|
+
delete target.rules.maxFileLines;
|
|
486
|
+
}
|
|
487
|
+
delete target.coverage;
|
|
488
|
+
delete target.conventions;
|
|
293
489
|
}
|
|
294
490
|
}
|
|
295
|
-
return normalizePackageOverrides(packages);
|
|
296
491
|
}
|
|
297
492
|
|
|
298
493
|
// src/utils/prompt-menu-handlers.ts
|
|
@@ -335,48 +530,21 @@ function getPackageDiffs(pkg, root) {
|
|
|
335
530
|
return diffs;
|
|
336
531
|
}
|
|
337
532
|
function buildMenuOptions(state, packageCount) {
|
|
338
|
-
const
|
|
533
|
+
const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
|
|
534
|
+
const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
|
|
535
|
+
const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
|
|
339
536
|
const options = [
|
|
340
|
-
{ value: "
|
|
341
|
-
{ value: "
|
|
537
|
+
{ value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
|
|
538
|
+
{ value: "naming", label: "Naming & conventions", hint: namingHint },
|
|
539
|
+
{ value: "testing", label: "Testing & coverage", hint: testingHint }
|
|
342
540
|
];
|
|
343
|
-
if (
|
|
541
|
+
if (packageCount > 0) {
|
|
344
542
|
options.push({
|
|
345
|
-
value: "
|
|
346
|
-
label: "
|
|
347
|
-
hint:
|
|
543
|
+
value: "packageOverrides",
|
|
544
|
+
label: "Per-package overrides",
|
|
545
|
+
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
348
546
|
});
|
|
349
547
|
}
|
|
350
|
-
const isMonorepo = packageCount > 0;
|
|
351
|
-
const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
|
|
352
|
-
const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
|
|
353
|
-
options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
|
|
354
|
-
options.push({
|
|
355
|
-
value: "enforceMissingTests",
|
|
356
|
-
label: "Enforce missing tests",
|
|
357
|
-
hint: state.enforceMissingTests ? "yes" : "no"
|
|
358
|
-
});
|
|
359
|
-
if (state.testCoverage > 0) {
|
|
360
|
-
options.push(
|
|
361
|
-
{
|
|
362
|
-
value: "coverageSummaryPath",
|
|
363
|
-
label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
|
|
364
|
-
hint: state.coverageSummaryPath
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
value: "coverageCommand",
|
|
368
|
-
label: isMonorepo ? "Default coverage command" : "Coverage command",
|
|
369
|
-
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
370
|
-
}
|
|
371
|
-
);
|
|
372
|
-
if (isMonorepo) {
|
|
373
|
-
options.push({
|
|
374
|
-
value: "packageOverrides",
|
|
375
|
-
label: "Per-package coverage overrides",
|
|
376
|
-
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
548
|
options.push(
|
|
381
549
|
{ value: "reset", label: "Reset all to detected defaults" },
|
|
382
550
|
{ value: "done", label: "Done" }
|
|
@@ -384,37 +552,43 @@ function buildMenuOptions(state, packageCount) {
|
|
|
384
552
|
return options;
|
|
385
553
|
}
|
|
386
554
|
function clonePackages(packages) {
|
|
387
|
-
return packages
|
|
388
|
-
...pkg,
|
|
389
|
-
stack: pkg.stack ? { ...pkg.stack } : void 0,
|
|
390
|
-
structure: pkg.structure ? { ...pkg.structure } : void 0,
|
|
391
|
-
conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
|
|
392
|
-
rules: pkg.rules ? { ...pkg.rules } : void 0,
|
|
393
|
-
coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
|
|
394
|
-
ignore: pkg.ignore ? [...pkg.ignore] : void 0,
|
|
395
|
-
boundaries: pkg.boundaries ? {
|
|
396
|
-
deny: [...pkg.boundaries.deny],
|
|
397
|
-
ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
|
|
398
|
-
} : void 0
|
|
399
|
-
}));
|
|
555
|
+
return packages ? structuredClone(packages) : void 0;
|
|
400
556
|
}
|
|
401
557
|
async function handleMenuChoice(choice, state, defaults, root) {
|
|
402
558
|
if (choice === "reset") {
|
|
403
559
|
state.maxFileLines = defaults.maxFileLines;
|
|
560
|
+
state.maxTestFileLines = defaults.maxTestFileLines;
|
|
404
561
|
state.testCoverage = defaults.testCoverage;
|
|
405
562
|
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
406
563
|
state.enforceNaming = defaults.enforceNaming;
|
|
407
564
|
state.fileNamingValue = defaults.fileNamingValue;
|
|
565
|
+
state.componentNaming = defaults.componentNaming;
|
|
566
|
+
state.hookNaming = defaults.hookNaming;
|
|
567
|
+
state.importAlias = defaults.importAlias;
|
|
408
568
|
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
409
569
|
state.coverageCommand = defaults.coverageCommand;
|
|
410
570
|
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
411
571
|
clack3.log.info("Reset all rules to detected defaults.");
|
|
412
572
|
return;
|
|
413
573
|
}
|
|
574
|
+
if (choice === "fileLimits") {
|
|
575
|
+
await promptFileLimitsMenu(state);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (choice === "naming") {
|
|
579
|
+
await promptNamingMenu(state);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (choice === "testing") {
|
|
583
|
+
await promptTestingMenu(state);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
414
586
|
if (choice === "packageOverrides") {
|
|
415
587
|
if (state.packageOverrides) {
|
|
416
588
|
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
417
|
-
state.packageOverrides = await
|
|
589
|
+
state.packageOverrides = await promptPackageOverrides(state.packageOverrides, {
|
|
590
|
+
fileNamingValue: state.fileNamingValue,
|
|
591
|
+
maxFileLines: state.maxFileLines,
|
|
418
592
|
testCoverage: state.testCoverage,
|
|
419
593
|
coverageSummaryPath: state.coverageSummaryPath,
|
|
420
594
|
coverageCommand: state.coverageCommand
|
|
@@ -427,89 +601,9 @@ async function handleMenuChoice(choice, state, defaults, root) {
|
|
|
427
601
|
}
|
|
428
602
|
return;
|
|
429
603
|
}
|
|
430
|
-
if (choice === "maxFileLines") {
|
|
431
|
-
const result = await clack3.text({
|
|
432
|
-
message: "Maximum lines per source file?",
|
|
433
|
-
initialValue: String(state.maxFileLines),
|
|
434
|
-
validate: (v) => {
|
|
435
|
-
if (typeof v !== "string") return "Enter a positive number";
|
|
436
|
-
const n = Number.parseInt(v, 10);
|
|
437
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
assertNotCancelled(result);
|
|
441
|
-
state.maxFileLines = Number.parseInt(result, 10);
|
|
442
|
-
}
|
|
443
|
-
if (choice === "enforceMissingTests") {
|
|
444
|
-
const result = await clack3.confirm({
|
|
445
|
-
message: "Require every source file to have a corresponding test file?",
|
|
446
|
-
initialValue: state.enforceMissingTests
|
|
447
|
-
});
|
|
448
|
-
assertNotCancelled(result);
|
|
449
|
-
state.enforceMissingTests = result;
|
|
450
|
-
}
|
|
451
|
-
if (choice === "testCoverage") {
|
|
452
|
-
const result = await clack3.text({
|
|
453
|
-
message: "Test coverage target (0 disables coverage checks)?",
|
|
454
|
-
initialValue: String(state.testCoverage),
|
|
455
|
-
validate: (v) => {
|
|
456
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
457
|
-
const n = Number.parseInt(v, 10);
|
|
458
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
assertNotCancelled(result);
|
|
462
|
-
state.testCoverage = Number.parseInt(result, 10);
|
|
463
|
-
}
|
|
464
|
-
if (choice === "coverageSummaryPath") {
|
|
465
|
-
const result = await clack3.text({
|
|
466
|
-
message: "Coverage summary path (relative to package root)?",
|
|
467
|
-
initialValue: state.coverageSummaryPath,
|
|
468
|
-
validate: (v) => {
|
|
469
|
-
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
assertNotCancelled(result);
|
|
473
|
-
state.coverageSummaryPath = result.trim();
|
|
474
|
-
}
|
|
475
|
-
if (choice === "coverageCommand") {
|
|
476
|
-
const result = await clack3.text({
|
|
477
|
-
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
478
|
-
initialValue: state.coverageCommand ?? "",
|
|
479
|
-
placeholder: "(auto-detect from package.json test runner)"
|
|
480
|
-
});
|
|
481
|
-
assertNotCancelled(result);
|
|
482
|
-
const trimmed = result.trim();
|
|
483
|
-
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
484
|
-
}
|
|
485
|
-
if (choice === "enforceNaming") {
|
|
486
|
-
const result = await clack3.confirm({
|
|
487
|
-
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
488
|
-
initialValue: state.enforceNaming
|
|
489
|
-
});
|
|
490
|
-
assertNotCancelled(result);
|
|
491
|
-
state.enforceNaming = result;
|
|
492
|
-
}
|
|
493
|
-
if (choice === "fileNaming") {
|
|
494
|
-
const selected = await clack3.select({
|
|
495
|
-
message: "Which file naming convention should be enforced?",
|
|
496
|
-
options: [
|
|
497
|
-
{ value: "kebab-case", label: "kebab-case" },
|
|
498
|
-
{ value: "camelCase", label: "camelCase" },
|
|
499
|
-
{ value: "PascalCase", label: "PascalCase" },
|
|
500
|
-
{ value: "snake_case", label: "snake_case" }
|
|
501
|
-
],
|
|
502
|
-
initialValue: state.fileNamingValue
|
|
503
|
-
});
|
|
504
|
-
assertNotCancelled(selected);
|
|
505
|
-
state.fileNamingValue = selected;
|
|
506
|
-
}
|
|
507
604
|
}
|
|
508
605
|
|
|
509
606
|
// src/utils/prompt-rules.ts
|
|
510
|
-
function getRootPackage(packages) {
|
|
511
|
-
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
512
|
-
}
|
|
513
607
|
async function promptRuleMenu(defaults) {
|
|
514
608
|
const state = {
|
|
515
609
|
...defaults,
|
|
@@ -526,10 +620,14 @@ async function promptRuleMenu(defaults) {
|
|
|
526
620
|
}
|
|
527
621
|
return {
|
|
528
622
|
maxFileLines: state.maxFileLines,
|
|
623
|
+
maxTestFileLines: state.maxTestFileLines,
|
|
529
624
|
testCoverage: state.testCoverage,
|
|
530
625
|
enforceMissingTests: state.enforceMissingTests,
|
|
531
626
|
enforceNaming: state.enforceNaming,
|
|
532
627
|
fileNamingValue: state.fileNamingValue,
|
|
628
|
+
componentNaming: state.componentNaming,
|
|
629
|
+
hookNaming: state.hookNaming,
|
|
630
|
+
importAlias: state.importAlias,
|
|
533
631
|
coverageSummaryPath: state.coverageSummaryPath,
|
|
534
632
|
coverageCommand: state.coverageCommand,
|
|
535
633
|
packageOverrides: state.packageOverrides
|
|
@@ -577,30 +675,6 @@ async function promptExistingConfigAction(configFile) {
|
|
|
577
675
|
assertNotCancelled(result);
|
|
578
676
|
return result;
|
|
579
677
|
}
|
|
580
|
-
async function promptInitDecision() {
|
|
581
|
-
const result = await clack5.select({
|
|
582
|
-
message: "How do you want to proceed?",
|
|
583
|
-
options: [
|
|
584
|
-
{
|
|
585
|
-
value: "accept",
|
|
586
|
-
label: "Accept defaults",
|
|
587
|
-
hint: "writes the config with these defaults; use --enforce in CI to block"
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
value: "customize",
|
|
591
|
-
label: "Customize rules",
|
|
592
|
-
hint: "edit limits, naming, test coverage, and package overrides"
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
value: "review",
|
|
596
|
-
label: "Review detected details",
|
|
597
|
-
hint: "show the full scan report with package and structure details"
|
|
598
|
-
}
|
|
599
|
-
]
|
|
600
|
-
});
|
|
601
|
-
assertNotCancelled(result);
|
|
602
|
-
return result;
|
|
603
|
-
}
|
|
604
678
|
|
|
605
679
|
// src/utils/resolve-workspace-packages.ts
|
|
606
680
|
import * as fs2 from "fs";
|
|
@@ -1218,13 +1292,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1218
1292
|
const testSuffix = testPattern.replace("*", "");
|
|
1219
1293
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1220
1294
|
for (const relFile of sourceFiles) {
|
|
1221
|
-
const
|
|
1222
|
-
if (
|
|
1295
|
+
const basename10 = path6.basename(relFile);
|
|
1296
|
+
if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
|
|
1223
1297
|
continue;
|
|
1224
1298
|
}
|
|
1225
|
-
const ext = path6.extname(
|
|
1299
|
+
const ext = path6.extname(basename10);
|
|
1226
1300
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1227
|
-
const stem =
|
|
1301
|
+
const stem = basename10.slice(0, -ext.length);
|
|
1228
1302
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1229
1303
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1230
1304
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
@@ -1305,9 +1379,9 @@ async function checkCommand(options, cwd) {
|
|
|
1305
1379
|
}
|
|
1306
1380
|
const violations = [];
|
|
1307
1381
|
const severity = options.enforce ? "error" : "warn";
|
|
1308
|
-
const
|
|
1382
|
+
const log8 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(chalk3.dim(msg)) : () => {
|
|
1309
1383
|
};
|
|
1310
|
-
|
|
1384
|
+
log8(" Checking files...");
|
|
1311
1385
|
for (const file of filesToCheck) {
|
|
1312
1386
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1313
1387
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1340,9 +1414,9 @@ async function checkCommand(options, cwd) {
|
|
|
1340
1414
|
}
|
|
1341
1415
|
}
|
|
1342
1416
|
}
|
|
1343
|
-
|
|
1417
|
+
log8(" done\n");
|
|
1344
1418
|
if (!options.files) {
|
|
1345
|
-
|
|
1419
|
+
log8(" Checking missing tests...");
|
|
1346
1420
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1347
1421
|
if (options.staged) {
|
|
1348
1422
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1355,14 +1429,14 @@ async function checkCommand(options, cwd) {
|
|
|
1355
1429
|
} else {
|
|
1356
1430
|
violations.push(...testViolations);
|
|
1357
1431
|
}
|
|
1358
|
-
|
|
1432
|
+
log8(" done\n");
|
|
1359
1433
|
}
|
|
1360
1434
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1361
|
-
|
|
1435
|
+
log8(" Running test coverage...\n");
|
|
1362
1436
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1363
1437
|
staged: options.staged,
|
|
1364
1438
|
enforce: options.enforce,
|
|
1365
|
-
onProgress: (pkg) =>
|
|
1439
|
+
onProgress: (pkg) => log8(` Coverage: ${pkg}...
|
|
1366
1440
|
`)
|
|
1367
1441
|
});
|
|
1368
1442
|
violations.push(...coverageViolations);
|
|
@@ -1387,7 +1461,7 @@ async function checkCommand(options, cwd) {
|
|
|
1387
1461
|
severity
|
|
1388
1462
|
});
|
|
1389
1463
|
}
|
|
1390
|
-
|
|
1464
|
+
log8(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1391
1465
|
`);
|
|
1392
1466
|
}
|
|
1393
1467
|
if (options.format === "json") {
|
|
@@ -1654,15 +1728,6 @@ function formatMonorepoResultsText(scanResult) {
|
|
|
1654
1728
|
}
|
|
1655
1729
|
|
|
1656
1730
|
// src/display.ts
|
|
1657
|
-
var INIT_OVERVIEW_NAMES = {
|
|
1658
|
-
typescript: "TypeScript",
|
|
1659
|
-
javascript: "JavaScript",
|
|
1660
|
-
eslint: "ESLint",
|
|
1661
|
-
prettier: "Prettier",
|
|
1662
|
-
jest: "Jest",
|
|
1663
|
-
vitest: "Vitest",
|
|
1664
|
-
biome: "Biome"
|
|
1665
|
-
};
|
|
1666
1731
|
function formatItem(item, nameMap) {
|
|
1667
1732
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1668
1733
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1792,134 +1857,6 @@ function displayRulesPreview(config) {
|
|
|
1792
1857
|
);
|
|
1793
1858
|
console.log("");
|
|
1794
1859
|
}
|
|
1795
|
-
function formatDetectedOverview(scanResult) {
|
|
1796
|
-
const { stack } = scanResult;
|
|
1797
|
-
const primaryParts = [];
|
|
1798
|
-
const secondaryParts = [];
|
|
1799
|
-
const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
|
|
1800
|
-
if (scanResult.packages.length > 1) {
|
|
1801
|
-
primaryParts.push("monorepo");
|
|
1802
|
-
primaryParts.push(`${scanResult.packages.length} packages`);
|
|
1803
|
-
} else if (stack.framework) {
|
|
1804
|
-
primaryParts.push(formatItem(stack.framework, FRAMEWORK_NAMES2));
|
|
1805
|
-
} else {
|
|
1806
|
-
primaryParts.push("single package");
|
|
1807
|
-
}
|
|
1808
|
-
primaryParts.push(formatOverviewItem(stack.language));
|
|
1809
|
-
if (stack.styling) {
|
|
1810
|
-
primaryParts.push(formatOverviewItem(stack.styling, STYLING_NAMES2));
|
|
1811
|
-
}
|
|
1812
|
-
if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
|
|
1813
|
-
if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
|
|
1814
|
-
if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
|
|
1815
|
-
if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
|
|
1816
|
-
const primary = primaryParts.map((part) => chalk5.cyan(part)).join(chalk5.dim(" \xB7 "));
|
|
1817
|
-
const secondary = secondaryParts.join(chalk5.dim(" \xB7 "));
|
|
1818
|
-
return secondary ? `${primary}
|
|
1819
|
-
${chalk5.dim(secondary)}` : primary;
|
|
1820
|
-
}
|
|
1821
|
-
function displayInitOverview(scanResult, config, exemptedPackages) {
|
|
1822
|
-
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1823
|
-
const isMonorepo = config.packages.length > 1;
|
|
1824
|
-
const ok = chalk5.green("\u2713");
|
|
1825
|
-
const info = chalk5.yellow("~");
|
|
1826
|
-
console.log("");
|
|
1827
|
-
console.log(` ${chalk5.bold("Ready to initialize:")}`);
|
|
1828
|
-
console.log(` ${formatDetectedOverview(scanResult)}`);
|
|
1829
|
-
console.log("");
|
|
1830
|
-
console.log(` ${chalk5.bold("Rules to apply:")}`);
|
|
1831
|
-
console.log(` ${ok} Max file size: ${chalk5.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1832
|
-
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1833
|
-
if (config.rules.enforceNaming && fileNaming) {
|
|
1834
|
-
console.log(` ${ok} File naming: ${chalk5.cyan(fileNaming)}`);
|
|
1835
|
-
} else {
|
|
1836
|
-
console.log(` ${info} File naming: ${chalk5.dim("not enforced")}`);
|
|
1837
|
-
}
|
|
1838
|
-
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1839
|
-
if (config.rules.enforceMissingTests && testPattern) {
|
|
1840
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan(`enforced (${testPattern})`)}`);
|
|
1841
|
-
} else if (config.rules.enforceMissingTests) {
|
|
1842
|
-
console.log(` ${ok} Missing tests: ${chalk5.cyan("enforced")}`);
|
|
1843
|
-
} else {
|
|
1844
|
-
console.log(` ${info} Missing tests: ${chalk5.dim("not enforced")}`);
|
|
1845
|
-
}
|
|
1846
|
-
if (config.rules.testCoverage > 0) {
|
|
1847
|
-
if (isMonorepo) {
|
|
1848
|
-
const withCoverage = config.packages.filter(
|
|
1849
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1850
|
-
);
|
|
1851
|
-
console.log(
|
|
1852
|
-
` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)} default ${chalk5.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1853
|
-
);
|
|
1854
|
-
} else {
|
|
1855
|
-
console.log(` ${ok} Coverage: ${chalk5.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1856
|
-
}
|
|
1857
|
-
} else {
|
|
1858
|
-
console.log(` ${info} Coverage: ${chalk5.dim("disabled")}`);
|
|
1859
|
-
}
|
|
1860
|
-
if (exemptedPackages.length > 0) {
|
|
1861
|
-
console.log(
|
|
1862
|
-
` ${chalk5.dim(" exempted:")} ${chalk5.dim(exemptedPackages.join(", "))} ${chalk5.dim("(types-only)")}`
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
console.log("");
|
|
1866
|
-
console.log(` ${chalk5.bold("Also available:")}`);
|
|
1867
|
-
if (isMonorepo) {
|
|
1868
|
-
console.log(` ${info} Infer boundaries from current imports`);
|
|
1869
|
-
}
|
|
1870
|
-
console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
|
|
1871
|
-
console.log(
|
|
1872
|
-
`
|
|
1873
|
-
${chalk5.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
|
|
1874
|
-
);
|
|
1875
|
-
console.log("");
|
|
1876
|
-
}
|
|
1877
|
-
function summarizeSelectedIntegrations(integrations, opts) {
|
|
1878
|
-
const lines = [];
|
|
1879
|
-
if (opts.hasBoundaries) {
|
|
1880
|
-
lines.push("\u2713 Boundary rules: inferred from current imports");
|
|
1881
|
-
} else {
|
|
1882
|
-
lines.push("~ Boundary rules: not enabled");
|
|
1883
|
-
}
|
|
1884
|
-
if (opts.hasCoverage) {
|
|
1885
|
-
lines.push("\u2713 Coverage checks: enabled");
|
|
1886
|
-
} else {
|
|
1887
|
-
lines.push("~ Coverage checks: disabled");
|
|
1888
|
-
}
|
|
1889
|
-
const selectedIntegrations = [
|
|
1890
|
-
integrations.preCommitHook ? "pre-commit hook" : void 0,
|
|
1891
|
-
integrations.typecheckHook ? "typecheck" : void 0,
|
|
1892
|
-
integrations.lintHook ? "lint check" : void 0,
|
|
1893
|
-
integrations.claudeCodeHook ? "Claude Code hook" : void 0,
|
|
1894
|
-
integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
|
|
1895
|
-
integrations.githubAction ? "GitHub Actions workflow" : void 0
|
|
1896
|
-
].filter(Boolean);
|
|
1897
|
-
if (selectedIntegrations.length > 0) {
|
|
1898
|
-
lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
|
|
1899
|
-
} else {
|
|
1900
|
-
lines.push("~ Integrations: none selected");
|
|
1901
|
-
}
|
|
1902
|
-
return lines;
|
|
1903
|
-
}
|
|
1904
|
-
function displaySetupPlan(config, integrations, opts = {}) {
|
|
1905
|
-
const configFile = opts.configFile ?? "viberails.config.json";
|
|
1906
|
-
const lines = summarizeSelectedIntegrations(integrations, {
|
|
1907
|
-
hasBoundaries: config.rules.enforceBoundaries,
|
|
1908
|
-
hasCoverage: config.rules.testCoverage > 0
|
|
1909
|
-
});
|
|
1910
|
-
console.log("");
|
|
1911
|
-
console.log(` ${chalk5.bold("Ready to write:")}`);
|
|
1912
|
-
console.log(
|
|
1913
|
-
` ${opts.replacingExistingConfig ? chalk5.yellow("!") : chalk5.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? chalk5.dim(" (replacing existing config)") : ""}`
|
|
1914
|
-
);
|
|
1915
|
-
console.log(` ${chalk5.green("\u2713")} .viberails/context.md`);
|
|
1916
|
-
console.log(` ${chalk5.green("\u2713")} .viberails/scan-result.json`);
|
|
1917
|
-
for (const line of lines) {
|
|
1918
|
-
const icon = line.startsWith("\u2713") ? chalk5.green("\u2713") : chalk5.yellow("~");
|
|
1919
|
-
console.log(` ${icon} ${line.slice(2)}`);
|
|
1920
|
-
}
|
|
1921
|
-
console.log("");
|
|
1922
|
-
}
|
|
1923
1860
|
|
|
1924
1861
|
// src/display-text.ts
|
|
1925
1862
|
function plainConfidenceLabel(convention) {
|
|
@@ -2038,7 +1975,9 @@ function formatScanResultsText(scanResult) {
|
|
|
2038
1975
|
// src/utils/apply-rule-overrides.ts
|
|
2039
1976
|
function applyRuleOverrides(config, overrides) {
|
|
2040
1977
|
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1978
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2041
1979
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
1980
|
+
config.rules.maxTestFileLines = overrides.maxTestFileLines;
|
|
2042
1981
|
config.rules.testCoverage = overrides.testCoverage;
|
|
2043
1982
|
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
2044
1983
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
@@ -2052,7 +1991,6 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2052
1991
|
}
|
|
2053
1992
|
}
|
|
2054
1993
|
if (overrides.fileNamingValue) {
|
|
2055
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2056
1994
|
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
2057
1995
|
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2058
1996
|
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
@@ -2064,6 +2002,18 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2064
2002
|
}
|
|
2065
2003
|
}
|
|
2066
2004
|
}
|
|
2005
|
+
if (rootPkg) {
|
|
2006
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2007
|
+
if (overrides.componentNaming !== void 0) {
|
|
2008
|
+
rootPkg.conventions.componentNaming = overrides.componentNaming || void 0;
|
|
2009
|
+
}
|
|
2010
|
+
if (overrides.hookNaming !== void 0) {
|
|
2011
|
+
rootPkg.conventions.hookNaming = overrides.hookNaming || void 0;
|
|
2012
|
+
}
|
|
2013
|
+
if (overrides.importAlias !== void 0) {
|
|
2014
|
+
rootPkg.conventions.importAlias = overrides.importAlias || void 0;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2067
2017
|
}
|
|
2068
2018
|
|
|
2069
2019
|
// src/utils/diff-configs.ts
|
|
@@ -2240,10 +2190,14 @@ async function configCommand(options, cwd) {
|
|
|
2240
2190
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2241
2191
|
const overrides = await promptRuleMenu({
|
|
2242
2192
|
maxFileLines: config.rules.maxFileLines,
|
|
2193
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2243
2194
|
testCoverage: config.rules.testCoverage,
|
|
2244
2195
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2245
2196
|
enforceNaming: config.rules.enforceNaming,
|
|
2246
2197
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2198
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2199
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2200
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
2247
2201
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
2248
2202
|
coverageCommand: config.defaults?.coverage?.command,
|
|
2249
2203
|
packageOverrides: config.packages
|
|
@@ -2624,10 +2578,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2624
2578
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2625
2579
|
const testPattern = pkg?.structure?.testPattern;
|
|
2626
2580
|
if (!testPattern) return null;
|
|
2627
|
-
const
|
|
2628
|
-
const ext = path12.extname(
|
|
2581
|
+
const basename10 = path12.basename(sourceRelPath);
|
|
2582
|
+
const ext = path12.extname(basename10);
|
|
2629
2583
|
if (!ext) return null;
|
|
2630
|
-
const stem =
|
|
2584
|
+
const stem = basename10.slice(0, -ext.length);
|
|
2631
2585
|
const testSuffix = testPattern.replace("*", "");
|
|
2632
2586
|
const testFilename = `${stem}${testSuffix}`;
|
|
2633
2587
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2799,169 +2753,601 @@ ${chalk8.yellow("!")} No safe fixes to apply. Resolve aliased imports first.`);
|
|
|
2799
2753
|
}
|
|
2800
2754
|
|
|
2801
2755
|
// src/commands/init.ts
|
|
2802
|
-
import * as
|
|
2803
|
-
import * as
|
|
2756
|
+
import * as fs21 from "fs";
|
|
2757
|
+
import * as path21 from "path";
|
|
2758
|
+
import * as clack12 from "@clack/prompts";
|
|
2759
|
+
import { compactConfig as compactConfig4, generateConfig as generateConfig2 } from "@viberails/config";
|
|
2760
|
+
import { scan as scan3 } from "@viberails/scanner";
|
|
2761
|
+
import chalk13 from "chalk";
|
|
2762
|
+
|
|
2763
|
+
// src/utils/check-prerequisites.ts
|
|
2764
|
+
import * as fs14 from "fs";
|
|
2765
|
+
import * as path14 from "path";
|
|
2766
|
+
import * as clack7 from "@clack/prompts";
|
|
2767
|
+
import chalk9 from "chalk";
|
|
2768
|
+
|
|
2769
|
+
// src/utils/spawn-async.ts
|
|
2770
|
+
import { spawn } from "child_process";
|
|
2771
|
+
function spawnAsync(command, cwd) {
|
|
2772
|
+
return new Promise((resolve4) => {
|
|
2773
|
+
const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
|
|
2774
|
+
let stdout = "";
|
|
2775
|
+
let stderr = "";
|
|
2776
|
+
child.stdout.on("data", (d) => {
|
|
2777
|
+
stdout += d.toString();
|
|
2778
|
+
});
|
|
2779
|
+
child.stderr.on("data", (d) => {
|
|
2780
|
+
stderr += d.toString();
|
|
2781
|
+
});
|
|
2782
|
+
child.on("close", (status) => {
|
|
2783
|
+
resolve4({ status, stdout, stderr });
|
|
2784
|
+
});
|
|
2785
|
+
child.on("error", () => {
|
|
2786
|
+
resolve4({ status: 1, stdout, stderr });
|
|
2787
|
+
});
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// src/utils/check-prerequisites.ts
|
|
2792
|
+
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2793
|
+
const pm = scanResult.stack.packageManager.name;
|
|
2794
|
+
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
2795
|
+
const hasVitest = vitestPackages.length > 0 || scanResult.stack.testRunner?.name === "vitest";
|
|
2796
|
+
if (!hasVitest) return [];
|
|
2797
|
+
let installed = hasDependency(projectRoot, "@vitest/coverage-v8") || hasDependency(projectRoot, "@vitest/coverage-istanbul");
|
|
2798
|
+
if (!installed && vitestPackages.length > 0) {
|
|
2799
|
+
installed = vitestPackages.every((rel) => {
|
|
2800
|
+
const pkgDir = path14.join(projectRoot, rel);
|
|
2801
|
+
return hasDependency(pkgDir, "@vitest/coverage-v8") || hasDependency(pkgDir, "@vitest/coverage-istanbul");
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
const isWorkspace = scanResult.packages.length > 1;
|
|
2805
|
+
const addCmd = pm === "yarn" ? "yarn add -D" : pm === "pnpm" && isWorkspace ? "pnpm add -D -w" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
|
|
2806
|
+
const affectedPackages = vitestPackages.length > 1 ? vitestPackages : void 0;
|
|
2807
|
+
const reason = affectedPackages ? `Required for coverage in: ${affectedPackages.join(", ")}` : "Required for coverage percentage checks with vitest";
|
|
2808
|
+
return [
|
|
2809
|
+
{
|
|
2810
|
+
label: "@vitest/coverage-v8",
|
|
2811
|
+
installed,
|
|
2812
|
+
installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
|
|
2813
|
+
reason,
|
|
2814
|
+
affectedPackages
|
|
2815
|
+
}
|
|
2816
|
+
];
|
|
2817
|
+
}
|
|
2818
|
+
function displayMissingPrereqs(prereqs) {
|
|
2819
|
+
const missing = prereqs.filter((p) => !p.installed);
|
|
2820
|
+
for (const m of missing) {
|
|
2821
|
+
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2822
|
+
console.log(` ${chalk9.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2823
|
+
if (m.installCommand) {
|
|
2824
|
+
console.log(` Install: ${chalk9.cyan(m.installCommand)}`);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function planCoverageInstall(prereqs) {
|
|
2829
|
+
const missing = prereqs.find((p) => !p.installed && p.installCommand);
|
|
2830
|
+
if (!missing?.installCommand) return void 0;
|
|
2831
|
+
return {
|
|
2832
|
+
label: missing.label,
|
|
2833
|
+
command: missing.installCommand
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
function hasDependency(projectRoot, name) {
|
|
2837
|
+
try {
|
|
2838
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
2839
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
2840
|
+
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2841
|
+
} catch {
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/utils/deferred-install.ts
|
|
2804
2847
|
import * as clack8 from "@clack/prompts";
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2848
|
+
async function executeDeferredInstalls(projectRoot, installs) {
|
|
2849
|
+
if (installs.length === 0) return 0;
|
|
2850
|
+
let successCount = 0;
|
|
2851
|
+
for (const install of installs) {
|
|
2852
|
+
const s = clack8.spinner();
|
|
2853
|
+
s.start(`Installing ${install.label}...`);
|
|
2854
|
+
const result = await spawnAsync(install.command, projectRoot);
|
|
2855
|
+
if (result.status === 0) {
|
|
2856
|
+
s.stop(`Installed ${install.label}`);
|
|
2857
|
+
install.onSuccess?.();
|
|
2858
|
+
successCount++;
|
|
2859
|
+
} else {
|
|
2860
|
+
s.stop(`Failed to install ${install.label}`);
|
|
2861
|
+
clack8.log.warn(`Install manually: ${install.command}`);
|
|
2862
|
+
install.onFailure?.();
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return successCount;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/utils/prompt-main-menu.ts
|
|
2869
|
+
import * as clack10 from "@clack/prompts";
|
|
2870
|
+
|
|
2871
|
+
// src/utils/prompt-integrations.ts
|
|
2872
|
+
import * as fs15 from "fs";
|
|
2873
|
+
import * as path15 from "path";
|
|
2874
|
+
import * as clack9 from "@clack/prompts";
|
|
2875
|
+
function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
2876
|
+
if (pm === "yarn") return "yarn add -D lefthook";
|
|
2877
|
+
if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
|
|
2878
|
+
if (pm === "npm") return "npm install -D lefthook";
|
|
2879
|
+
return `${pm} add -D lefthook`;
|
|
2880
|
+
}
|
|
2881
|
+
async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
|
|
2882
|
+
const options = [];
|
|
2883
|
+
const needsLefthook = !hookManager;
|
|
2884
|
+
if (needsLefthook) {
|
|
2885
|
+
const pm = packageManager ?? "npm";
|
|
2886
|
+
options.push({
|
|
2887
|
+
value: "installLefthook",
|
|
2888
|
+
label: "Install Lefthook",
|
|
2889
|
+
hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2893
|
+
const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
|
|
2894
|
+
options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
|
|
2895
|
+
if (tools?.isTypeScript) {
|
|
2896
|
+
options.push({
|
|
2897
|
+
value: "typecheck",
|
|
2898
|
+
label: "Typecheck (tsc --noEmit)",
|
|
2899
|
+
hint: "pre-commit hook + CI check"
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
if (tools?.linter) {
|
|
2903
|
+
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
2904
|
+
options.push({
|
|
2905
|
+
value: "lint",
|
|
2906
|
+
label: `Lint check (${linterName})`,
|
|
2907
|
+
hint: "pre-commit hook + CI check"
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
options.push(
|
|
2911
|
+
{
|
|
2912
|
+
value: "claude",
|
|
2913
|
+
label: "Claude Code hook",
|
|
2914
|
+
hint: "checks files when Claude edits them"
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
value: "claudeMd",
|
|
2918
|
+
label: "CLAUDE.md reference",
|
|
2919
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
value: "githubAction",
|
|
2923
|
+
label: "GitHub Actions workflow",
|
|
2924
|
+
hint: "blocks PRs that fail viberails check"
|
|
2925
|
+
}
|
|
2926
|
+
);
|
|
2927
|
+
const initialValues = options.map((o) => o.value);
|
|
2928
|
+
const result = await clack9.multiselect({
|
|
2929
|
+
message: "Integrations",
|
|
2930
|
+
options,
|
|
2931
|
+
initialValues,
|
|
2932
|
+
required: false
|
|
2933
|
+
});
|
|
2934
|
+
assertNotCancelled(result);
|
|
2935
|
+
let lefthookInstall;
|
|
2936
|
+
if (needsLefthook && result.includes("installLefthook")) {
|
|
2937
|
+
const pm = packageManager ?? "npm";
|
|
2938
|
+
lefthookInstall = {
|
|
2939
|
+
label: "Lefthook",
|
|
2940
|
+
command: buildLefthookInstallCommand(pm, isWorkspace),
|
|
2941
|
+
onSuccess: projectRoot ? () => {
|
|
2942
|
+
const ymlPath = path15.join(projectRoot, "lefthook.yml");
|
|
2943
|
+
if (!fs15.existsSync(ymlPath)) {
|
|
2944
|
+
fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
|
|
2945
|
+
}
|
|
2946
|
+
} : void 0
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
return {
|
|
2950
|
+
choice: {
|
|
2951
|
+
preCommitHook: result.includes("preCommit"),
|
|
2952
|
+
claudeCodeHook: result.includes("claude"),
|
|
2953
|
+
claudeMdRef: result.includes("claudeMd"),
|
|
2954
|
+
githubAction: result.includes("githubAction"),
|
|
2955
|
+
typecheckHook: result.includes("typecheck"),
|
|
2956
|
+
lintHook: result.includes("lint")
|
|
2957
|
+
},
|
|
2958
|
+
lefthookInstall
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// src/utils/prompt-main-menu-hints.ts
|
|
2963
|
+
function fileLimitsHint(config) {
|
|
2964
|
+
const max = config.rules.maxFileLines;
|
|
2965
|
+
const test = config.rules.maxTestFileLines;
|
|
2966
|
+
return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
|
|
2967
|
+
}
|
|
2968
|
+
function fileNamingHint(config, scanResult) {
|
|
2969
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2970
|
+
const naming = rootPkg.conventions?.fileNaming;
|
|
2971
|
+
if (!config.rules.enforceNaming) return "not enforced";
|
|
2972
|
+
if (naming) {
|
|
2973
|
+
const detected = scanResult.packages.some(
|
|
2974
|
+
(p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
|
|
2975
|
+
);
|
|
2976
|
+
return detected ? `${naming} (detected)` : naming;
|
|
2977
|
+
}
|
|
2978
|
+
return "mixed \u2014 will not enforce if skipped";
|
|
2979
|
+
}
|
|
2980
|
+
function fileNamingStatus(config) {
|
|
2981
|
+
if (!config.rules.enforceNaming) return "disabled";
|
|
2982
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2983
|
+
return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
|
|
2984
|
+
}
|
|
2985
|
+
function missingTestsHint(config) {
|
|
2986
|
+
if (!config.rules.enforceMissingTests) return "not enforced";
|
|
2987
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2988
|
+
const pattern = rootPkg.structure?.testPattern;
|
|
2989
|
+
return pattern ? `enforced (${pattern})` : "enforced";
|
|
2990
|
+
}
|
|
2991
|
+
function coverageHint(config, hasTestRunner) {
|
|
2992
|
+
if (config.rules.testCoverage === 0) return "disabled";
|
|
2993
|
+
if (!hasTestRunner)
|
|
2994
|
+
return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
|
|
2995
|
+
const isMonorepo = config.packages.length > 1;
|
|
2996
|
+
if (isMonorepo) {
|
|
2997
|
+
const withCov = config.packages.filter(
|
|
2998
|
+
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
2999
|
+
);
|
|
3000
|
+
const exempt = config.packages.length - withCov.length;
|
|
3001
|
+
return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
|
|
3002
|
+
}
|
|
3003
|
+
return `${config.rules.testCoverage}%`;
|
|
3004
|
+
}
|
|
3005
|
+
function advancedNamingHint(config) {
|
|
3006
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3007
|
+
const parts = [];
|
|
3008
|
+
if (rootPkg.conventions?.componentNaming)
|
|
3009
|
+
parts.push(`${rootPkg.conventions.componentNaming} components`);
|
|
3010
|
+
if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
|
|
3011
|
+
if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
|
|
3012
|
+
return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
|
|
3013
|
+
}
|
|
3014
|
+
function integrationsHint(state) {
|
|
3015
|
+
if (!state.visited.integrations || !state.integrations)
|
|
3016
|
+
return "not configured \u2014 select to set up";
|
|
3017
|
+
const items = [];
|
|
3018
|
+
if (state.integrations.preCommitHook) items.push("pre-commit");
|
|
3019
|
+
if (state.integrations.typecheckHook) items.push("typecheck");
|
|
3020
|
+
if (state.integrations.lintHook) items.push("lint");
|
|
3021
|
+
if (state.integrations.claudeCodeHook) items.push("Claude");
|
|
3022
|
+
if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
|
|
3023
|
+
if (state.integrations.githubAction) items.push("CI");
|
|
3024
|
+
return items.length > 0 ? items.join(" \xB7 ") : "none selected";
|
|
3025
|
+
}
|
|
3026
|
+
function packageOverridesHint(config) {
|
|
3027
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3028
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3029
|
+
const customized = editable.filter(
|
|
3030
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3031
|
+
).length;
|
|
3032
|
+
return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
|
|
3033
|
+
}
|
|
3034
|
+
function boundariesHint(config, state) {
|
|
3035
|
+
if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
|
|
3036
|
+
const deny = config.boundaries?.deny;
|
|
3037
|
+
if (!deny) return "enabled";
|
|
3038
|
+
const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
|
|
3039
|
+
const pkgCount = Object.keys(deny).length;
|
|
3040
|
+
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3041
|
+
}
|
|
3042
|
+
function statusIcon(status) {
|
|
3043
|
+
if (status === "ok") return "\u2713";
|
|
3044
|
+
if (status === "needs-input") return "?";
|
|
3045
|
+
return "~";
|
|
3046
|
+
}
|
|
3047
|
+
function buildMainMenuOptions(config, scanResult, state) {
|
|
3048
|
+
const namingStatus = fileNamingStatus(config);
|
|
3049
|
+
const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
|
|
3050
|
+
const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
|
|
3051
|
+
const options = [
|
|
3052
|
+
{
|
|
3053
|
+
value: "fileLimits",
|
|
3054
|
+
label: `${statusIcon("ok")} Max file size`,
|
|
3055
|
+
hint: fileLimitsHint(config)
|
|
3056
|
+
},
|
|
3057
|
+
{
|
|
3058
|
+
value: "fileNaming",
|
|
3059
|
+
label: `${statusIcon(namingStatus)} File naming`,
|
|
3060
|
+
hint: fileNamingHint(config, scanResult)
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
value: "missingTests",
|
|
3064
|
+
label: `${statusIcon(missingTestsStatus)} Missing tests`,
|
|
3065
|
+
hint: missingTestsHint(config)
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
value: "coverage",
|
|
3069
|
+
label: `${statusIcon(coverageStatus)} Coverage`,
|
|
3070
|
+
hint: coverageHint(config, state.hasTestRunner)
|
|
3071
|
+
},
|
|
3072
|
+
{ value: "advancedNaming", label: " Advanced naming", hint: advancedNamingHint(config) }
|
|
3073
|
+
];
|
|
3074
|
+
if (config.packages.length > 1) {
|
|
3075
|
+
const bIcon = state.visited.boundaries && config.rules.enforceBoundaries ? statusIcon("ok") : " ";
|
|
3076
|
+
options.push(
|
|
3077
|
+
{
|
|
3078
|
+
value: "packageOverrides",
|
|
3079
|
+
label: " Per-package overrides",
|
|
3080
|
+
hint: packageOverridesHint(config)
|
|
3081
|
+
},
|
|
3082
|
+
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
const iIcon = state.visited.integrations ? statusIcon("ok") : " ";
|
|
3086
|
+
options.push(
|
|
3087
|
+
{ value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
|
|
3088
|
+
{ value: "reset", label: " Reset all to defaults" },
|
|
3089
|
+
{ value: "review", label: " Review scan details" },
|
|
3090
|
+
{ value: "done", label: " Done \u2014 write config" }
|
|
3091
|
+
);
|
|
3092
|
+
return options;
|
|
3093
|
+
}
|
|
2808
3094
|
|
|
2809
|
-
// src/utils/
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3095
|
+
// src/utils/prompt-main-menu.ts
|
|
3096
|
+
async function handleAdvancedNaming(config) {
|
|
3097
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3098
|
+
const state = {
|
|
3099
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3100
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3101
|
+
testCoverage: config.rules.testCoverage,
|
|
3102
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3103
|
+
enforceNaming: config.rules.enforceNaming,
|
|
3104
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3105
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3106
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3107
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3108
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3109
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3110
|
+
};
|
|
3111
|
+
await promptNamingMenu(state);
|
|
3112
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3113
|
+
config.rules.enforceNaming = state.enforceNaming;
|
|
3114
|
+
if (state.fileNamingValue) {
|
|
3115
|
+
rootPkg.conventions.fileNaming = state.fileNamingValue;
|
|
3116
|
+
} else {
|
|
3117
|
+
delete rootPkg.conventions.fileNaming;
|
|
2825
3118
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3119
|
+
rootPkg.conventions.componentNaming = state.componentNaming || void 0;
|
|
3120
|
+
rootPkg.conventions.hookNaming = state.hookNaming || void 0;
|
|
3121
|
+
rootPkg.conventions.importAlias = state.importAlias || void 0;
|
|
3122
|
+
}
|
|
3123
|
+
async function promptMainMenu(config, scanResult, opts) {
|
|
3124
|
+
const originalConfig = structuredClone(config);
|
|
3125
|
+
const state = {
|
|
3126
|
+
visited: { integrations: false, boundaries: false },
|
|
3127
|
+
deferredInstalls: [],
|
|
3128
|
+
hasTestRunner: opts.hasTestRunner,
|
|
3129
|
+
hookManager: opts.hookManager
|
|
3130
|
+
};
|
|
3131
|
+
while (true) {
|
|
3132
|
+
const options = buildMainMenuOptions(config, scanResult, state);
|
|
3133
|
+
const choice = await clack10.select({ message: "Configure viberails", options });
|
|
3134
|
+
assertNotCancelled(choice);
|
|
3135
|
+
if (choice === "done") {
|
|
3136
|
+
if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
|
|
3137
|
+
config.rules.enforceNaming = false;
|
|
3138
|
+
}
|
|
3139
|
+
break;
|
|
2837
3140
|
}
|
|
2838
|
-
|
|
3141
|
+
if (choice === "fileLimits") {
|
|
3142
|
+
const s = {
|
|
3143
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3144
|
+
maxTestFileLines: config.rules.maxTestFileLines
|
|
3145
|
+
};
|
|
3146
|
+
await promptFileLimitsMenu(s);
|
|
3147
|
+
config.rules.maxFileLines = s.maxFileLines;
|
|
3148
|
+
config.rules.maxTestFileLines = s.maxTestFileLines;
|
|
3149
|
+
}
|
|
3150
|
+
if (choice === "fileNaming") await handleFileNaming(config, scanResult);
|
|
3151
|
+
if (choice === "missingTests") await handleMissingTests(config);
|
|
3152
|
+
if (choice === "coverage") await handleCoverage(config, state, opts);
|
|
3153
|
+
if (choice === "advancedNaming") await handleAdvancedNaming(config);
|
|
3154
|
+
if (choice === "packageOverrides") await handlePackageOverrides(config);
|
|
3155
|
+
if (choice === "boundaries") await handleBoundaries(config, state, opts);
|
|
3156
|
+
if (choice === "integrations") await handleIntegrations(state, opts);
|
|
3157
|
+
if (choice === "review") clack10.note(formatScanResultsText(scanResult), "Scan details");
|
|
3158
|
+
if (choice === "reset") {
|
|
3159
|
+
Object.assign(config, structuredClone(originalConfig));
|
|
3160
|
+
state.deferredInstalls = [];
|
|
3161
|
+
state.visited = { integrations: false, boundaries: false };
|
|
3162
|
+
state.integrations = void 0;
|
|
3163
|
+
clack10.log.info("Reset all settings to scan-detected defaults.");
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
return state;
|
|
2839
3167
|
}
|
|
2840
|
-
function
|
|
2841
|
-
const
|
|
2842
|
-
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
3168
|
+
async function handleFileNaming(config, scanResult) {
|
|
3169
|
+
const isMonorepo = config.packages.length > 1;
|
|
3170
|
+
if (isMonorepo) {
|
|
3171
|
+
const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
|
|
3172
|
+
path: p.relativePath,
|
|
3173
|
+
naming: p.conventions.fileNaming
|
|
3174
|
+
}));
|
|
3175
|
+
if (pkgData.length > 0) {
|
|
3176
|
+
const lines = pkgData.map(
|
|
3177
|
+
(p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
|
|
3178
|
+
);
|
|
3179
|
+
clack10.note(lines.join("\n"), "Per-package file naming detected");
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
|
|
3183
|
+
if (isMonorepo) {
|
|
3184
|
+
const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
|
|
3185
|
+
const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
|
|
3186
|
+
return { value: opt.value, label: opt.label, hint };
|
|
2847
3187
|
}
|
|
3188
|
+
return { value: opt.value, label: opt.label };
|
|
3189
|
+
});
|
|
3190
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3191
|
+
const selected = await clack10.select({
|
|
3192
|
+
message: isMonorepo ? "Default file naming convention" : "File naming convention",
|
|
3193
|
+
options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
|
|
3194
|
+
initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
|
|
3195
|
+
});
|
|
3196
|
+
assertNotCancelled(selected);
|
|
3197
|
+
if (selected === SENTINEL_SKIP) {
|
|
3198
|
+
config.rules.enforceNaming = false;
|
|
3199
|
+
if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
|
|
3200
|
+
} else {
|
|
3201
|
+
config.rules.enforceNaming = true;
|
|
3202
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3203
|
+
rootPkg.conventions.fileNaming = selected;
|
|
2848
3204
|
}
|
|
2849
3205
|
}
|
|
2850
|
-
async function
|
|
2851
|
-
const
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3206
|
+
async function handleMissingTests(config) {
|
|
3207
|
+
const result = await clack10.confirm({
|
|
3208
|
+
message: "Require every source file to have a test file?",
|
|
3209
|
+
initialValue: config.rules.enforceMissingTests
|
|
3210
|
+
});
|
|
3211
|
+
assertNotCancelled(result);
|
|
3212
|
+
config.rules.enforceMissingTests = result;
|
|
3213
|
+
}
|
|
3214
|
+
async function handleCoverage(config, state, opts) {
|
|
3215
|
+
if (!opts.hasTestRunner) {
|
|
3216
|
+
clack10.log.info("Coverage checks are inactive \u2014 no test runner detected.");
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
const planned = planCoverageInstall(opts.coveragePrereqs);
|
|
3220
|
+
if (planned) {
|
|
3221
|
+
const choice = await clack10.select({
|
|
3222
|
+
message: `${planned.label} is not installed. Needed for coverage checks.`,
|
|
2866
3223
|
options: [
|
|
2867
3224
|
{
|
|
2868
3225
|
value: "install",
|
|
2869
|
-
label: "Install
|
|
2870
|
-
hint:
|
|
2871
|
-
},
|
|
2872
|
-
{
|
|
2873
|
-
value: "disable",
|
|
2874
|
-
label: "Disable coverage checks",
|
|
2875
|
-
hint: "missing-test checks still stay active"
|
|
3226
|
+
label: "Install (after final confirmation)",
|
|
3227
|
+
hint: planned.command
|
|
2876
3228
|
},
|
|
3229
|
+
{ value: "disable", label: "Disable coverage checks" },
|
|
2877
3230
|
{
|
|
2878
3231
|
value: "skip",
|
|
2879
3232
|
label: "Skip for now",
|
|
2880
|
-
hint: `install later: ${
|
|
3233
|
+
hint: `install later: ${planned.command}`
|
|
2881
3234
|
}
|
|
2882
3235
|
]
|
|
2883
3236
|
});
|
|
2884
3237
|
assertNotCancelled(choice);
|
|
3238
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
|
|
2885
3239
|
if (choice === "install") {
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
is.stop(`Installed ${m.label}`);
|
|
2891
|
-
} else {
|
|
2892
|
-
is.stop(`Failed to install ${m.label}`);
|
|
2893
|
-
clack7.log.warn(
|
|
2894
|
-
`Install manually: ${m.installCommand}
|
|
2895
|
-
Coverage percentage checks will not work until the dependency is installed.`
|
|
2896
|
-
);
|
|
2897
|
-
}
|
|
3240
|
+
planned.onFailure = () => {
|
|
3241
|
+
config.rules.testCoverage = 0;
|
|
3242
|
+
};
|
|
3243
|
+
state.deferredInstalls.push(planned);
|
|
2898
3244
|
} else if (choice === "disable") {
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
} else {
|
|
2902
|
-
clack7.log.info(
|
|
2903
|
-
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2904
|
-
Install later: ${m.installCommand}`
|
|
2905
|
-
);
|
|
3245
|
+
config.rules.testCoverage = 0;
|
|
3246
|
+
return;
|
|
2906
3247
|
}
|
|
2907
3248
|
}
|
|
2908
|
-
|
|
3249
|
+
const result = await clack10.text({
|
|
3250
|
+
message: "Test coverage target (0 = disable)?",
|
|
3251
|
+
initialValue: String(config.rules.testCoverage),
|
|
3252
|
+
validate: (v) => {
|
|
3253
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
3254
|
+
const n = Number.parseInt(v, 10);
|
|
3255
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
3256
|
+
}
|
|
3257
|
+
});
|
|
3258
|
+
assertNotCancelled(result);
|
|
3259
|
+
config.rules.testCoverage = Number.parseInt(result, 10);
|
|
2909
3260
|
}
|
|
2910
|
-
function
|
|
3261
|
+
async function handlePackageOverrides(config) {
|
|
3262
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3263
|
+
config.packages = await promptPackageOverrides(config.packages, {
|
|
3264
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3265
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3266
|
+
testCoverage: config.rules.testCoverage,
|
|
3267
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3268
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3269
|
+
});
|
|
3270
|
+
normalizePackageOverrides(config.packages);
|
|
3271
|
+
}
|
|
3272
|
+
async function handleBoundaries(config, state, opts) {
|
|
3273
|
+
const shouldInfer = await clack10.confirm({
|
|
3274
|
+
message: "Infer boundary rules from current import patterns?",
|
|
3275
|
+
initialValue: false
|
|
3276
|
+
});
|
|
3277
|
+
assertNotCancelled(shouldInfer);
|
|
3278
|
+
state.visited.boundaries = true;
|
|
3279
|
+
if (!shouldInfer) {
|
|
3280
|
+
config.rules.enforceBoundaries = false;
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
const bs = clack10.spinner();
|
|
3284
|
+
bs.start("Building import graph...");
|
|
2911
3285
|
try {
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3286
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3287
|
+
const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
|
|
3288
|
+
const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
|
|
3289
|
+
const inferred = inferBoundaries(graph);
|
|
3290
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3291
|
+
if (denyCount > 0) {
|
|
3292
|
+
config.boundaries = inferred;
|
|
3293
|
+
config.rules.enforceBoundaries = true;
|
|
3294
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3295
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3296
|
+
} else {
|
|
3297
|
+
bs.stop("No boundary rules inferred");
|
|
3298
|
+
}
|
|
3299
|
+
} catch (err) {
|
|
3300
|
+
bs.stop("Failed to build import graph");
|
|
3301
|
+
clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
|
|
2917
3302
|
}
|
|
2918
3303
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3304
|
+
async function handleIntegrations(state, opts) {
|
|
3305
|
+
const result = await promptIntegrationsDeferred(
|
|
3306
|
+
state.hookManager,
|
|
3307
|
+
opts.tools,
|
|
3308
|
+
opts.tools.packageManager,
|
|
3309
|
+
opts.tools.isWorkspace,
|
|
3310
|
+
opts.projectRoot
|
|
3311
|
+
);
|
|
3312
|
+
state.visited.integrations = true;
|
|
3313
|
+
state.integrations = result.choice;
|
|
3314
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
|
|
3315
|
+
if (result.lefthookInstall) {
|
|
3316
|
+
state.deferredInstalls.push(result.lefthookInstall);
|
|
2930
3317
|
}
|
|
2931
|
-
return filtered;
|
|
2932
3318
|
}
|
|
2933
3319
|
|
|
2934
3320
|
// src/utils/update-gitignore.ts
|
|
2935
|
-
import * as
|
|
2936
|
-
import * as
|
|
3321
|
+
import * as fs16 from "fs";
|
|
3322
|
+
import * as path16 from "path";
|
|
2937
3323
|
function updateGitignore(projectRoot) {
|
|
2938
|
-
const gitignorePath =
|
|
3324
|
+
const gitignorePath = path16.join(projectRoot, ".gitignore");
|
|
2939
3325
|
let content = "";
|
|
2940
|
-
if (
|
|
2941
|
-
content =
|
|
3326
|
+
if (fs16.existsSync(gitignorePath)) {
|
|
3327
|
+
content = fs16.readFileSync(gitignorePath, "utf-8");
|
|
2942
3328
|
}
|
|
2943
3329
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
2944
3330
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
2945
3331
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
2946
3332
|
`;
|
|
2947
|
-
|
|
3333
|
+
fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
2948
3334
|
}
|
|
2949
3335
|
}
|
|
2950
3336
|
|
|
2951
3337
|
// src/commands/init-hooks.ts
|
|
2952
|
-
import * as
|
|
2953
|
-
import * as
|
|
3338
|
+
import * as fs18 from "fs";
|
|
3339
|
+
import * as path18 from "path";
|
|
2954
3340
|
import chalk10 from "chalk";
|
|
2955
3341
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2956
3342
|
|
|
2957
3343
|
// src/commands/resolve-typecheck.ts
|
|
2958
|
-
import * as
|
|
2959
|
-
import * as
|
|
3344
|
+
import * as fs17 from "fs";
|
|
3345
|
+
import * as path17 from "path";
|
|
2960
3346
|
function hasTurboTask(projectRoot, taskName) {
|
|
2961
|
-
const turboPath =
|
|
2962
|
-
if (!
|
|
3347
|
+
const turboPath = path17.join(projectRoot, "turbo.json");
|
|
3348
|
+
if (!fs17.existsSync(turboPath)) return false;
|
|
2963
3349
|
try {
|
|
2964
|
-
const turbo = JSON.parse(
|
|
3350
|
+
const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
|
|
2965
3351
|
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
2966
3352
|
return taskName in tasks;
|
|
2967
3353
|
} catch {
|
|
@@ -2972,10 +3358,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2972
3358
|
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
2973
3359
|
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
2974
3360
|
}
|
|
2975
|
-
const pkgJsonPath =
|
|
2976
|
-
if (
|
|
3361
|
+
const pkgJsonPath = path17.join(projectRoot, "package.json");
|
|
3362
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
2977
3363
|
try {
|
|
2978
|
-
const pkg = JSON.parse(
|
|
3364
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
2979
3365
|
if (pkg.scripts?.typecheck) {
|
|
2980
3366
|
const pm = packageManager ?? "npm";
|
|
2981
3367
|
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
@@ -2983,7 +3369,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2983
3369
|
} catch {
|
|
2984
3370
|
}
|
|
2985
3371
|
}
|
|
2986
|
-
if (
|
|
3372
|
+
if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
|
|
2987
3373
|
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
2988
3374
|
}
|
|
2989
3375
|
return {
|
|
@@ -2993,23 +3379,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2993
3379
|
|
|
2994
3380
|
// src/commands/init-hooks.ts
|
|
2995
3381
|
function setupPreCommitHook(projectRoot) {
|
|
2996
|
-
const lefthookPath =
|
|
2997
|
-
if (
|
|
3382
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3383
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
2998
3384
|
addLefthookPreCommit(lefthookPath);
|
|
2999
3385
|
console.log(` ${chalk10.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
3000
3386
|
return "lefthook.yml";
|
|
3001
3387
|
}
|
|
3002
|
-
const huskyDir =
|
|
3003
|
-
if (
|
|
3388
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3389
|
+
if (fs18.existsSync(huskyDir)) {
|
|
3004
3390
|
writeHuskyPreCommit(huskyDir);
|
|
3005
3391
|
console.log(` ${chalk10.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
3006
3392
|
return ".husky/pre-commit";
|
|
3007
3393
|
}
|
|
3008
|
-
const gitDir =
|
|
3009
|
-
if (
|
|
3010
|
-
const hooksDir =
|
|
3011
|
-
if (!
|
|
3012
|
-
|
|
3394
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3395
|
+
if (fs18.existsSync(gitDir)) {
|
|
3396
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3397
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
3398
|
+
fs18.mkdirSync(hooksDir, { recursive: true });
|
|
3013
3399
|
}
|
|
3014
3400
|
writeGitHookPreCommit(hooksDir);
|
|
3015
3401
|
console.log(` ${chalk10.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -3018,11 +3404,11 @@ function setupPreCommitHook(projectRoot) {
|
|
|
3018
3404
|
return void 0;
|
|
3019
3405
|
}
|
|
3020
3406
|
function writeGitHookPreCommit(hooksDir) {
|
|
3021
|
-
const hookPath =
|
|
3022
|
-
if (
|
|
3023
|
-
const existing =
|
|
3407
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3408
|
+
if (fs18.existsSync(hookPath)) {
|
|
3409
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3024
3410
|
if (existing.includes("viberails")) return;
|
|
3025
|
-
|
|
3411
|
+
fs18.writeFileSync(
|
|
3026
3412
|
hookPath,
|
|
3027
3413
|
`${existing.trimEnd()}
|
|
3028
3414
|
|
|
@@ -3039,10 +3425,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
|
|
|
3039
3425
|
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
3040
3426
|
""
|
|
3041
3427
|
].join("\n");
|
|
3042
|
-
|
|
3428
|
+
fs18.writeFileSync(hookPath, script, { mode: 493 });
|
|
3043
3429
|
}
|
|
3044
3430
|
function addLefthookPreCommit(lefthookPath) {
|
|
3045
|
-
const content =
|
|
3431
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
3046
3432
|
if (content.includes("viberails")) return;
|
|
3047
3433
|
const doc = parseYaml(content) ?? {};
|
|
3048
3434
|
if (!doc["pre-commit"]) {
|
|
@@ -3054,23 +3440,23 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
3054
3440
|
doc["pre-commit"].commands.viberails = {
|
|
3055
3441
|
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
3056
3442
|
};
|
|
3057
|
-
|
|
3443
|
+
fs18.writeFileSync(lefthookPath, stringifyYaml(doc));
|
|
3058
3444
|
}
|
|
3059
3445
|
function detectHookManager(projectRoot) {
|
|
3060
|
-
if (
|
|
3061
|
-
if (
|
|
3446
|
+
if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
3447
|
+
if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
|
|
3062
3448
|
return void 0;
|
|
3063
3449
|
}
|
|
3064
3450
|
function setupClaudeCodeHook(projectRoot) {
|
|
3065
|
-
const claudeDir =
|
|
3066
|
-
if (!
|
|
3067
|
-
|
|
3451
|
+
const claudeDir = path18.join(projectRoot, ".claude");
|
|
3452
|
+
if (!fs18.existsSync(claudeDir)) {
|
|
3453
|
+
fs18.mkdirSync(claudeDir, { recursive: true });
|
|
3068
3454
|
}
|
|
3069
|
-
const settingsPath =
|
|
3455
|
+
const settingsPath = path18.join(claudeDir, "settings.json");
|
|
3070
3456
|
let settings = {};
|
|
3071
|
-
if (
|
|
3457
|
+
if (fs18.existsSync(settingsPath)) {
|
|
3072
3458
|
try {
|
|
3073
|
-
settings = JSON.parse(
|
|
3459
|
+
settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
|
|
3074
3460
|
} catch {
|
|
3075
3461
|
console.warn(
|
|
3076
3462
|
` ${chalk10.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
@@ -3096,30 +3482,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3096
3482
|
}
|
|
3097
3483
|
];
|
|
3098
3484
|
settings.hooks = hooks;
|
|
3099
|
-
|
|
3485
|
+
fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3100
3486
|
`);
|
|
3101
3487
|
console.log(` ${chalk10.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
3102
3488
|
}
|
|
3103
3489
|
function setupClaudeMdReference(projectRoot) {
|
|
3104
|
-
const claudeMdPath =
|
|
3490
|
+
const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
|
|
3105
3491
|
let content = "";
|
|
3106
|
-
if (
|
|
3107
|
-
content =
|
|
3492
|
+
if (fs18.existsSync(claudeMdPath)) {
|
|
3493
|
+
content = fs18.readFileSync(claudeMdPath, "utf-8");
|
|
3108
3494
|
}
|
|
3109
3495
|
if (content.includes("@.viberails/context.md")) return;
|
|
3110
3496
|
const ref = "\n@.viberails/context.md\n";
|
|
3111
3497
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
3112
|
-
|
|
3498
|
+
fs18.writeFileSync(claudeMdPath, prefix + ref);
|
|
3113
3499
|
console.log(` ${chalk10.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
3114
3500
|
}
|
|
3115
3501
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
3116
|
-
const workflowDir =
|
|
3117
|
-
const workflowPath =
|
|
3118
|
-
if (
|
|
3119
|
-
const existing =
|
|
3502
|
+
const workflowDir = path18.join(projectRoot, ".github", "workflows");
|
|
3503
|
+
const workflowPath = path18.join(workflowDir, "viberails.yml");
|
|
3504
|
+
if (fs18.existsSync(workflowPath)) {
|
|
3505
|
+
const existing = fs18.readFileSync(workflowPath, "utf-8");
|
|
3120
3506
|
if (existing.includes("viberails")) return void 0;
|
|
3121
3507
|
}
|
|
3122
|
-
|
|
3508
|
+
fs18.mkdirSync(workflowDir, { recursive: true });
|
|
3123
3509
|
const pm = packageManager || "npm";
|
|
3124
3510
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
3125
3511
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -3173,74 +3559,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
3173
3559
|
""
|
|
3174
3560
|
);
|
|
3175
3561
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
3176
|
-
|
|
3562
|
+
fs18.writeFileSync(workflowPath, content);
|
|
3177
3563
|
return ".github/workflows/viberails.yml";
|
|
3178
3564
|
}
|
|
3179
3565
|
function writeHuskyPreCommit(huskyDir) {
|
|
3180
|
-
const hookPath =
|
|
3566
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
3181
3567
|
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3182
|
-
if (
|
|
3183
|
-
const existing =
|
|
3568
|
+
if (fs18.existsSync(hookPath)) {
|
|
3569
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3184
3570
|
if (!existing.includes("viberails")) {
|
|
3185
|
-
|
|
3571
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3186
3572
|
${cmd}
|
|
3187
3573
|
`);
|
|
3188
3574
|
}
|
|
3189
3575
|
return;
|
|
3190
3576
|
}
|
|
3191
|
-
|
|
3577
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
3192
3578
|
${cmd}
|
|
3193
3579
|
`, { mode: 493 });
|
|
3194
3580
|
}
|
|
3195
3581
|
|
|
3196
3582
|
// src/commands/init-hooks-extra.ts
|
|
3197
|
-
import * as
|
|
3198
|
-
import * as
|
|
3583
|
+
import * as fs19 from "fs";
|
|
3584
|
+
import * as path19 from "path";
|
|
3199
3585
|
import chalk11 from "chalk";
|
|
3200
3586
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
3201
3587
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3202
|
-
const lefthookPath =
|
|
3203
|
-
if (
|
|
3204
|
-
const content =
|
|
3588
|
+
const lefthookPath = path19.join(projectRoot, "lefthook.yml");
|
|
3589
|
+
if (fs19.existsSync(lefthookPath)) {
|
|
3590
|
+
const content = fs19.readFileSync(lefthookPath, "utf-8");
|
|
3205
3591
|
if (content.includes(marker)) return void 0;
|
|
3206
3592
|
const doc = parseYaml2(content) ?? {};
|
|
3207
3593
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
3208
3594
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
3209
3595
|
doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
|
|
3210
|
-
|
|
3596
|
+
fs19.writeFileSync(lefthookPath, stringifyYaml2(doc));
|
|
3211
3597
|
return "lefthook.yml";
|
|
3212
3598
|
}
|
|
3213
|
-
const huskyDir =
|
|
3214
|
-
if (
|
|
3215
|
-
const hookPath =
|
|
3216
|
-
if (
|
|
3217
|
-
const existing =
|
|
3599
|
+
const huskyDir = path19.join(projectRoot, ".husky");
|
|
3600
|
+
if (fs19.existsSync(huskyDir)) {
|
|
3601
|
+
const hookPath = path19.join(huskyDir, "pre-commit");
|
|
3602
|
+
if (fs19.existsSync(hookPath)) {
|
|
3603
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3218
3604
|
if (existing.includes(marker)) return void 0;
|
|
3219
|
-
|
|
3605
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3220
3606
|
${command}
|
|
3221
3607
|
`);
|
|
3222
3608
|
} else {
|
|
3223
|
-
|
|
3609
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3224
3610
|
${command}
|
|
3225
3611
|
`, { mode: 493 });
|
|
3226
3612
|
}
|
|
3227
3613
|
return ".husky/pre-commit";
|
|
3228
3614
|
}
|
|
3229
|
-
const gitDir =
|
|
3230
|
-
if (
|
|
3231
|
-
const hooksDir =
|
|
3232
|
-
if (!
|
|
3233
|
-
const hookPath =
|
|
3234
|
-
if (
|
|
3235
|
-
const existing =
|
|
3615
|
+
const gitDir = path19.join(projectRoot, ".git");
|
|
3616
|
+
if (fs19.existsSync(gitDir)) {
|
|
3617
|
+
const hooksDir = path19.join(gitDir, "hooks");
|
|
3618
|
+
if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
|
|
3619
|
+
const hookPath = path19.join(hooksDir, "pre-commit");
|
|
3620
|
+
if (fs19.existsSync(hookPath)) {
|
|
3621
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3236
3622
|
if (existing.includes(marker)) return void 0;
|
|
3237
|
-
|
|
3623
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3238
3624
|
|
|
3239
3625
|
# ${name}
|
|
3240
3626
|
${command}
|
|
3241
3627
|
`);
|
|
3242
3628
|
} else {
|
|
3243
|
-
|
|
3629
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3244
3630
|
# Generated by viberails
|
|
3245
3631
|
|
|
3246
3632
|
# ${name}
|
|
@@ -3266,7 +3652,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
|
|
|
3266
3652
|
return target;
|
|
3267
3653
|
}
|
|
3268
3654
|
function setupLintHook(projectRoot, linter) {
|
|
3269
|
-
const isLefthook =
|
|
3655
|
+
const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
|
|
3270
3656
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
3271
3657
|
let command;
|
|
3272
3658
|
let lefthookExtra;
|
|
@@ -3290,6 +3676,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3290
3676
|
const created = [];
|
|
3291
3677
|
if (integrations.preCommitHook) {
|
|
3292
3678
|
const t = setupPreCommitHook(projectRoot);
|
|
3679
|
+
if (t && opts.lefthookExpected && !t.includes("lefthook")) {
|
|
3680
|
+
console.log(` ${chalk11.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
|
|
3681
|
+
}
|
|
3293
3682
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
3294
3683
|
}
|
|
3295
3684
|
if (integrations.typecheckHook) {
|
|
@@ -3318,34 +3707,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3318
3707
|
return created;
|
|
3319
3708
|
}
|
|
3320
3709
|
|
|
3321
|
-
// src/commands/init.ts
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
}
|
|
3333
|
-
const
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3710
|
+
// src/commands/init-non-interactive.ts
|
|
3711
|
+
import * as fs20 from "fs";
|
|
3712
|
+
import * as path20 from "path";
|
|
3713
|
+
import * as clack11 from "@clack/prompts";
|
|
3714
|
+
import { compactConfig as compactConfig3, generateConfig } from "@viberails/config";
|
|
3715
|
+
import { scan as scan2 } from "@viberails/scanner";
|
|
3716
|
+
import chalk12 from "chalk";
|
|
3717
|
+
|
|
3718
|
+
// src/utils/filter-confidence.ts
|
|
3719
|
+
function filterHighConfidence(conventions, meta) {
|
|
3720
|
+
if (!meta) return conventions;
|
|
3721
|
+
const filtered = {};
|
|
3722
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
3723
|
+
if (value === void 0) continue;
|
|
3724
|
+
const convMeta = meta[key];
|
|
3725
|
+
if (!convMeta || convMeta.confidence === "high") {
|
|
3726
|
+
filtered[key] = value;
|
|
3337
3727
|
}
|
|
3338
|
-
console.log(
|
|
3339
|
-
`${chalk12.yellow("!")} viberails is already initialized.
|
|
3340
|
-
Run ${chalk12.cyan("viberails")} to review or edit the existing setup, ${chalk12.cyan("viberails sync")} to update generated files, or ${chalk12.cyan("viberails init --force")} to replace it.`
|
|
3341
|
-
);
|
|
3342
|
-
return;
|
|
3343
3728
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3729
|
+
return filtered;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// src/commands/init-non-interactive.ts
|
|
3733
|
+
function getExemptedPackages(config) {
|
|
3734
|
+
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3346
3735
|
}
|
|
3347
3736
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3348
|
-
const s =
|
|
3737
|
+
const s = clack11.spinner();
|
|
3349
3738
|
s.start("Scanning project...");
|
|
3350
3739
|
const scanResult = await scan2(projectRoot);
|
|
3351
3740
|
const config = generateConfig(scanResult);
|
|
@@ -3364,7 +3753,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3364
3753
|
);
|
|
3365
3754
|
}
|
|
3366
3755
|
if (config.packages.length > 1) {
|
|
3367
|
-
const bs =
|
|
3756
|
+
const bs = clack11.spinner();
|
|
3368
3757
|
bs.start("Building import graph...");
|
|
3369
3758
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3370
3759
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3380,7 +3769,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3380
3769
|
}
|
|
3381
3770
|
}
|
|
3382
3771
|
const compacted = compactConfig3(config);
|
|
3383
|
-
|
|
3772
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3384
3773
|
`);
|
|
3385
3774
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3386
3775
|
updateGitignore(projectRoot);
|
|
@@ -3399,7 +3788,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3399
3788
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3400
3789
|
const ok = chalk12.green("\u2713");
|
|
3401
3790
|
const created = [
|
|
3402
|
-
`${ok} ${
|
|
3791
|
+
`${ok} ${path20.basename(configPath)}`,
|
|
3403
3792
|
`${ok} .viberails/context.md`,
|
|
3404
3793
|
`${ok} .viberails/scan-result.json`,
|
|
3405
3794
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
@@ -3413,13 +3802,36 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3413
3802
|
Created:
|
|
3414
3803
|
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
3415
3804
|
}
|
|
3805
|
+
|
|
3806
|
+
// src/commands/init.ts
|
|
3807
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
3808
|
+
async function initCommand(options, cwd) {
|
|
3809
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
3810
|
+
if (!projectRoot) {
|
|
3811
|
+
throw new Error(
|
|
3812
|
+
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE5);
|
|
3816
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3817
|
+
if (!options.yes) {
|
|
3818
|
+
return initInteractive(projectRoot, configPath, options);
|
|
3819
|
+
}
|
|
3820
|
+
console.log(
|
|
3821
|
+
`${chalk13.yellow("!")} viberails is already initialized.
|
|
3822
|
+
Run ${chalk13.cyan("viberails")} to review or edit the existing setup, ${chalk13.cyan("viberails sync")} to update generated files, or ${chalk13.cyan("viberails init --force")} to replace it.`
|
|
3823
|
+
);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
if (options.yes) return initNonInteractive(projectRoot, configPath);
|
|
3827
|
+
await initInteractive(projectRoot, configPath, options);
|
|
3828
|
+
}
|
|
3416
3829
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
const action = await promptExistingConfigAction(path19.basename(configPath));
|
|
3830
|
+
clack12.intro("viberails");
|
|
3831
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3832
|
+
const action = await promptExistingConfigAction(path21.basename(configPath));
|
|
3421
3833
|
if (action === "cancel") {
|
|
3422
|
-
|
|
3834
|
+
clack12.outro("Aborted. No files were written.");
|
|
3423
3835
|
return;
|
|
3424
3836
|
}
|
|
3425
3837
|
if (action === "edit") {
|
|
@@ -3428,136 +3840,93 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3428
3840
|
}
|
|
3429
3841
|
options.force = true;
|
|
3430
3842
|
}
|
|
3431
|
-
if (
|
|
3843
|
+
if (fs21.existsSync(configPath) && options.force) {
|
|
3432
3844
|
const replace = await confirmDangerous(
|
|
3433
|
-
`${
|
|
3845
|
+
`${path21.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3434
3846
|
);
|
|
3435
3847
|
if (!replace) {
|
|
3436
|
-
|
|
3848
|
+
clack12.outro("Aborted. No files were written.");
|
|
3437
3849
|
return;
|
|
3438
3850
|
}
|
|
3439
3851
|
}
|
|
3440
|
-
const s =
|
|
3852
|
+
const s = clack12.spinner();
|
|
3441
3853
|
s.start("Scanning project...");
|
|
3442
|
-
const scanResult = await
|
|
3443
|
-
const config =
|
|
3854
|
+
const scanResult = await scan3(projectRoot);
|
|
3855
|
+
const config = generateConfig2(scanResult);
|
|
3444
3856
|
s.stop("Scan complete");
|
|
3445
3857
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3446
|
-
|
|
3858
|
+
clack12.log.warn(
|
|
3447
3859
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3448
3860
|
);
|
|
3449
3861
|
}
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
const nextDecision = await promptInitDecision();
|
|
3455
|
-
if (nextDecision === "review") {
|
|
3456
|
-
clack8.note(formatScanResultsText(scanResult), "Detected details");
|
|
3457
|
-
continue;
|
|
3458
|
-
}
|
|
3459
|
-
decision = nextDecision;
|
|
3460
|
-
break;
|
|
3461
|
-
}
|
|
3462
|
-
if (decision === "customize") {
|
|
3463
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3464
|
-
const overrides = await promptRuleMenu({
|
|
3465
|
-
maxFileLines: config.rules.maxFileLines,
|
|
3466
|
-
testCoverage: config.rules.testCoverage,
|
|
3467
|
-
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3468
|
-
enforceNaming: config.rules.enforceNaming,
|
|
3469
|
-
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3470
|
-
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
3471
|
-
coverageCommand: config.defaults?.coverage?.command,
|
|
3472
|
-
packageOverrides: config.packages
|
|
3473
|
-
});
|
|
3474
|
-
applyRuleOverrides(config, overrides);
|
|
3475
|
-
}
|
|
3476
|
-
if (config.packages.length > 1) {
|
|
3477
|
-
clack8.note(
|
|
3478
|
-
"Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
|
|
3479
|
-
"Boundaries"
|
|
3862
|
+
const hasTestRunner = !!scanResult.stack.testRunner;
|
|
3863
|
+
if (!hasTestRunner) {
|
|
3864
|
+
clack12.log.info(
|
|
3865
|
+
"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."
|
|
3480
3866
|
);
|
|
3481
|
-
const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
|
|
3482
|
-
if (shouldInfer) {
|
|
3483
|
-
const bs = clack8.spinner();
|
|
3484
|
-
bs.start("Building import graph...");
|
|
3485
|
-
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3486
|
-
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
3487
|
-
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
3488
|
-
const inferred = inferBoundaries(graph);
|
|
3489
|
-
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3490
|
-
if (denyCount > 0) {
|
|
3491
|
-
config.boundaries = inferred;
|
|
3492
|
-
config.rules.enforceBoundaries = true;
|
|
3493
|
-
const pkgCount = Object.keys(inferred.deny).length;
|
|
3494
|
-
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3495
|
-
} else {
|
|
3496
|
-
bs.stop("No boundary rules inferred");
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
3867
|
}
|
|
3500
3868
|
const hookManager = detectHookManager(projectRoot);
|
|
3501
3869
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3502
|
-
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3503
|
-
if (hasMissingPrereqs) {
|
|
3504
|
-
clack8.log.info("Some dependencies are needed for full functionality.");
|
|
3505
|
-
}
|
|
3506
|
-
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3507
|
-
if (prereqResult.disableCoverage) {
|
|
3508
|
-
config.rules.testCoverage = 0;
|
|
3509
|
-
}
|
|
3510
3870
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3871
|
+
const state = await promptMainMenu(config, scanResult, {
|
|
3872
|
+
hasTestRunner,
|
|
3873
|
+
hookManager,
|
|
3874
|
+
coveragePrereqs,
|
|
3875
|
+
projectRoot,
|
|
3876
|
+
tools: {
|
|
3877
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3878
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3879
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3880
|
+
isWorkspace: config.packages.length > 1
|
|
3881
|
+
}
|
|
3520
3882
|
});
|
|
3521
3883
|
const shouldWrite = await confirm3("Apply this setup?");
|
|
3522
3884
|
if (!shouldWrite) {
|
|
3523
|
-
|
|
3885
|
+
clack12.outro("Aborted. No files were written.");
|
|
3524
3886
|
return;
|
|
3525
3887
|
}
|
|
3526
|
-
|
|
3888
|
+
if (state.deferredInstalls.length > 0) {
|
|
3889
|
+
await executeDeferredInstalls(projectRoot, state.deferredInstalls);
|
|
3890
|
+
}
|
|
3891
|
+
const ws = clack12.spinner();
|
|
3527
3892
|
ws.start("Writing configuration...");
|
|
3528
|
-
const compacted =
|
|
3529
|
-
|
|
3893
|
+
const compacted = compactConfig4(config);
|
|
3894
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3530
3895
|
`);
|
|
3531
3896
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3532
3897
|
updateGitignore(projectRoot);
|
|
3533
3898
|
ws.stop("Configuration written");
|
|
3534
|
-
const ok =
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3899
|
+
const ok = chalk13.green("\u2713");
|
|
3900
|
+
clack12.log.step(`${ok} ${path21.basename(configPath)}`);
|
|
3901
|
+
clack12.log.step(`${ok} .viberails/context.md`);
|
|
3902
|
+
clack12.log.step(`${ok} .viberails/scan-result.json`);
|
|
3903
|
+
if (state.visited.integrations && state.integrations) {
|
|
3904
|
+
const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
|
|
3905
|
+
setupSelectedIntegrations(projectRoot, state.integrations, {
|
|
3906
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3907
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3908
|
+
lefthookExpected
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
clack12.outro(
|
|
3543
3912
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3544
|
-
${
|
|
3913
|
+
${chalk13.dim("Tip: use")} ${chalk13.cyan("viberails check --enforce")} ${chalk13.dim("in CI to block PRs on violations.")}`
|
|
3545
3914
|
);
|
|
3546
3915
|
}
|
|
3547
3916
|
|
|
3548
3917
|
// src/commands/sync.ts
|
|
3549
|
-
import * as
|
|
3550
|
-
import * as
|
|
3551
|
-
import * as
|
|
3552
|
-
import { compactConfig as
|
|
3553
|
-
import { scan as
|
|
3554
|
-
import
|
|
3918
|
+
import * as fs22 from "fs";
|
|
3919
|
+
import * as path22 from "path";
|
|
3920
|
+
import * as clack13 from "@clack/prompts";
|
|
3921
|
+
import { compactConfig as compactConfig5, loadConfig as loadConfig5, mergeConfig as mergeConfig2 } from "@viberails/config";
|
|
3922
|
+
import { scan as scan4 } from "@viberails/scanner";
|
|
3923
|
+
import chalk14 from "chalk";
|
|
3555
3924
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3556
3925
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3557
3926
|
function loadPreviousStats(projectRoot) {
|
|
3558
|
-
const scanResultPath =
|
|
3927
|
+
const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3559
3928
|
try {
|
|
3560
|
-
const raw =
|
|
3929
|
+
const raw = fs22.readFileSync(scanResultPath, "utf-8");
|
|
3561
3930
|
const parsed = JSON.parse(raw);
|
|
3562
3931
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3563
3932
|
return parsed.statistics;
|
|
@@ -3574,17 +3943,17 @@ async function syncCommand(options, cwd) {
|
|
|
3574
3943
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
3575
3944
|
);
|
|
3576
3945
|
}
|
|
3577
|
-
const configPath =
|
|
3946
|
+
const configPath = path22.join(projectRoot, CONFIG_FILE6);
|
|
3578
3947
|
const existing = await loadConfig5(configPath);
|
|
3579
3948
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3580
|
-
const s =
|
|
3949
|
+
const s = clack13.spinner();
|
|
3581
3950
|
s.start("Scanning project...");
|
|
3582
|
-
const scanResult = await
|
|
3951
|
+
const scanResult = await scan4(projectRoot);
|
|
3583
3952
|
s.stop("Scan complete");
|
|
3584
3953
|
const merged = mergeConfig2(existing, scanResult);
|
|
3585
|
-
const compacted =
|
|
3954
|
+
const compacted = compactConfig5(merged);
|
|
3586
3955
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3587
|
-
const rawDisk =
|
|
3956
|
+
const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
|
|
3588
3957
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3589
3958
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3590
3959
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3592,19 +3961,19 @@ async function syncCommand(options, cwd) {
|
|
|
3592
3961
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3593
3962
|
if (changes.length > 0 || statsDelta) {
|
|
3594
3963
|
console.log(`
|
|
3595
|
-
${
|
|
3964
|
+
${chalk14.bold("Changes:")}`);
|
|
3596
3965
|
for (const change of changes) {
|
|
3597
|
-
const icon = change.type === "removed" ?
|
|
3966
|
+
const icon = change.type === "removed" ? chalk14.red("-") : chalk14.green("+");
|
|
3598
3967
|
console.log(` ${icon} ${change.description}`);
|
|
3599
3968
|
}
|
|
3600
3969
|
if (statsDelta) {
|
|
3601
|
-
console.log(` ${
|
|
3970
|
+
console.log(` ${chalk14.dim(statsDelta)}`);
|
|
3602
3971
|
}
|
|
3603
3972
|
}
|
|
3604
3973
|
if (options?.interactive) {
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
const decision = await
|
|
3974
|
+
clack13.intro("viberails sync (interactive)");
|
|
3975
|
+
clack13.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
3976
|
+
const decision = await clack13.select({
|
|
3608
3977
|
message: "How would you like to proceed?",
|
|
3609
3978
|
options: [
|
|
3610
3979
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -3614,47 +3983,51 @@ ${chalk13.bold("Changes:")}`);
|
|
|
3614
3983
|
});
|
|
3615
3984
|
assertNotCancelled(decision);
|
|
3616
3985
|
if (decision === "cancel") {
|
|
3617
|
-
|
|
3986
|
+
clack13.outro("Sync cancelled. No files were written.");
|
|
3618
3987
|
return;
|
|
3619
3988
|
}
|
|
3620
3989
|
if (decision === "customize") {
|
|
3621
3990
|
const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
|
|
3622
3991
|
const overrides = await promptRuleMenu({
|
|
3623
3992
|
maxFileLines: merged.rules.maxFileLines,
|
|
3993
|
+
maxTestFileLines: merged.rules.maxTestFileLines,
|
|
3624
3994
|
testCoverage: merged.rules.testCoverage,
|
|
3625
3995
|
enforceMissingTests: merged.rules.enforceMissingTests,
|
|
3626
3996
|
enforceNaming: merged.rules.enforceNaming,
|
|
3627
3997
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3998
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3999
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
4000
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3628
4001
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3629
4002
|
coverageCommand: merged.defaults?.coverage?.command,
|
|
3630
4003
|
packageOverrides: merged.packages
|
|
3631
4004
|
});
|
|
3632
4005
|
applyRuleOverrides(merged, overrides);
|
|
3633
|
-
const recompacted =
|
|
3634
|
-
|
|
4006
|
+
const recompacted = compactConfig5(merged);
|
|
4007
|
+
fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3635
4008
|
`);
|
|
3636
4009
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3637
|
-
|
|
3638
|
-
|
|
4010
|
+
clack13.log.success("Updated config with your customizations.");
|
|
4011
|
+
clack13.outro("Done! Run viberails check to verify.");
|
|
3639
4012
|
return;
|
|
3640
4013
|
}
|
|
3641
4014
|
}
|
|
3642
|
-
|
|
4015
|
+
fs22.writeFileSync(configPath, `${compactedJson}
|
|
3643
4016
|
`);
|
|
3644
4017
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3645
4018
|
console.log(`
|
|
3646
|
-
${
|
|
4019
|
+
${chalk14.bold("Synced:")}`);
|
|
3647
4020
|
if (configChanged) {
|
|
3648
|
-
console.log(` ${
|
|
4021
|
+
console.log(` ${chalk14.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3649
4022
|
} else {
|
|
3650
|
-
console.log(` ${
|
|
4023
|
+
console.log(` ${chalk14.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3651
4024
|
}
|
|
3652
|
-
console.log(` ${
|
|
3653
|
-
console.log(` ${
|
|
4025
|
+
console.log(` ${chalk14.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
4026
|
+
console.log(` ${chalk14.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
3654
4027
|
}
|
|
3655
4028
|
|
|
3656
4029
|
// src/index.ts
|
|
3657
|
-
var VERSION = "0.6.
|
|
4030
|
+
var VERSION = "0.6.6";
|
|
3658
4031
|
var program = new Command();
|
|
3659
4032
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3660
4033
|
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
|
|
@@ -3662,7 +4035,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3662
4035
|
await initCommand(options);
|
|
3663
4036
|
} catch (err) {
|
|
3664
4037
|
const message = err instanceof Error ? err.message : String(err);
|
|
3665
|
-
console.error(`${
|
|
4038
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3666
4039
|
process.exit(1);
|
|
3667
4040
|
}
|
|
3668
4041
|
});
|
|
@@ -3671,7 +4044,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3671
4044
|
await syncCommand(options);
|
|
3672
4045
|
} catch (err) {
|
|
3673
4046
|
const message = err instanceof Error ? err.message : String(err);
|
|
3674
|
-
console.error(`${
|
|
4047
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3675
4048
|
process.exit(1);
|
|
3676
4049
|
}
|
|
3677
4050
|
});
|
|
@@ -3680,7 +4053,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3680
4053
|
await configCommand(options);
|
|
3681
4054
|
} catch (err) {
|
|
3682
4055
|
const message = err instanceof Error ? err.message : String(err);
|
|
3683
|
-
console.error(`${
|
|
4056
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3684
4057
|
process.exit(1);
|
|
3685
4058
|
}
|
|
3686
4059
|
});
|
|
@@ -3701,7 +4074,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3701
4074
|
process.exit(exitCode);
|
|
3702
4075
|
} catch (err) {
|
|
3703
4076
|
const message = err instanceof Error ? err.message : String(err);
|
|
3704
|
-
console.error(`${
|
|
4077
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3705
4078
|
process.exit(1);
|
|
3706
4079
|
}
|
|
3707
4080
|
}
|
|
@@ -3712,7 +4085,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3712
4085
|
process.exit(exitCode);
|
|
3713
4086
|
} catch (err) {
|
|
3714
4087
|
const message = err instanceof Error ? err.message : String(err);
|
|
3715
|
-
console.error(`${
|
|
4088
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3716
4089
|
process.exit(1);
|
|
3717
4090
|
}
|
|
3718
4091
|
});
|
|
@@ -3721,7 +4094,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3721
4094
|
await boundariesCommand(options);
|
|
3722
4095
|
} catch (err) {
|
|
3723
4096
|
const message = err instanceof Error ? err.message : String(err);
|
|
3724
|
-
console.error(`${
|
|
4097
|
+
console.error(`${chalk15.red("Error:")} ${message}`);
|
|
3725
4098
|
process.exit(1);
|
|
3726
4099
|
}
|
|
3727
4100
|
});
|