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.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
VERSION: () => VERSION
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_chalk15 = __toESM(require("chalk"), 1);
|
|
38
38
|
var import_commander = require("commander");
|
|
39
39
|
|
|
40
40
|
// src/commands/boundaries.ts
|
|
@@ -63,146 +63,285 @@ function findProjectRoot(startDir) {
|
|
|
63
63
|
// src/utils/prompt.ts
|
|
64
64
|
var clack5 = __toESM(require("@clack/prompts"), 1);
|
|
65
65
|
|
|
66
|
-
// src/utils/prompt-
|
|
67
|
-
var
|
|
66
|
+
// src/utils/prompt-rules.ts
|
|
67
|
+
var clack4 = __toESM(require("@clack/prompts"), 1);
|
|
68
68
|
|
|
69
|
-
// src/utils/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return new Promise((resolve4) => {
|
|
73
|
-
const child = (0, import_node_child_process.spawn)(command, { cwd, shell: true, stdio: "pipe" });
|
|
74
|
-
let stdout = "";
|
|
75
|
-
let stderr = "";
|
|
76
|
-
child.stdout.on("data", (d) => {
|
|
77
|
-
stdout += d.toString();
|
|
78
|
-
});
|
|
79
|
-
child.stderr.on("data", (d) => {
|
|
80
|
-
stderr += d.toString();
|
|
81
|
-
});
|
|
82
|
-
child.on("close", (status) => {
|
|
83
|
-
resolve4({ status, stdout, stderr });
|
|
84
|
-
});
|
|
85
|
-
child.on("error", () => {
|
|
86
|
-
resolve4({ status: 1, stdout, stderr });
|
|
87
|
-
});
|
|
88
|
-
});
|
|
69
|
+
// src/utils/get-root-package.ts
|
|
70
|
+
function getRootPackage(packages) {
|
|
71
|
+
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
// src/utils/prompt-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
// src/utils/prompt-menu-handlers.ts
|
|
75
|
+
var clack3 = __toESM(require("@clack/prompts"), 1);
|
|
76
|
+
|
|
77
|
+
// src/utils/prompt-package-overrides.ts
|
|
78
|
+
var clack2 = __toESM(require("@clack/prompts"), 1);
|
|
79
|
+
|
|
80
|
+
// src/utils/prompt-constants.ts
|
|
81
|
+
var SENTINEL_DONE = "__done__";
|
|
82
|
+
var SENTINEL_CLEAR = "__clear__";
|
|
83
|
+
var SENTINEL_CUSTOM = "__custom__";
|
|
84
|
+
var SENTINEL_NONE = "__none__";
|
|
85
|
+
var SENTINEL_INHERIT = "__inherit__";
|
|
86
|
+
var SENTINEL_SKIP = "__skip__";
|
|
87
|
+
|
|
88
|
+
// src/utils/prompt-submenus.ts
|
|
89
|
+
var clack = __toESM(require("@clack/prompts"), 1);
|
|
90
|
+
var FILE_NAMING_OPTIONS = [
|
|
91
|
+
{ value: "kebab-case", label: "kebab-case" },
|
|
92
|
+
{ value: "camelCase", label: "camelCase" },
|
|
93
|
+
{ value: "PascalCase", label: "PascalCase" },
|
|
94
|
+
{ value: "snake_case", label: "snake_case" }
|
|
95
|
+
];
|
|
96
|
+
var COMPONENT_NAMING_OPTIONS = [
|
|
97
|
+
{ value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
|
|
98
|
+
{ value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
|
|
99
|
+
];
|
|
100
|
+
var HOOK_NAMING_OPTIONS = [
|
|
101
|
+
{ value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
|
|
102
|
+
{ value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
|
|
103
|
+
];
|
|
104
|
+
async function promptFileLimitsMenu(state) {
|
|
105
|
+
while (true) {
|
|
106
|
+
const choice = await clack.select({
|
|
107
|
+
message: "File limits",
|
|
108
|
+
options: [
|
|
109
|
+
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
110
|
+
{
|
|
111
|
+
value: "maxTestFileLines",
|
|
112
|
+
label: "Max test file lines",
|
|
113
|
+
hint: state.maxTestFileLines > 0 ? String(state.maxTestFileLines) : "0 (unlimited)"
|
|
114
|
+
},
|
|
115
|
+
{ value: "back", label: "Back" }
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
assertNotCancelled(choice);
|
|
119
|
+
if (choice === "back") return;
|
|
120
|
+
if (choice === "maxFileLines") {
|
|
121
|
+
const result = await clack.text({
|
|
122
|
+
message: "Maximum lines per source file?",
|
|
123
|
+
initialValue: String(state.maxFileLines),
|
|
124
|
+
validate: (v) => {
|
|
125
|
+
if (typeof v !== "string") return "Enter a positive number";
|
|
126
|
+
const n = Number.parseInt(v, 10);
|
|
127
|
+
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
assertNotCancelled(result);
|
|
131
|
+
state.maxFileLines = Number.parseInt(result, 10);
|
|
132
|
+
}
|
|
133
|
+
if (choice === "maxTestFileLines") {
|
|
134
|
+
const result = await clack.text({
|
|
135
|
+
message: "Maximum lines per test file (0 to disable)?",
|
|
136
|
+
initialValue: String(state.maxTestFileLines),
|
|
137
|
+
validate: (v) => {
|
|
138
|
+
if (typeof v !== "string") return "Enter a number (0 or positive)";
|
|
139
|
+
const n = Number.parseInt(v, 10);
|
|
140
|
+
if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
assertNotCancelled(result);
|
|
144
|
+
state.maxTestFileLines = Number.parseInt(result, 10);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function promptNamingMenu(state) {
|
|
149
|
+
while (true) {
|
|
150
|
+
const options = [
|
|
96
151
|
{
|
|
97
|
-
value: "
|
|
98
|
-
label: "
|
|
99
|
-
hint:
|
|
152
|
+
value: "enforceNaming",
|
|
153
|
+
label: "Enforce file naming",
|
|
154
|
+
hint: state.enforceNaming ? "yes" : "no"
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
if (state.enforceNaming) {
|
|
158
|
+
options.push({
|
|
159
|
+
value: "fileNaming",
|
|
160
|
+
label: "File naming convention",
|
|
161
|
+
hint: state.fileNamingValue ?? "(not set)"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
options.push(
|
|
165
|
+
{
|
|
166
|
+
value: "componentNaming",
|
|
167
|
+
label: "Component naming",
|
|
168
|
+
hint: state.componentNaming ?? "(not set)"
|
|
100
169
|
},
|
|
101
170
|
{
|
|
102
|
-
value: "
|
|
103
|
-
label: "
|
|
104
|
-
hint:
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const s = clack.spinner();
|
|
113
|
-
s.start("Installing Lefthook...");
|
|
114
|
-
const result = await spawnAsync(installCmd, projectRoot);
|
|
115
|
-
if (result.status === 0) {
|
|
116
|
-
const fs21 = await import("fs");
|
|
117
|
-
const path21 = await import("path");
|
|
118
|
-
const lefthookPath = path21.join(projectRoot, "lefthook.yml");
|
|
119
|
-
if (!fs21.existsSync(lefthookPath)) {
|
|
120
|
-
fs21.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
121
|
-
}
|
|
122
|
-
s.stop("Installed Lefthook");
|
|
123
|
-
return "Lefthook";
|
|
124
|
-
}
|
|
125
|
-
s.stop("Failed to install Lefthook");
|
|
126
|
-
clack.log.warn(`Install manually: ${installCmd}`);
|
|
127
|
-
return void 0;
|
|
128
|
-
}
|
|
129
|
-
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
130
|
-
let resolvedHookManager = hookManager;
|
|
131
|
-
if (!resolvedHookManager) {
|
|
132
|
-
resolvedHookManager = await promptHookManagerInstall(
|
|
133
|
-
projectRoot,
|
|
134
|
-
tools?.packageManager ?? "npm",
|
|
135
|
-
tools?.isWorkspace
|
|
171
|
+
value: "hookNaming",
|
|
172
|
+
label: "Hook naming",
|
|
173
|
+
hint: state.hookNaming ?? "(not set)"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: "importAlias",
|
|
177
|
+
label: "Import alias",
|
|
178
|
+
hint: state.importAlias ?? "(not set)"
|
|
179
|
+
},
|
|
180
|
+
{ value: "back", label: "Back" }
|
|
136
181
|
);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
182
|
+
const choice = await clack.select({ message: "Naming & conventions", options });
|
|
183
|
+
assertNotCancelled(choice);
|
|
184
|
+
if (choice === "back") return;
|
|
185
|
+
if (choice === "enforceNaming") {
|
|
186
|
+
const result = await clack.confirm({
|
|
187
|
+
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
188
|
+
initialValue: state.enforceNaming
|
|
189
|
+
});
|
|
190
|
+
assertNotCancelled(result);
|
|
191
|
+
if (result && !state.fileNamingValue) {
|
|
192
|
+
const selected = await clack.select({
|
|
193
|
+
message: "Which file naming convention should be enforced?",
|
|
194
|
+
options: [...FILE_NAMING_OPTIONS]
|
|
195
|
+
});
|
|
196
|
+
assertNotCancelled(selected);
|
|
197
|
+
state.fileNamingValue = selected;
|
|
198
|
+
}
|
|
199
|
+
state.enforceNaming = result;
|
|
200
|
+
}
|
|
201
|
+
if (choice === "fileNaming") {
|
|
202
|
+
const selected = await clack.select({
|
|
203
|
+
message: "Which file naming convention should be enforced?",
|
|
204
|
+
options: [...FILE_NAMING_OPTIONS],
|
|
205
|
+
initialValue: state.fileNamingValue
|
|
206
|
+
});
|
|
207
|
+
assertNotCancelled(selected);
|
|
208
|
+
state.fileNamingValue = selected;
|
|
209
|
+
}
|
|
210
|
+
if (choice === "componentNaming") {
|
|
211
|
+
const selected = await clack.select({
|
|
212
|
+
message: "Component naming convention",
|
|
213
|
+
options: [
|
|
214
|
+
...COMPONENT_NAMING_OPTIONS,
|
|
215
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
216
|
+
],
|
|
217
|
+
initialValue: state.componentNaming ?? SENTINEL_CLEAR
|
|
218
|
+
});
|
|
219
|
+
assertNotCancelled(selected);
|
|
220
|
+
state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
221
|
+
}
|
|
222
|
+
if (choice === "hookNaming") {
|
|
223
|
+
const selected = await clack.select({
|
|
224
|
+
message: "Hook naming convention",
|
|
225
|
+
options: [
|
|
226
|
+
...HOOK_NAMING_OPTIONS,
|
|
227
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no convention)" }
|
|
228
|
+
],
|
|
229
|
+
initialValue: state.hookNaming ?? SENTINEL_CLEAR
|
|
230
|
+
});
|
|
231
|
+
assertNotCancelled(selected);
|
|
232
|
+
state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
233
|
+
}
|
|
234
|
+
if (choice === "importAlias") {
|
|
235
|
+
const selected = await clack.select({
|
|
236
|
+
message: "Import alias pattern",
|
|
237
|
+
options: [
|
|
238
|
+
{ value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
|
|
239
|
+
{ value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
|
|
240
|
+
{ value: SENTINEL_CUSTOM, label: "Custom..." },
|
|
241
|
+
{ value: SENTINEL_CLEAR, label: "Clear (no alias)" }
|
|
242
|
+
],
|
|
243
|
+
initialValue: state.importAlias ?? SENTINEL_CLEAR
|
|
244
|
+
});
|
|
245
|
+
assertNotCancelled(selected);
|
|
246
|
+
if (selected === SENTINEL_CLEAR) {
|
|
247
|
+
state.importAlias = void 0;
|
|
248
|
+
} else if (selected === SENTINEL_CUSTOM) {
|
|
249
|
+
const result = await clack.text({
|
|
250
|
+
message: "Custom import alias (e.g. #/*)?",
|
|
251
|
+
initialValue: state.importAlias ?? "",
|
|
252
|
+
placeholder: "e.g. #/*",
|
|
253
|
+
validate: (v) => {
|
|
254
|
+
if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
|
|
255
|
+
if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
|
|
256
|
+
return "Must match pattern like @/*, ~/*, or #src/*";
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
assertNotCancelled(result);
|
|
260
|
+
state.importAlias = result.trim();
|
|
261
|
+
} else {
|
|
262
|
+
state.importAlias = selected;
|
|
263
|
+
}
|
|
146
264
|
}
|
|
147
|
-
];
|
|
148
|
-
if (tools?.isTypeScript) {
|
|
149
|
-
options.push({
|
|
150
|
-
value: "typecheck",
|
|
151
|
-
label: "Typecheck (tsc --noEmit)",
|
|
152
|
-
hint: "pre-commit hook + CI check"
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (tools?.linter) {
|
|
156
|
-
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
157
|
-
options.push({
|
|
158
|
-
value: "lint",
|
|
159
|
-
label: `Lint check (${linterName})`,
|
|
160
|
-
hint: "pre-commit hook + CI check"
|
|
161
|
-
});
|
|
162
265
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
266
|
+
}
|
|
267
|
+
async function promptTestingMenu(state) {
|
|
268
|
+
while (true) {
|
|
269
|
+
const options = [
|
|
270
|
+
{
|
|
271
|
+
value: "enforceMissingTests",
|
|
272
|
+
label: "Enforce missing tests",
|
|
273
|
+
hint: state.enforceMissingTests ? "yes" : "no"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
value: "testCoverage",
|
|
277
|
+
label: "Test coverage target",
|
|
278
|
+
hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
if (state.testCoverage > 0) {
|
|
282
|
+
options.push(
|
|
283
|
+
{
|
|
284
|
+
value: "coverageSummaryPath",
|
|
285
|
+
label: "Coverage summary path",
|
|
286
|
+
hint: state.coverageSummaryPath
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
value: "coverageCommand",
|
|
290
|
+
label: "Coverage command",
|
|
291
|
+
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
292
|
+
}
|
|
293
|
+
);
|
|
178
294
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
295
|
+
options.push({ value: "back", label: "Back" });
|
|
296
|
+
const choice = await clack.select({ message: "Testing & coverage", options });
|
|
297
|
+
assertNotCancelled(choice);
|
|
298
|
+
if (choice === "back") return;
|
|
299
|
+
if (choice === "enforceMissingTests") {
|
|
300
|
+
const result = await clack.confirm({
|
|
301
|
+
message: "Require every source file to have a corresponding test file?",
|
|
302
|
+
initialValue: state.enforceMissingTests
|
|
303
|
+
});
|
|
304
|
+
assertNotCancelled(result);
|
|
305
|
+
state.enforceMissingTests = result;
|
|
306
|
+
}
|
|
307
|
+
if (choice === "testCoverage") {
|
|
308
|
+
const result = await clack.text({
|
|
309
|
+
message: "Test coverage target (0 disables coverage checks)?",
|
|
310
|
+
initialValue: String(state.testCoverage),
|
|
311
|
+
validate: (v) => {
|
|
312
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
313
|
+
const n = Number.parseInt(v, 10);
|
|
314
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
assertNotCancelled(result);
|
|
318
|
+
state.testCoverage = Number.parseInt(result, 10);
|
|
319
|
+
}
|
|
320
|
+
if (choice === "coverageSummaryPath") {
|
|
321
|
+
const result = await clack.text({
|
|
322
|
+
message: "Coverage summary path (relative to package root)?",
|
|
323
|
+
initialValue: state.coverageSummaryPath,
|
|
324
|
+
validate: (v) => {
|
|
325
|
+
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
assertNotCancelled(result);
|
|
329
|
+
state.coverageSummaryPath = result.trim();
|
|
330
|
+
}
|
|
331
|
+
if (choice === "coverageCommand") {
|
|
332
|
+
const result = await clack.text({
|
|
333
|
+
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
334
|
+
initialValue: state.coverageCommand ?? "",
|
|
335
|
+
placeholder: "(auto-detect from package.json test runner)"
|
|
336
|
+
});
|
|
337
|
+
assertNotCancelled(result);
|
|
338
|
+
const trimmed = result.trim();
|
|
339
|
+
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
196
342
|
}
|
|
197
343
|
|
|
198
|
-
// src/utils/prompt-rules.ts
|
|
199
|
-
var clack4 = __toESM(require("@clack/prompts"), 1);
|
|
200
|
-
|
|
201
|
-
// src/utils/prompt-menu-handlers.ts
|
|
202
|
-
var clack3 = __toESM(require("@clack/prompts"), 1);
|
|
203
|
-
|
|
204
344
|
// src/utils/prompt-package-overrides.ts
|
|
205
|
-
var clack2 = __toESM(require("@clack/prompts"), 1);
|
|
206
345
|
function normalizePackageOverrides(packages) {
|
|
207
346
|
for (const pkg of packages) {
|
|
208
347
|
if (pkg.rules && Object.keys(pkg.rules).length === 0) {
|
|
@@ -211,121 +350,177 @@ function normalizePackageOverrides(packages) {
|
|
|
211
350
|
if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
|
|
212
351
|
delete pkg.coverage;
|
|
213
352
|
}
|
|
353
|
+
if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
|
|
354
|
+
delete pkg.conventions;
|
|
355
|
+
}
|
|
214
356
|
}
|
|
215
357
|
return packages;
|
|
216
358
|
}
|
|
217
|
-
function
|
|
359
|
+
function packageOverrideHint(pkg, defaults) {
|
|
360
|
+
const tags = [];
|
|
361
|
+
if (pkg.conventions?.fileNaming && pkg.conventions.fileNaming !== defaults.fileNamingValue) {
|
|
362
|
+
tags.push(pkg.conventions.fileNaming);
|
|
363
|
+
}
|
|
364
|
+
if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== defaults.maxFileLines && pkg.rules.maxFileLines > 0) {
|
|
365
|
+
tags.push(`${pkg.rules.maxFileLines} lines`);
|
|
366
|
+
}
|
|
218
367
|
const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
|
|
219
368
|
const isExempt = coverage === 0;
|
|
369
|
+
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
370
|
+
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
371
|
+
if (isExempt) {
|
|
372
|
+
tags.push(isTypesOnly ? "exempt (types-only)" : "exempt");
|
|
373
|
+
} else if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== defaults.testCoverage) {
|
|
374
|
+
tags.push(`${coverage}%`);
|
|
375
|
+
}
|
|
220
376
|
const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
|
|
221
377
|
const defaultCommand = defaults.coverageCommand ?? "";
|
|
222
378
|
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
223
|
-
const tags = [];
|
|
224
|
-
const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
|
|
225
|
-
const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
|
|
226
|
-
tags.push(isExempt ? isTypesOnly ? "exempt (types-only)" : "exempt" : `${coverage}%`);
|
|
227
379
|
if (hasSummaryOverride) tags.push("summary override");
|
|
228
380
|
if (hasCommandOverride) tags.push("command override");
|
|
229
|
-
return tags.join(", ");
|
|
381
|
+
return tags.length > 0 ? tags.join(", ") : "(no overrides)";
|
|
230
382
|
}
|
|
231
|
-
async function
|
|
383
|
+
async function promptPackageOverrides(packages, defaults) {
|
|
232
384
|
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
233
385
|
if (editablePackages.length === 0) return packages;
|
|
234
386
|
while (true) {
|
|
235
387
|
const selectedPath = await clack2.select({
|
|
236
|
-
message: "Select package to edit
|
|
388
|
+
message: "Select package to edit overrides",
|
|
237
389
|
options: [
|
|
238
390
|
...editablePackages.map((pkg) => ({
|
|
239
391
|
value: pkg.path,
|
|
240
392
|
label: `${pkg.path} (${pkg.name})`,
|
|
241
|
-
hint:
|
|
393
|
+
hint: packageOverrideHint(pkg, defaults)
|
|
242
394
|
})),
|
|
243
|
-
{ value:
|
|
395
|
+
{ value: SENTINEL_DONE, label: "Done" }
|
|
244
396
|
]
|
|
245
397
|
});
|
|
246
398
|
assertNotCancelled(selectedPath);
|
|
247
|
-
if (selectedPath ===
|
|
399
|
+
if (selectedPath === SENTINEL_DONE) break;
|
|
248
400
|
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
249
401
|
if (!target) continue;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
402
|
+
await promptSinglePackageOverrides(target, defaults);
|
|
403
|
+
normalizePackageOverrides(editablePackages);
|
|
404
|
+
}
|
|
405
|
+
return normalizePackageOverrides(packages);
|
|
406
|
+
}
|
|
407
|
+
async function promptSinglePackageOverrides(target, defaults) {
|
|
408
|
+
while (true) {
|
|
409
|
+
const effectiveNaming = target.conventions?.fileNaming ?? defaults.fileNamingValue;
|
|
410
|
+
const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
|
|
411
|
+
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
412
|
+
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
413
|
+
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
|
|
414
|
+
const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
|
|
415
|
+
const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
|
|
416
|
+
const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
|
|
417
|
+
const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
|
|
418
|
+
const choice = await clack2.select({
|
|
419
|
+
message: `Edit overrides for ${target.path}`,
|
|
420
|
+
options: [
|
|
421
|
+
{ value: "fileNaming", label: "File naming", hint: namingHint },
|
|
422
|
+
{ value: "maxFileLines", label: "Max file lines", hint: maxLinesHint },
|
|
423
|
+
{ value: "testCoverage", label: "Test coverage", hint: String(effectiveCoverage) },
|
|
424
|
+
{ value: "summaryPath", label: "Coverage summary path", hint: effectiveSummary },
|
|
425
|
+
{ value: "command", label: "Coverage command", hint: effectiveCommand },
|
|
426
|
+
{ value: "reset", label: "Reset all overrides for this package" },
|
|
427
|
+
{ value: "back", label: "Back to package list" }
|
|
428
|
+
]
|
|
429
|
+
});
|
|
430
|
+
assertNotCancelled(choice);
|
|
431
|
+
if (choice === "back") break;
|
|
432
|
+
if (choice === "fileNaming") {
|
|
433
|
+
const selected = await clack2.select({
|
|
434
|
+
message: `File naming for ${target.path}`,
|
|
256
435
|
options: [
|
|
257
|
-
|
|
258
|
-
{ value:
|
|
259
|
-
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
]
|
|
263
|
-
});
|
|
264
|
-
assertNotCancelled(choice);
|
|
265
|
-
if (choice === "back") break;
|
|
266
|
-
if (choice === "testCoverage") {
|
|
267
|
-
const result = await clack2.text({
|
|
268
|
-
message: "Package testCoverage (0 to exempt package)?",
|
|
269
|
-
initialValue: String(effectiveCoverage),
|
|
270
|
-
validate: (v) => {
|
|
271
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
272
|
-
const n = Number.parseInt(v, 10);
|
|
273
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
436
|
+
...FILE_NAMING_OPTIONS,
|
|
437
|
+
{ value: SENTINEL_NONE, label: "(none \u2014 exempt from checks)" },
|
|
438
|
+
{
|
|
439
|
+
value: SENTINEL_INHERIT,
|
|
440
|
+
label: `Inherit default${defaults.fileNamingValue ? ` (${defaults.fileNamingValue})` : ""}`
|
|
274
441
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
}
|
|
442
|
+
],
|
|
443
|
+
initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
|
|
444
|
+
});
|
|
445
|
+
assertNotCancelled(selected);
|
|
446
|
+
if (selected === SENTINEL_INHERIT) {
|
|
447
|
+
if (target.conventions) delete target.conventions.fileNaming;
|
|
448
|
+
} else if (selected === SENTINEL_NONE) {
|
|
449
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: "" };
|
|
450
|
+
} else {
|
|
451
|
+
target.conventions = { ...target.conventions ?? {}, fileNaming: selected };
|
|
285
452
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
300
|
-
}
|
|
453
|
+
}
|
|
454
|
+
if (choice === "maxFileLines") {
|
|
455
|
+
const result = await clack2.text({
|
|
456
|
+
message: `Max file lines for ${target.path} (blank to inherit default)?`,
|
|
457
|
+
initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
|
|
458
|
+
placeholder: String(defaults.maxFileLines)
|
|
459
|
+
});
|
|
460
|
+
assertNotCancelled(result);
|
|
461
|
+
const value = result.trim();
|
|
462
|
+
if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
|
|
463
|
+
if (target.rules) delete target.rules.maxFileLines;
|
|
464
|
+
} else {
|
|
465
|
+
target.rules = { ...target.rules ?? {}, maxFileLines: Number.parseInt(value, 10) };
|
|
301
466
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (value.length === 0 || value === defaultCommand) {
|
|
312
|
-
if (target.coverage) {
|
|
313
|
-
delete target.coverage.command;
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
467
|
+
}
|
|
468
|
+
if (choice === "testCoverage") {
|
|
469
|
+
const result = await clack2.text({
|
|
470
|
+
message: "Package testCoverage (0 to exempt package)?",
|
|
471
|
+
initialValue: String(effectiveCoverage),
|
|
472
|
+
validate: (v) => {
|
|
473
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
474
|
+
const n = Number.parseInt(v, 10);
|
|
475
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
317
476
|
}
|
|
477
|
+
});
|
|
478
|
+
assertNotCancelled(result);
|
|
479
|
+
const nextCoverage = Number.parseInt(result, 10);
|
|
480
|
+
if (nextCoverage === defaults.testCoverage) {
|
|
481
|
+
if (target.rules) delete target.rules.testCoverage;
|
|
482
|
+
} else {
|
|
483
|
+
target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
|
|
318
484
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
485
|
+
}
|
|
486
|
+
if (choice === "summaryPath") {
|
|
487
|
+
const result = await clack2.text({
|
|
488
|
+
message: "Path to coverage summary file (blank to inherit default)?",
|
|
489
|
+
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
490
|
+
placeholder: defaults.coverageSummaryPath
|
|
491
|
+
});
|
|
492
|
+
assertNotCancelled(result);
|
|
493
|
+
const value = result.trim();
|
|
494
|
+
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
495
|
+
if (target.coverage) delete target.coverage.summaryPath;
|
|
496
|
+
} else {
|
|
497
|
+
target.coverage = { ...target.coverage ?? {}, summaryPath: value };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (choice === "command") {
|
|
501
|
+
const result = await clack2.text({
|
|
502
|
+
message: "Coverage command (blank to auto-detect)?",
|
|
503
|
+
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
504
|
+
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
505
|
+
});
|
|
506
|
+
assertNotCancelled(result);
|
|
507
|
+
const value = result.trim();
|
|
508
|
+
const defaultCommand = defaults.coverageCommand ?? "";
|
|
509
|
+
if (value.length === 0 || value === defaultCommand) {
|
|
510
|
+
if (target.coverage) delete target.coverage.command;
|
|
511
|
+
} else {
|
|
512
|
+
target.coverage = { ...target.coverage ?? {}, command: value };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (choice === "reset") {
|
|
516
|
+
if (target.rules) {
|
|
517
|
+
delete target.rules.testCoverage;
|
|
518
|
+
delete target.rules.maxFileLines;
|
|
324
519
|
}
|
|
325
|
-
|
|
520
|
+
delete target.coverage;
|
|
521
|
+
delete target.conventions;
|
|
326
522
|
}
|
|
327
523
|
}
|
|
328
|
-
return normalizePackageOverrides(packages);
|
|
329
524
|
}
|
|
330
525
|
|
|
331
526
|
// src/utils/prompt-menu-handlers.ts
|
|
@@ -368,48 +563,21 @@ function getPackageDiffs(pkg, root) {
|
|
|
368
563
|
return diffs;
|
|
369
564
|
}
|
|
370
565
|
function buildMenuOptions(state, packageCount) {
|
|
371
|
-
const
|
|
566
|
+
const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
|
|
567
|
+
const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
|
|
568
|
+
const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
|
|
372
569
|
const options = [
|
|
373
|
-
{ value: "
|
|
374
|
-
{ value: "
|
|
570
|
+
{ value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
|
|
571
|
+
{ value: "naming", label: "Naming & conventions", hint: namingHint },
|
|
572
|
+
{ value: "testing", label: "Testing & coverage", hint: testingHint }
|
|
375
573
|
];
|
|
376
|
-
if (
|
|
574
|
+
if (packageCount > 0) {
|
|
377
575
|
options.push({
|
|
378
|
-
value: "
|
|
379
|
-
label: "
|
|
380
|
-
hint:
|
|
576
|
+
value: "packageOverrides",
|
|
577
|
+
label: "Per-package overrides",
|
|
578
|
+
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
381
579
|
});
|
|
382
580
|
}
|
|
383
|
-
const isMonorepo = packageCount > 0;
|
|
384
|
-
const coverageLabel = isMonorepo ? "Default coverage target" : "Test coverage target";
|
|
385
|
-
const coverageHint = state.testCoverage === 0 ? "0 (disabled)" : isMonorepo ? `${state.testCoverage}% (per-package default)` : `${state.testCoverage}%`;
|
|
386
|
-
options.push({ value: "testCoverage", label: coverageLabel, hint: coverageHint });
|
|
387
|
-
options.push({
|
|
388
|
-
value: "enforceMissingTests",
|
|
389
|
-
label: "Enforce missing tests",
|
|
390
|
-
hint: state.enforceMissingTests ? "yes" : "no"
|
|
391
|
-
});
|
|
392
|
-
if (state.testCoverage > 0) {
|
|
393
|
-
options.push(
|
|
394
|
-
{
|
|
395
|
-
value: "coverageSummaryPath",
|
|
396
|
-
label: isMonorepo ? "Default coverage summary path" : "Coverage summary path",
|
|
397
|
-
hint: state.coverageSummaryPath
|
|
398
|
-
},
|
|
399
|
-
{
|
|
400
|
-
value: "coverageCommand",
|
|
401
|
-
label: isMonorepo ? "Default coverage command" : "Coverage command",
|
|
402
|
-
hint: state.coverageCommand ?? "auto-detect from package.json test runner"
|
|
403
|
-
}
|
|
404
|
-
);
|
|
405
|
-
if (isMonorepo) {
|
|
406
|
-
options.push({
|
|
407
|
-
value: "packageOverrides",
|
|
408
|
-
label: "Per-package coverage overrides",
|
|
409
|
-
hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
581
|
options.push(
|
|
414
582
|
{ value: "reset", label: "Reset all to detected defaults" },
|
|
415
583
|
{ value: "done", label: "Done" }
|
|
@@ -417,37 +585,43 @@ function buildMenuOptions(state, packageCount) {
|
|
|
417
585
|
return options;
|
|
418
586
|
}
|
|
419
587
|
function clonePackages(packages) {
|
|
420
|
-
return packages
|
|
421
|
-
...pkg,
|
|
422
|
-
stack: pkg.stack ? { ...pkg.stack } : void 0,
|
|
423
|
-
structure: pkg.structure ? { ...pkg.structure } : void 0,
|
|
424
|
-
conventions: pkg.conventions ? { ...pkg.conventions } : void 0,
|
|
425
|
-
rules: pkg.rules ? { ...pkg.rules } : void 0,
|
|
426
|
-
coverage: pkg.coverage ? { ...pkg.coverage } : void 0,
|
|
427
|
-
ignore: pkg.ignore ? [...pkg.ignore] : void 0,
|
|
428
|
-
boundaries: pkg.boundaries ? {
|
|
429
|
-
deny: [...pkg.boundaries.deny],
|
|
430
|
-
ignore: pkg.boundaries.ignore ? [...pkg.boundaries.ignore] : void 0
|
|
431
|
-
} : void 0
|
|
432
|
-
}));
|
|
588
|
+
return packages ? structuredClone(packages) : void 0;
|
|
433
589
|
}
|
|
434
590
|
async function handleMenuChoice(choice, state, defaults, root) {
|
|
435
591
|
if (choice === "reset") {
|
|
436
592
|
state.maxFileLines = defaults.maxFileLines;
|
|
593
|
+
state.maxTestFileLines = defaults.maxTestFileLines;
|
|
437
594
|
state.testCoverage = defaults.testCoverage;
|
|
438
595
|
state.enforceMissingTests = defaults.enforceMissingTests;
|
|
439
596
|
state.enforceNaming = defaults.enforceNaming;
|
|
440
597
|
state.fileNamingValue = defaults.fileNamingValue;
|
|
598
|
+
state.componentNaming = defaults.componentNaming;
|
|
599
|
+
state.hookNaming = defaults.hookNaming;
|
|
600
|
+
state.importAlias = defaults.importAlias;
|
|
441
601
|
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
442
602
|
state.coverageCommand = defaults.coverageCommand;
|
|
443
603
|
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
444
604
|
clack3.log.info("Reset all rules to detected defaults.");
|
|
445
605
|
return;
|
|
446
606
|
}
|
|
607
|
+
if (choice === "fileLimits") {
|
|
608
|
+
await promptFileLimitsMenu(state);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (choice === "naming") {
|
|
612
|
+
await promptNamingMenu(state);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (choice === "testing") {
|
|
616
|
+
await promptTestingMenu(state);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
447
619
|
if (choice === "packageOverrides") {
|
|
448
620
|
if (state.packageOverrides) {
|
|
449
621
|
const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
|
|
450
|
-
state.packageOverrides = await
|
|
622
|
+
state.packageOverrides = await promptPackageOverrides(state.packageOverrides, {
|
|
623
|
+
fileNamingValue: state.fileNamingValue,
|
|
624
|
+
maxFileLines: state.maxFileLines,
|
|
451
625
|
testCoverage: state.testCoverage,
|
|
452
626
|
coverageSummaryPath: state.coverageSummaryPath,
|
|
453
627
|
coverageCommand: state.coverageCommand
|
|
@@ -460,89 +634,9 @@ async function handleMenuChoice(choice, state, defaults, root) {
|
|
|
460
634
|
}
|
|
461
635
|
return;
|
|
462
636
|
}
|
|
463
|
-
if (choice === "maxFileLines") {
|
|
464
|
-
const result = await clack3.text({
|
|
465
|
-
message: "Maximum lines per source file?",
|
|
466
|
-
initialValue: String(state.maxFileLines),
|
|
467
|
-
validate: (v) => {
|
|
468
|
-
if (typeof v !== "string") return "Enter a positive number";
|
|
469
|
-
const n = Number.parseInt(v, 10);
|
|
470
|
-
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
assertNotCancelled(result);
|
|
474
|
-
state.maxFileLines = Number.parseInt(result, 10);
|
|
475
|
-
}
|
|
476
|
-
if (choice === "enforceMissingTests") {
|
|
477
|
-
const result = await clack3.confirm({
|
|
478
|
-
message: "Require every source file to have a corresponding test file?",
|
|
479
|
-
initialValue: state.enforceMissingTests
|
|
480
|
-
});
|
|
481
|
-
assertNotCancelled(result);
|
|
482
|
-
state.enforceMissingTests = result;
|
|
483
|
-
}
|
|
484
|
-
if (choice === "testCoverage") {
|
|
485
|
-
const result = await clack3.text({
|
|
486
|
-
message: "Test coverage target (0 disables coverage checks)?",
|
|
487
|
-
initialValue: String(state.testCoverage),
|
|
488
|
-
validate: (v) => {
|
|
489
|
-
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
490
|
-
const n = Number.parseInt(v, 10);
|
|
491
|
-
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
assertNotCancelled(result);
|
|
495
|
-
state.testCoverage = Number.parseInt(result, 10);
|
|
496
|
-
}
|
|
497
|
-
if (choice === "coverageSummaryPath") {
|
|
498
|
-
const result = await clack3.text({
|
|
499
|
-
message: "Coverage summary path (relative to package root)?",
|
|
500
|
-
initialValue: state.coverageSummaryPath,
|
|
501
|
-
validate: (v) => {
|
|
502
|
-
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
assertNotCancelled(result);
|
|
506
|
-
state.coverageSummaryPath = result.trim();
|
|
507
|
-
}
|
|
508
|
-
if (choice === "coverageCommand") {
|
|
509
|
-
const result = await clack3.text({
|
|
510
|
-
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
511
|
-
initialValue: state.coverageCommand ?? "",
|
|
512
|
-
placeholder: "(auto-detect from package.json test runner)"
|
|
513
|
-
});
|
|
514
|
-
assertNotCancelled(result);
|
|
515
|
-
const trimmed = result.trim();
|
|
516
|
-
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
517
|
-
}
|
|
518
|
-
if (choice === "enforceNaming") {
|
|
519
|
-
const result = await clack3.confirm({
|
|
520
|
-
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
521
|
-
initialValue: state.enforceNaming
|
|
522
|
-
});
|
|
523
|
-
assertNotCancelled(result);
|
|
524
|
-
state.enforceNaming = result;
|
|
525
|
-
}
|
|
526
|
-
if (choice === "fileNaming") {
|
|
527
|
-
const selected = await clack3.select({
|
|
528
|
-
message: "Which file naming convention should be enforced?",
|
|
529
|
-
options: [
|
|
530
|
-
{ value: "kebab-case", label: "kebab-case" },
|
|
531
|
-
{ value: "camelCase", label: "camelCase" },
|
|
532
|
-
{ value: "PascalCase", label: "PascalCase" },
|
|
533
|
-
{ value: "snake_case", label: "snake_case" }
|
|
534
|
-
],
|
|
535
|
-
initialValue: state.fileNamingValue
|
|
536
|
-
});
|
|
537
|
-
assertNotCancelled(selected);
|
|
538
|
-
state.fileNamingValue = selected;
|
|
539
|
-
}
|
|
540
637
|
}
|
|
541
638
|
|
|
542
639
|
// src/utils/prompt-rules.ts
|
|
543
|
-
function getRootPackage(packages) {
|
|
544
|
-
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
545
|
-
}
|
|
546
640
|
async function promptRuleMenu(defaults) {
|
|
547
641
|
const state = {
|
|
548
642
|
...defaults,
|
|
@@ -559,10 +653,14 @@ async function promptRuleMenu(defaults) {
|
|
|
559
653
|
}
|
|
560
654
|
return {
|
|
561
655
|
maxFileLines: state.maxFileLines,
|
|
656
|
+
maxTestFileLines: state.maxTestFileLines,
|
|
562
657
|
testCoverage: state.testCoverage,
|
|
563
658
|
enforceMissingTests: state.enforceMissingTests,
|
|
564
659
|
enforceNaming: state.enforceNaming,
|
|
565
660
|
fileNamingValue: state.fileNamingValue,
|
|
661
|
+
componentNaming: state.componentNaming,
|
|
662
|
+
hookNaming: state.hookNaming,
|
|
663
|
+
importAlias: state.importAlias,
|
|
566
664
|
coverageSummaryPath: state.coverageSummaryPath,
|
|
567
665
|
coverageCommand: state.coverageCommand,
|
|
568
666
|
packageOverrides: state.packageOverrides
|
|
@@ -610,30 +708,6 @@ async function promptExistingConfigAction(configFile) {
|
|
|
610
708
|
assertNotCancelled(result);
|
|
611
709
|
return result;
|
|
612
710
|
}
|
|
613
|
-
async function promptInitDecision() {
|
|
614
|
-
const result = await clack5.select({
|
|
615
|
-
message: "How do you want to proceed?",
|
|
616
|
-
options: [
|
|
617
|
-
{
|
|
618
|
-
value: "accept",
|
|
619
|
-
label: "Accept defaults",
|
|
620
|
-
hint: "writes the config with these defaults; use --enforce in CI to block"
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
value: "customize",
|
|
624
|
-
label: "Customize rules",
|
|
625
|
-
hint: "edit limits, naming, test coverage, and package overrides"
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
value: "review",
|
|
629
|
-
label: "Review detected details",
|
|
630
|
-
hint: "show the full scan report with package and structure details"
|
|
631
|
-
}
|
|
632
|
-
]
|
|
633
|
-
});
|
|
634
|
-
assertNotCancelled(result);
|
|
635
|
-
return result;
|
|
636
|
-
}
|
|
637
711
|
|
|
638
712
|
// src/utils/resolve-workspace-packages.ts
|
|
639
713
|
var fs2 = __toESM(require("fs"), 1);
|
|
@@ -826,7 +900,7 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
826
900
|
}
|
|
827
901
|
|
|
828
902
|
// src/commands/check-coverage.ts
|
|
829
|
-
var
|
|
903
|
+
var import_node_child_process = require("child_process");
|
|
830
904
|
var fs4 = __toESM(require("fs"), 1);
|
|
831
905
|
var path4 = __toESM(require("path"), 1);
|
|
832
906
|
var import_config3 = require("@viberails/config");
|
|
@@ -869,7 +943,7 @@ function readCoveragePercentage(summaryPath) {
|
|
|
869
943
|
}
|
|
870
944
|
}
|
|
871
945
|
function runCoverageCommand(pkgRoot, command) {
|
|
872
|
-
const result = (0,
|
|
946
|
+
const result = (0, import_node_child_process.spawnSync)(command, {
|
|
873
947
|
cwd: pkgRoot,
|
|
874
948
|
shell: true,
|
|
875
949
|
encoding: "utf-8",
|
|
@@ -964,7 +1038,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
|
964
1038
|
}
|
|
965
1039
|
|
|
966
1040
|
// src/commands/check-files.ts
|
|
967
|
-
var
|
|
1041
|
+
var import_node_child_process2 = require("child_process");
|
|
968
1042
|
var fs5 = __toESM(require("fs"), 1);
|
|
969
1043
|
var path5 = __toESM(require("path"), 1);
|
|
970
1044
|
var import_config4 = require("@viberails/config");
|
|
@@ -1037,7 +1111,7 @@ function checkNaming(relPath, conventions) {
|
|
|
1037
1111
|
}
|
|
1038
1112
|
function getStagedFiles(projectRoot) {
|
|
1039
1113
|
try {
|
|
1040
|
-
const output = (0,
|
|
1114
|
+
const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
|
|
1041
1115
|
cwd: projectRoot,
|
|
1042
1116
|
encoding: "utf-8",
|
|
1043
1117
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1049,12 +1123,12 @@ function getStagedFiles(projectRoot) {
|
|
|
1049
1123
|
}
|
|
1050
1124
|
function getDiffFiles(projectRoot, base) {
|
|
1051
1125
|
try {
|
|
1052
|
-
const allOutput = (0,
|
|
1126
|
+
const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
|
|
1053
1127
|
cwd: projectRoot,
|
|
1054
1128
|
encoding: "utf-8",
|
|
1055
1129
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1056
1130
|
});
|
|
1057
|
-
const addedOutput = (0,
|
|
1131
|
+
const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
|
|
1058
1132
|
cwd: projectRoot,
|
|
1059
1133
|
encoding: "utf-8",
|
|
1060
1134
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1100,7 +1174,7 @@ function deletedTestFileToSourceFile(deletedTestFile, config) {
|
|
|
1100
1174
|
}
|
|
1101
1175
|
function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
1102
1176
|
try {
|
|
1103
|
-
const output = (0,
|
|
1177
|
+
const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=D", {
|
|
1104
1178
|
cwd: projectRoot,
|
|
1105
1179
|
encoding: "utf-8",
|
|
1106
1180
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1112,7 +1186,7 @@ function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
|
1112
1186
|
}
|
|
1113
1187
|
function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
|
|
1114
1188
|
try {
|
|
1115
|
-
const output = (0,
|
|
1189
|
+
const output = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
|
|
1116
1190
|
cwd: projectRoot,
|
|
1117
1191
|
encoding: "utf-8",
|
|
1118
1192
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1251,13 +1325,13 @@ function checkMissingTests(projectRoot, config, severity) {
|
|
|
1251
1325
|
const testSuffix = testPattern.replace("*", "");
|
|
1252
1326
|
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
1253
1327
|
for (const relFile of sourceFiles) {
|
|
1254
|
-
const
|
|
1255
|
-
if (
|
|
1328
|
+
const basename10 = path6.basename(relFile);
|
|
1329
|
+
if (basename10.includes(".test.") || basename10.includes(".spec.") || basename10.startsWith("index.") || basename10.endsWith(".d.ts")) {
|
|
1256
1330
|
continue;
|
|
1257
1331
|
}
|
|
1258
|
-
const ext = path6.extname(
|
|
1332
|
+
const ext = path6.extname(basename10);
|
|
1259
1333
|
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
1260
|
-
const stem =
|
|
1334
|
+
const stem = basename10.slice(0, -ext.length);
|
|
1261
1335
|
const expectedTestFile = `${stem}${testSuffix}`;
|
|
1262
1336
|
const dir = path6.dirname(path6.join(projectRoot, relFile));
|
|
1263
1337
|
const colocatedTest = path6.join(dir, expectedTestFile);
|
|
@@ -1338,9 +1412,9 @@ async function checkCommand(options, cwd) {
|
|
|
1338
1412
|
}
|
|
1339
1413
|
const violations = [];
|
|
1340
1414
|
const severity = options.enforce ? "error" : "warn";
|
|
1341
|
-
const
|
|
1415
|
+
const log8 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
|
|
1342
1416
|
};
|
|
1343
|
-
|
|
1417
|
+
log8(" Checking files...");
|
|
1344
1418
|
for (const file of filesToCheck) {
|
|
1345
1419
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1346
1420
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1373,9 +1447,9 @@ async function checkCommand(options, cwd) {
|
|
|
1373
1447
|
}
|
|
1374
1448
|
}
|
|
1375
1449
|
}
|
|
1376
|
-
|
|
1450
|
+
log8(" done\n");
|
|
1377
1451
|
if (!options.files) {
|
|
1378
|
-
|
|
1452
|
+
log8(" Checking missing tests...");
|
|
1379
1453
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1380
1454
|
if (options.staged) {
|
|
1381
1455
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1388,14 +1462,14 @@ async function checkCommand(options, cwd) {
|
|
|
1388
1462
|
} else {
|
|
1389
1463
|
violations.push(...testViolations);
|
|
1390
1464
|
}
|
|
1391
|
-
|
|
1465
|
+
log8(" done\n");
|
|
1392
1466
|
}
|
|
1393
1467
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1394
|
-
|
|
1468
|
+
log8(" Running test coverage...\n");
|
|
1395
1469
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1396
1470
|
staged: options.staged,
|
|
1397
1471
|
enforce: options.enforce,
|
|
1398
|
-
onProgress: (pkg) =>
|
|
1472
|
+
onProgress: (pkg) => log8(` Coverage: ${pkg}...
|
|
1399
1473
|
`)
|
|
1400
1474
|
});
|
|
1401
1475
|
violations.push(...coverageViolations);
|
|
@@ -1420,7 +1494,7 @@ async function checkCommand(options, cwd) {
|
|
|
1420
1494
|
severity
|
|
1421
1495
|
});
|
|
1422
1496
|
}
|
|
1423
|
-
|
|
1497
|
+
log8(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1424
1498
|
`);
|
|
1425
1499
|
}
|
|
1426
1500
|
if (options.format === "json") {
|
|
@@ -1675,15 +1749,6 @@ function formatMonorepoResultsText(scanResult) {
|
|
|
1675
1749
|
}
|
|
1676
1750
|
|
|
1677
1751
|
// src/display.ts
|
|
1678
|
-
var INIT_OVERVIEW_NAMES = {
|
|
1679
|
-
typescript: "TypeScript",
|
|
1680
|
-
javascript: "JavaScript",
|
|
1681
|
-
eslint: "ESLint",
|
|
1682
|
-
prettier: "Prettier",
|
|
1683
|
-
jest: "Jest",
|
|
1684
|
-
vitest: "Vitest",
|
|
1685
|
-
biome: "Biome"
|
|
1686
|
-
};
|
|
1687
1752
|
function formatItem(item, nameMap) {
|
|
1688
1753
|
const name = nameMap?.[item.name] ?? item.name;
|
|
1689
1754
|
return item.version ? `${name} ${item.version}` : name;
|
|
@@ -1813,134 +1878,6 @@ function displayRulesPreview(config) {
|
|
|
1813
1878
|
);
|
|
1814
1879
|
console.log("");
|
|
1815
1880
|
}
|
|
1816
|
-
function formatDetectedOverview(scanResult) {
|
|
1817
|
-
const { stack } = scanResult;
|
|
1818
|
-
const primaryParts = [];
|
|
1819
|
-
const secondaryParts = [];
|
|
1820
|
-
const formatOverviewItem = (item, nameMap) => formatItem(item, { ...INIT_OVERVIEW_NAMES, ...nameMap });
|
|
1821
|
-
if (scanResult.packages.length > 1) {
|
|
1822
|
-
primaryParts.push("monorepo");
|
|
1823
|
-
primaryParts.push(`${scanResult.packages.length} packages`);
|
|
1824
|
-
} else if (stack.framework) {
|
|
1825
|
-
primaryParts.push(formatItem(stack.framework, import_types3.FRAMEWORK_NAMES));
|
|
1826
|
-
} else {
|
|
1827
|
-
primaryParts.push("single package");
|
|
1828
|
-
}
|
|
1829
|
-
primaryParts.push(formatOverviewItem(stack.language));
|
|
1830
|
-
if (stack.styling) {
|
|
1831
|
-
primaryParts.push(formatOverviewItem(stack.styling, import_types3.STYLING_NAMES));
|
|
1832
|
-
}
|
|
1833
|
-
if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
|
|
1834
|
-
if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
|
|
1835
|
-
if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
|
|
1836
|
-
if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
|
|
1837
|
-
const primary = primaryParts.map((part) => import_chalk5.default.cyan(part)).join(import_chalk5.default.dim(" \xB7 "));
|
|
1838
|
-
const secondary = secondaryParts.join(import_chalk5.default.dim(" \xB7 "));
|
|
1839
|
-
return secondary ? `${primary}
|
|
1840
|
-
${import_chalk5.default.dim(secondary)}` : primary;
|
|
1841
|
-
}
|
|
1842
|
-
function displayInitOverview(scanResult, config, exemptedPackages) {
|
|
1843
|
-
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
1844
|
-
const isMonorepo = config.packages.length > 1;
|
|
1845
|
-
const ok = import_chalk5.default.green("\u2713");
|
|
1846
|
-
const info = import_chalk5.default.yellow("~");
|
|
1847
|
-
console.log("");
|
|
1848
|
-
console.log(` ${import_chalk5.default.bold("Ready to initialize:")}`);
|
|
1849
|
-
console.log(` ${formatDetectedOverview(scanResult)}`);
|
|
1850
|
-
console.log("");
|
|
1851
|
-
console.log(` ${import_chalk5.default.bold("Rules to apply:")}`);
|
|
1852
|
-
console.log(` ${ok} Max file size: ${import_chalk5.default.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
1853
|
-
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
1854
|
-
if (config.rules.enforceNaming && fileNaming) {
|
|
1855
|
-
console.log(` ${ok} File naming: ${import_chalk5.default.cyan(fileNaming)}`);
|
|
1856
|
-
} else {
|
|
1857
|
-
console.log(` ${info} File naming: ${import_chalk5.default.dim("not enforced")}`);
|
|
1858
|
-
}
|
|
1859
|
-
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
1860
|
-
if (config.rules.enforceMissingTests && testPattern) {
|
|
1861
|
-
console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan(`enforced (${testPattern})`)}`);
|
|
1862
|
-
} else if (config.rules.enforceMissingTests) {
|
|
1863
|
-
console.log(` ${ok} Missing tests: ${import_chalk5.default.cyan("enforced")}`);
|
|
1864
|
-
} else {
|
|
1865
|
-
console.log(` ${info} Missing tests: ${import_chalk5.default.dim("not enforced")}`);
|
|
1866
|
-
}
|
|
1867
|
-
if (config.rules.testCoverage > 0) {
|
|
1868
|
-
if (isMonorepo) {
|
|
1869
|
-
const withCoverage = config.packages.filter(
|
|
1870
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
1871
|
-
);
|
|
1872
|
-
console.log(
|
|
1873
|
-
` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk5.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
1874
|
-
);
|
|
1875
|
-
} else {
|
|
1876
|
-
console.log(` ${ok} Coverage: ${import_chalk5.default.cyan(`${config.rules.testCoverage}%`)}`);
|
|
1877
|
-
}
|
|
1878
|
-
} else {
|
|
1879
|
-
console.log(` ${info} Coverage: ${import_chalk5.default.dim("disabled")}`);
|
|
1880
|
-
}
|
|
1881
|
-
if (exemptedPackages.length > 0) {
|
|
1882
|
-
console.log(
|
|
1883
|
-
` ${import_chalk5.default.dim(" exempted:")} ${import_chalk5.default.dim(exemptedPackages.join(", "))} ${import_chalk5.default.dim("(types-only)")}`
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
console.log("");
|
|
1887
|
-
console.log(` ${import_chalk5.default.bold("Also available:")}`);
|
|
1888
|
-
if (isMonorepo) {
|
|
1889
|
-
console.log(` ${info} Infer boundaries from current imports`);
|
|
1890
|
-
}
|
|
1891
|
-
console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
|
|
1892
|
-
console.log(
|
|
1893
|
-
`
|
|
1894
|
-
${import_chalk5.default.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
|
|
1895
|
-
);
|
|
1896
|
-
console.log("");
|
|
1897
|
-
}
|
|
1898
|
-
function summarizeSelectedIntegrations(integrations, opts) {
|
|
1899
|
-
const lines = [];
|
|
1900
|
-
if (opts.hasBoundaries) {
|
|
1901
|
-
lines.push("\u2713 Boundary rules: inferred from current imports");
|
|
1902
|
-
} else {
|
|
1903
|
-
lines.push("~ Boundary rules: not enabled");
|
|
1904
|
-
}
|
|
1905
|
-
if (opts.hasCoverage) {
|
|
1906
|
-
lines.push("\u2713 Coverage checks: enabled");
|
|
1907
|
-
} else {
|
|
1908
|
-
lines.push("~ Coverage checks: disabled");
|
|
1909
|
-
}
|
|
1910
|
-
const selectedIntegrations = [
|
|
1911
|
-
integrations.preCommitHook ? "pre-commit hook" : void 0,
|
|
1912
|
-
integrations.typecheckHook ? "typecheck" : void 0,
|
|
1913
|
-
integrations.lintHook ? "lint check" : void 0,
|
|
1914
|
-
integrations.claudeCodeHook ? "Claude Code hook" : void 0,
|
|
1915
|
-
integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
|
|
1916
|
-
integrations.githubAction ? "GitHub Actions workflow" : void 0
|
|
1917
|
-
].filter(Boolean);
|
|
1918
|
-
if (selectedIntegrations.length > 0) {
|
|
1919
|
-
lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
|
|
1920
|
-
} else {
|
|
1921
|
-
lines.push("~ Integrations: none selected");
|
|
1922
|
-
}
|
|
1923
|
-
return lines;
|
|
1924
|
-
}
|
|
1925
|
-
function displaySetupPlan(config, integrations, opts = {}) {
|
|
1926
|
-
const configFile = opts.configFile ?? "viberails.config.json";
|
|
1927
|
-
const lines = summarizeSelectedIntegrations(integrations, {
|
|
1928
|
-
hasBoundaries: config.rules.enforceBoundaries,
|
|
1929
|
-
hasCoverage: config.rules.testCoverage > 0
|
|
1930
|
-
});
|
|
1931
|
-
console.log("");
|
|
1932
|
-
console.log(` ${import_chalk5.default.bold("Ready to write:")}`);
|
|
1933
|
-
console.log(
|
|
1934
|
-
` ${opts.replacingExistingConfig ? import_chalk5.default.yellow("!") : import_chalk5.default.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? import_chalk5.default.dim(" (replacing existing config)") : ""}`
|
|
1935
|
-
);
|
|
1936
|
-
console.log(` ${import_chalk5.default.green("\u2713")} .viberails/context.md`);
|
|
1937
|
-
console.log(` ${import_chalk5.default.green("\u2713")} .viberails/scan-result.json`);
|
|
1938
|
-
for (const line of lines) {
|
|
1939
|
-
const icon = line.startsWith("\u2713") ? import_chalk5.default.green("\u2713") : import_chalk5.default.yellow("~");
|
|
1940
|
-
console.log(` ${icon} ${line.slice(2)}`);
|
|
1941
|
-
}
|
|
1942
|
-
console.log("");
|
|
1943
|
-
}
|
|
1944
1881
|
|
|
1945
1882
|
// src/display-text.ts
|
|
1946
1883
|
function plainConfidenceLabel(convention) {
|
|
@@ -2059,7 +1996,9 @@ function formatScanResultsText(scanResult) {
|
|
|
2059
1996
|
// src/utils/apply-rule-overrides.ts
|
|
2060
1997
|
function applyRuleOverrides(config, overrides) {
|
|
2061
1998
|
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
1999
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2062
2000
|
config.rules.maxFileLines = overrides.maxFileLines;
|
|
2001
|
+
config.rules.maxTestFileLines = overrides.maxTestFileLines;
|
|
2063
2002
|
config.rules.testCoverage = overrides.testCoverage;
|
|
2064
2003
|
config.rules.enforceMissingTests = overrides.enforceMissingTests;
|
|
2065
2004
|
config.rules.enforceNaming = overrides.enforceNaming;
|
|
@@ -2073,7 +2012,6 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2073
2012
|
}
|
|
2074
2013
|
}
|
|
2075
2014
|
if (overrides.fileNamingValue) {
|
|
2076
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2077
2015
|
const oldNaming = rootPkg.conventions?.fileNaming;
|
|
2078
2016
|
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2079
2017
|
rootPkg.conventions.fileNaming = overrides.fileNamingValue;
|
|
@@ -2085,6 +2023,18 @@ function applyRuleOverrides(config, overrides) {
|
|
|
2085
2023
|
}
|
|
2086
2024
|
}
|
|
2087
2025
|
}
|
|
2026
|
+
if (rootPkg) {
|
|
2027
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
2028
|
+
if (overrides.componentNaming !== void 0) {
|
|
2029
|
+
rootPkg.conventions.componentNaming = overrides.componentNaming || void 0;
|
|
2030
|
+
}
|
|
2031
|
+
if (overrides.hookNaming !== void 0) {
|
|
2032
|
+
rootPkg.conventions.hookNaming = overrides.hookNaming || void 0;
|
|
2033
|
+
}
|
|
2034
|
+
if (overrides.importAlias !== void 0) {
|
|
2035
|
+
rootPkg.conventions.importAlias = overrides.importAlias || void 0;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2088
2038
|
}
|
|
2089
2039
|
|
|
2090
2040
|
// src/utils/diff-configs.ts
|
|
@@ -2261,10 +2211,14 @@ async function configCommand(options, cwd) {
|
|
|
2261
2211
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2262
2212
|
const overrides = await promptRuleMenu({
|
|
2263
2213
|
maxFileLines: config.rules.maxFileLines,
|
|
2214
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2264
2215
|
testCoverage: config.rules.testCoverage,
|
|
2265
2216
|
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2266
2217
|
enforceNaming: config.rules.enforceNaming,
|
|
2267
2218
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2219
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2220
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2221
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
2268
2222
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
2269
2223
|
coverageCommand: config.defaults?.coverage?.command,
|
|
2270
2224
|
packageOverrides: config.packages
|
|
@@ -2339,7 +2293,7 @@ var import_config7 = require("@viberails/config");
|
|
|
2339
2293
|
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
2340
2294
|
|
|
2341
2295
|
// src/commands/fix-helpers.ts
|
|
2342
|
-
var
|
|
2296
|
+
var import_node_child_process3 = require("child_process");
|
|
2343
2297
|
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
2344
2298
|
function printPlan(renames, stubs) {
|
|
2345
2299
|
if (renames.length > 0) {
|
|
@@ -2357,7 +2311,7 @@ function printPlan(renames, stubs) {
|
|
|
2357
2311
|
}
|
|
2358
2312
|
function checkGitDirty(projectRoot) {
|
|
2359
2313
|
try {
|
|
2360
|
-
const output = (0,
|
|
2314
|
+
const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
|
|
2361
2315
|
cwd: projectRoot,
|
|
2362
2316
|
encoding: "utf-8",
|
|
2363
2317
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -2645,10 +2599,10 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
|
2645
2599
|
const pkg = resolvePackageForFile(sourceRelPath, config);
|
|
2646
2600
|
const testPattern = pkg?.structure?.testPattern;
|
|
2647
2601
|
if (!testPattern) return null;
|
|
2648
|
-
const
|
|
2649
|
-
const ext = path12.extname(
|
|
2602
|
+
const basename10 = path12.basename(sourceRelPath);
|
|
2603
|
+
const ext = path12.extname(basename10);
|
|
2650
2604
|
if (!ext) return null;
|
|
2651
|
-
const stem =
|
|
2605
|
+
const stem = basename10.slice(0, -ext.length);
|
|
2652
2606
|
const testSuffix = testPattern.replace("*", "");
|
|
2653
2607
|
const testFilename = `${stem}${testSuffix}`;
|
|
2654
2608
|
const dir = path12.dirname(path12.join(projectRoot, sourceRelPath));
|
|
@@ -2820,169 +2774,601 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
|
|
|
2820
2774
|
}
|
|
2821
2775
|
|
|
2822
2776
|
// src/commands/init.ts
|
|
2823
|
-
var
|
|
2824
|
-
var
|
|
2777
|
+
var fs21 = __toESM(require("fs"), 1);
|
|
2778
|
+
var path21 = __toESM(require("path"), 1);
|
|
2779
|
+
var clack12 = __toESM(require("@clack/prompts"), 1);
|
|
2780
|
+
var import_config9 = require("@viberails/config");
|
|
2781
|
+
var import_scanner3 = require("@viberails/scanner");
|
|
2782
|
+
var import_chalk13 = __toESM(require("chalk"), 1);
|
|
2783
|
+
|
|
2784
|
+
// src/utils/check-prerequisites.ts
|
|
2785
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
2786
|
+
var path14 = __toESM(require("path"), 1);
|
|
2787
|
+
var clack7 = __toESM(require("@clack/prompts"), 1);
|
|
2788
|
+
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
2789
|
+
|
|
2790
|
+
// src/utils/spawn-async.ts
|
|
2791
|
+
var import_node_child_process4 = require("child_process");
|
|
2792
|
+
function spawnAsync(command, cwd) {
|
|
2793
|
+
return new Promise((resolve4) => {
|
|
2794
|
+
const child = (0, import_node_child_process4.spawn)(command, { cwd, shell: true, stdio: "pipe" });
|
|
2795
|
+
let stdout = "";
|
|
2796
|
+
let stderr = "";
|
|
2797
|
+
child.stdout.on("data", (d) => {
|
|
2798
|
+
stdout += d.toString();
|
|
2799
|
+
});
|
|
2800
|
+
child.stderr.on("data", (d) => {
|
|
2801
|
+
stderr += d.toString();
|
|
2802
|
+
});
|
|
2803
|
+
child.on("close", (status) => {
|
|
2804
|
+
resolve4({ status, stdout, stderr });
|
|
2805
|
+
});
|
|
2806
|
+
child.on("error", () => {
|
|
2807
|
+
resolve4({ status: 1, stdout, stderr });
|
|
2808
|
+
});
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// src/utils/check-prerequisites.ts
|
|
2813
|
+
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
2814
|
+
const pm = scanResult.stack.packageManager.name;
|
|
2815
|
+
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
2816
|
+
const hasVitest = vitestPackages.length > 0 || scanResult.stack.testRunner?.name === "vitest";
|
|
2817
|
+
if (!hasVitest) return [];
|
|
2818
|
+
let installed = hasDependency(projectRoot, "@vitest/coverage-v8") || hasDependency(projectRoot, "@vitest/coverage-istanbul");
|
|
2819
|
+
if (!installed && vitestPackages.length > 0) {
|
|
2820
|
+
installed = vitestPackages.every((rel) => {
|
|
2821
|
+
const pkgDir = path14.join(projectRoot, rel);
|
|
2822
|
+
return hasDependency(pkgDir, "@vitest/coverage-v8") || hasDependency(pkgDir, "@vitest/coverage-istanbul");
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
const isWorkspace = scanResult.packages.length > 1;
|
|
2826
|
+
const addCmd = pm === "yarn" ? "yarn add -D" : pm === "pnpm" && isWorkspace ? "pnpm add -D -w" : pm === "npm" ? "npm install -D" : `${pm} add -D`;
|
|
2827
|
+
const affectedPackages = vitestPackages.length > 1 ? vitestPackages : void 0;
|
|
2828
|
+
const reason = affectedPackages ? `Required for coverage in: ${affectedPackages.join(", ")}` : "Required for coverage percentage checks with vitest";
|
|
2829
|
+
return [
|
|
2830
|
+
{
|
|
2831
|
+
label: "@vitest/coverage-v8",
|
|
2832
|
+
installed,
|
|
2833
|
+
installCommand: installed ? void 0 : `${addCmd} @vitest/coverage-v8`,
|
|
2834
|
+
reason,
|
|
2835
|
+
affectedPackages
|
|
2836
|
+
}
|
|
2837
|
+
];
|
|
2838
|
+
}
|
|
2839
|
+
function displayMissingPrereqs(prereqs) {
|
|
2840
|
+
const missing = prereqs.filter((p) => !p.installed);
|
|
2841
|
+
for (const m of missing) {
|
|
2842
|
+
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
2843
|
+
console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
|
|
2844
|
+
if (m.installCommand) {
|
|
2845
|
+
console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function planCoverageInstall(prereqs) {
|
|
2850
|
+
const missing = prereqs.find((p) => !p.installed && p.installCommand);
|
|
2851
|
+
if (!missing?.installCommand) return void 0;
|
|
2852
|
+
return {
|
|
2853
|
+
label: missing.label,
|
|
2854
|
+
command: missing.installCommand
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function hasDependency(projectRoot, name) {
|
|
2858
|
+
try {
|
|
2859
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
2860
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
2861
|
+
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2862
|
+
} catch {
|
|
2863
|
+
return false;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/utils/deferred-install.ts
|
|
2825
2868
|
var clack8 = __toESM(require("@clack/prompts"), 1);
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2869
|
+
async function executeDeferredInstalls(projectRoot, installs) {
|
|
2870
|
+
if (installs.length === 0) return 0;
|
|
2871
|
+
let successCount = 0;
|
|
2872
|
+
for (const install of installs) {
|
|
2873
|
+
const s = clack8.spinner();
|
|
2874
|
+
s.start(`Installing ${install.label}...`);
|
|
2875
|
+
const result = await spawnAsync(install.command, projectRoot);
|
|
2876
|
+
if (result.status === 0) {
|
|
2877
|
+
s.stop(`Installed ${install.label}`);
|
|
2878
|
+
install.onSuccess?.();
|
|
2879
|
+
successCount++;
|
|
2880
|
+
} else {
|
|
2881
|
+
s.stop(`Failed to install ${install.label}`);
|
|
2882
|
+
clack8.log.warn(`Install manually: ${install.command}`);
|
|
2883
|
+
install.onFailure?.();
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
return successCount;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/utils/prompt-main-menu.ts
|
|
2890
|
+
var clack10 = __toESM(require("@clack/prompts"), 1);
|
|
2891
|
+
|
|
2892
|
+
// src/utils/prompt-integrations.ts
|
|
2893
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
2894
|
+
var path15 = __toESM(require("path"), 1);
|
|
2895
|
+
var clack9 = __toESM(require("@clack/prompts"), 1);
|
|
2896
|
+
function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
2897
|
+
if (pm === "yarn") return "yarn add -D lefthook";
|
|
2898
|
+
if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
|
|
2899
|
+
if (pm === "npm") return "npm install -D lefthook";
|
|
2900
|
+
return `${pm} add -D lefthook`;
|
|
2901
|
+
}
|
|
2902
|
+
async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
|
|
2903
|
+
const options = [];
|
|
2904
|
+
const needsLefthook = !hookManager;
|
|
2905
|
+
if (needsLefthook) {
|
|
2906
|
+
const pm = packageManager ?? "npm";
|
|
2907
|
+
options.push({
|
|
2908
|
+
value: "installLefthook",
|
|
2909
|
+
label: "Install Lefthook",
|
|
2910
|
+
hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2914
|
+
const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
|
|
2915
|
+
options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
|
|
2916
|
+
if (tools?.isTypeScript) {
|
|
2917
|
+
options.push({
|
|
2918
|
+
value: "typecheck",
|
|
2919
|
+
label: "Typecheck (tsc --noEmit)",
|
|
2920
|
+
hint: "pre-commit hook + CI check"
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
if (tools?.linter) {
|
|
2924
|
+
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
2925
|
+
options.push({
|
|
2926
|
+
value: "lint",
|
|
2927
|
+
label: `Lint check (${linterName})`,
|
|
2928
|
+
hint: "pre-commit hook + CI check"
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
options.push(
|
|
2932
|
+
{
|
|
2933
|
+
value: "claude",
|
|
2934
|
+
label: "Claude Code hook",
|
|
2935
|
+
hint: "checks files when Claude edits them"
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
value: "claudeMd",
|
|
2939
|
+
label: "CLAUDE.md reference",
|
|
2940
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
value: "githubAction",
|
|
2944
|
+
label: "GitHub Actions workflow",
|
|
2945
|
+
hint: "blocks PRs that fail viberails check"
|
|
2946
|
+
}
|
|
2947
|
+
);
|
|
2948
|
+
const initialValues = options.map((o) => o.value);
|
|
2949
|
+
const result = await clack9.multiselect({
|
|
2950
|
+
message: "Integrations",
|
|
2951
|
+
options,
|
|
2952
|
+
initialValues,
|
|
2953
|
+
required: false
|
|
2954
|
+
});
|
|
2955
|
+
assertNotCancelled(result);
|
|
2956
|
+
let lefthookInstall;
|
|
2957
|
+
if (needsLefthook && result.includes("installLefthook")) {
|
|
2958
|
+
const pm = packageManager ?? "npm";
|
|
2959
|
+
lefthookInstall = {
|
|
2960
|
+
label: "Lefthook",
|
|
2961
|
+
command: buildLefthookInstallCommand(pm, isWorkspace),
|
|
2962
|
+
onSuccess: projectRoot ? () => {
|
|
2963
|
+
const ymlPath = path15.join(projectRoot, "lefthook.yml");
|
|
2964
|
+
if (!fs15.existsSync(ymlPath)) {
|
|
2965
|
+
fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
|
|
2966
|
+
}
|
|
2967
|
+
} : void 0
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
return {
|
|
2971
|
+
choice: {
|
|
2972
|
+
preCommitHook: result.includes("preCommit"),
|
|
2973
|
+
claudeCodeHook: result.includes("claude"),
|
|
2974
|
+
claudeMdRef: result.includes("claudeMd"),
|
|
2975
|
+
githubAction: result.includes("githubAction"),
|
|
2976
|
+
typecheckHook: result.includes("typecheck"),
|
|
2977
|
+
lintHook: result.includes("lint")
|
|
2978
|
+
},
|
|
2979
|
+
lefthookInstall
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/utils/prompt-main-menu-hints.ts
|
|
2984
|
+
function fileLimitsHint(config) {
|
|
2985
|
+
const max = config.rules.maxFileLines;
|
|
2986
|
+
const test = config.rules.maxTestFileLines;
|
|
2987
|
+
return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
|
|
2988
|
+
}
|
|
2989
|
+
function fileNamingHint(config, scanResult) {
|
|
2990
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2991
|
+
const naming = rootPkg.conventions?.fileNaming;
|
|
2992
|
+
if (!config.rules.enforceNaming) return "not enforced";
|
|
2993
|
+
if (naming) {
|
|
2994
|
+
const detected = scanResult.packages.some(
|
|
2995
|
+
(p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
|
|
2996
|
+
);
|
|
2997
|
+
return detected ? `${naming} (detected)` : naming;
|
|
2998
|
+
}
|
|
2999
|
+
return "mixed \u2014 will not enforce if skipped";
|
|
3000
|
+
}
|
|
3001
|
+
function fileNamingStatus(config) {
|
|
3002
|
+
if (!config.rules.enforceNaming) return "disabled";
|
|
3003
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3004
|
+
return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
|
|
3005
|
+
}
|
|
3006
|
+
function missingTestsHint(config) {
|
|
3007
|
+
if (!config.rules.enforceMissingTests) return "not enforced";
|
|
3008
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3009
|
+
const pattern = rootPkg.structure?.testPattern;
|
|
3010
|
+
return pattern ? `enforced (${pattern})` : "enforced";
|
|
3011
|
+
}
|
|
3012
|
+
function coverageHint(config, hasTestRunner) {
|
|
3013
|
+
if (config.rules.testCoverage === 0) return "disabled";
|
|
3014
|
+
if (!hasTestRunner)
|
|
3015
|
+
return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
|
|
3016
|
+
const isMonorepo = config.packages.length > 1;
|
|
3017
|
+
if (isMonorepo) {
|
|
3018
|
+
const withCov = config.packages.filter(
|
|
3019
|
+
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
3020
|
+
);
|
|
3021
|
+
const exempt = config.packages.length - withCov.length;
|
|
3022
|
+
return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
|
|
3023
|
+
}
|
|
3024
|
+
return `${config.rules.testCoverage}%`;
|
|
3025
|
+
}
|
|
3026
|
+
function advancedNamingHint(config) {
|
|
3027
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3028
|
+
const parts = [];
|
|
3029
|
+
if (rootPkg.conventions?.componentNaming)
|
|
3030
|
+
parts.push(`${rootPkg.conventions.componentNaming} components`);
|
|
3031
|
+
if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
|
|
3032
|
+
if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
|
|
3033
|
+
return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
|
|
3034
|
+
}
|
|
3035
|
+
function integrationsHint(state) {
|
|
3036
|
+
if (!state.visited.integrations || !state.integrations)
|
|
3037
|
+
return "not configured \u2014 select to set up";
|
|
3038
|
+
const items = [];
|
|
3039
|
+
if (state.integrations.preCommitHook) items.push("pre-commit");
|
|
3040
|
+
if (state.integrations.typecheckHook) items.push("typecheck");
|
|
3041
|
+
if (state.integrations.lintHook) items.push("lint");
|
|
3042
|
+
if (state.integrations.claudeCodeHook) items.push("Claude");
|
|
3043
|
+
if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
|
|
3044
|
+
if (state.integrations.githubAction) items.push("CI");
|
|
3045
|
+
return items.length > 0 ? items.join(" \xB7 ") : "none selected";
|
|
3046
|
+
}
|
|
3047
|
+
function packageOverridesHint(config) {
|
|
3048
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3049
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3050
|
+
const customized = editable.filter(
|
|
3051
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3052
|
+
).length;
|
|
3053
|
+
return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
|
|
3054
|
+
}
|
|
3055
|
+
function boundariesHint(config, state) {
|
|
3056
|
+
if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
|
|
3057
|
+
const deny = config.boundaries?.deny;
|
|
3058
|
+
if (!deny) return "enabled";
|
|
3059
|
+
const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
|
|
3060
|
+
const pkgCount = Object.keys(deny).length;
|
|
3061
|
+
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3062
|
+
}
|
|
3063
|
+
function statusIcon(status) {
|
|
3064
|
+
if (status === "ok") return "\u2713";
|
|
3065
|
+
if (status === "needs-input") return "?";
|
|
3066
|
+
return "~";
|
|
3067
|
+
}
|
|
3068
|
+
function buildMainMenuOptions(config, scanResult, state) {
|
|
3069
|
+
const namingStatus = fileNamingStatus(config);
|
|
3070
|
+
const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
|
|
3071
|
+
const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
|
|
3072
|
+
const options = [
|
|
3073
|
+
{
|
|
3074
|
+
value: "fileLimits",
|
|
3075
|
+
label: `${statusIcon("ok")} Max file size`,
|
|
3076
|
+
hint: fileLimitsHint(config)
|
|
3077
|
+
},
|
|
3078
|
+
{
|
|
3079
|
+
value: "fileNaming",
|
|
3080
|
+
label: `${statusIcon(namingStatus)} File naming`,
|
|
3081
|
+
hint: fileNamingHint(config, scanResult)
|
|
3082
|
+
},
|
|
3083
|
+
{
|
|
3084
|
+
value: "missingTests",
|
|
3085
|
+
label: `${statusIcon(missingTestsStatus)} Missing tests`,
|
|
3086
|
+
hint: missingTestsHint(config)
|
|
3087
|
+
},
|
|
3088
|
+
{
|
|
3089
|
+
value: "coverage",
|
|
3090
|
+
label: `${statusIcon(coverageStatus)} Coverage`,
|
|
3091
|
+
hint: coverageHint(config, state.hasTestRunner)
|
|
3092
|
+
},
|
|
3093
|
+
{ value: "advancedNaming", label: " Advanced naming", hint: advancedNamingHint(config) }
|
|
3094
|
+
];
|
|
3095
|
+
if (config.packages.length > 1) {
|
|
3096
|
+
const bIcon = state.visited.boundaries && config.rules.enforceBoundaries ? statusIcon("ok") : " ";
|
|
3097
|
+
options.push(
|
|
3098
|
+
{
|
|
3099
|
+
value: "packageOverrides",
|
|
3100
|
+
label: " Per-package overrides",
|
|
3101
|
+
hint: packageOverridesHint(config)
|
|
3102
|
+
},
|
|
3103
|
+
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
const iIcon = state.visited.integrations ? statusIcon("ok") : " ";
|
|
3107
|
+
options.push(
|
|
3108
|
+
{ value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
|
|
3109
|
+
{ value: "reset", label: " Reset all to defaults" },
|
|
3110
|
+
{ value: "review", label: " Review scan details" },
|
|
3111
|
+
{ value: "done", label: " Done \u2014 write config" }
|
|
3112
|
+
);
|
|
3113
|
+
return options;
|
|
3114
|
+
}
|
|
2829
3115
|
|
|
2830
|
-
// src/utils/
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
3116
|
+
// src/utils/prompt-main-menu.ts
|
|
3117
|
+
async function handleAdvancedNaming(config) {
|
|
3118
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3119
|
+
const state = {
|
|
3120
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3121
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3122
|
+
testCoverage: config.rules.testCoverage,
|
|
3123
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3124
|
+
enforceNaming: config.rules.enforceNaming,
|
|
3125
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3126
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3127
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3128
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3129
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3130
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3131
|
+
};
|
|
3132
|
+
await promptNamingMenu(state);
|
|
3133
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3134
|
+
config.rules.enforceNaming = state.enforceNaming;
|
|
3135
|
+
if (state.fileNamingValue) {
|
|
3136
|
+
rootPkg.conventions.fileNaming = state.fileNamingValue;
|
|
3137
|
+
} else {
|
|
3138
|
+
delete rootPkg.conventions.fileNaming;
|
|
2846
3139
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3140
|
+
rootPkg.conventions.componentNaming = state.componentNaming || void 0;
|
|
3141
|
+
rootPkg.conventions.hookNaming = state.hookNaming || void 0;
|
|
3142
|
+
rootPkg.conventions.importAlias = state.importAlias || void 0;
|
|
3143
|
+
}
|
|
3144
|
+
async function promptMainMenu(config, scanResult, opts) {
|
|
3145
|
+
const originalConfig = structuredClone(config);
|
|
3146
|
+
const state = {
|
|
3147
|
+
visited: { integrations: false, boundaries: false },
|
|
3148
|
+
deferredInstalls: [],
|
|
3149
|
+
hasTestRunner: opts.hasTestRunner,
|
|
3150
|
+
hookManager: opts.hookManager
|
|
3151
|
+
};
|
|
3152
|
+
while (true) {
|
|
3153
|
+
const options = buildMainMenuOptions(config, scanResult, state);
|
|
3154
|
+
const choice = await clack10.select({ message: "Configure viberails", options });
|
|
3155
|
+
assertNotCancelled(choice);
|
|
3156
|
+
if (choice === "done") {
|
|
3157
|
+
if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
|
|
3158
|
+
config.rules.enforceNaming = false;
|
|
3159
|
+
}
|
|
3160
|
+
break;
|
|
2858
3161
|
}
|
|
2859
|
-
|
|
3162
|
+
if (choice === "fileLimits") {
|
|
3163
|
+
const s = {
|
|
3164
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3165
|
+
maxTestFileLines: config.rules.maxTestFileLines
|
|
3166
|
+
};
|
|
3167
|
+
await promptFileLimitsMenu(s);
|
|
3168
|
+
config.rules.maxFileLines = s.maxFileLines;
|
|
3169
|
+
config.rules.maxTestFileLines = s.maxTestFileLines;
|
|
3170
|
+
}
|
|
3171
|
+
if (choice === "fileNaming") await handleFileNaming(config, scanResult);
|
|
3172
|
+
if (choice === "missingTests") await handleMissingTests(config);
|
|
3173
|
+
if (choice === "coverage") await handleCoverage(config, state, opts);
|
|
3174
|
+
if (choice === "advancedNaming") await handleAdvancedNaming(config);
|
|
3175
|
+
if (choice === "packageOverrides") await handlePackageOverrides(config);
|
|
3176
|
+
if (choice === "boundaries") await handleBoundaries(config, state, opts);
|
|
3177
|
+
if (choice === "integrations") await handleIntegrations(state, opts);
|
|
3178
|
+
if (choice === "review") clack10.note(formatScanResultsText(scanResult), "Scan details");
|
|
3179
|
+
if (choice === "reset") {
|
|
3180
|
+
Object.assign(config, structuredClone(originalConfig));
|
|
3181
|
+
state.deferredInstalls = [];
|
|
3182
|
+
state.visited = { integrations: false, boundaries: false };
|
|
3183
|
+
state.integrations = void 0;
|
|
3184
|
+
clack10.log.info("Reset all settings to scan-detected defaults.");
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return state;
|
|
2860
3188
|
}
|
|
2861
|
-
function
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
const
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
3189
|
+
async function handleFileNaming(config, scanResult) {
|
|
3190
|
+
const isMonorepo = config.packages.length > 1;
|
|
3191
|
+
if (isMonorepo) {
|
|
3192
|
+
const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
|
|
3193
|
+
path: p.relativePath,
|
|
3194
|
+
naming: p.conventions.fileNaming
|
|
3195
|
+
}));
|
|
3196
|
+
if (pkgData.length > 0) {
|
|
3197
|
+
const lines = pkgData.map(
|
|
3198
|
+
(p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
|
|
3199
|
+
);
|
|
3200
|
+
clack10.note(lines.join("\n"), "Per-package file naming detected");
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
|
|
3204
|
+
if (isMonorepo) {
|
|
3205
|
+
const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
|
|
3206
|
+
const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
|
|
3207
|
+
return { value: opt.value, label: opt.label, hint };
|
|
2868
3208
|
}
|
|
3209
|
+
return { value: opt.value, label: opt.label };
|
|
3210
|
+
});
|
|
3211
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3212
|
+
const selected = await clack10.select({
|
|
3213
|
+
message: isMonorepo ? "Default file naming convention" : "File naming convention",
|
|
3214
|
+
options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
|
|
3215
|
+
initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
|
|
3216
|
+
});
|
|
3217
|
+
assertNotCancelled(selected);
|
|
3218
|
+
if (selected === SENTINEL_SKIP) {
|
|
3219
|
+
config.rules.enforceNaming = false;
|
|
3220
|
+
if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
|
|
3221
|
+
} else {
|
|
3222
|
+
config.rules.enforceNaming = true;
|
|
3223
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3224
|
+
rootPkg.conventions.fileNaming = selected;
|
|
2869
3225
|
}
|
|
2870
3226
|
}
|
|
2871
|
-
async function
|
|
2872
|
-
const
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3227
|
+
async function handleMissingTests(config) {
|
|
3228
|
+
const result = await clack10.confirm({
|
|
3229
|
+
message: "Require every source file to have a test file?",
|
|
3230
|
+
initialValue: config.rules.enforceMissingTests
|
|
3231
|
+
});
|
|
3232
|
+
assertNotCancelled(result);
|
|
3233
|
+
config.rules.enforceMissingTests = result;
|
|
3234
|
+
}
|
|
3235
|
+
async function handleCoverage(config, state, opts) {
|
|
3236
|
+
if (!opts.hasTestRunner) {
|
|
3237
|
+
clack10.log.info("Coverage checks are inactive \u2014 no test runner detected.");
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
const planned = planCoverageInstall(opts.coveragePrereqs);
|
|
3241
|
+
if (planned) {
|
|
3242
|
+
const choice = await clack10.select({
|
|
3243
|
+
message: `${planned.label} is not installed. Needed for coverage checks.`,
|
|
2887
3244
|
options: [
|
|
2888
3245
|
{
|
|
2889
3246
|
value: "install",
|
|
2890
|
-
label: "Install
|
|
2891
|
-
hint:
|
|
2892
|
-
},
|
|
2893
|
-
{
|
|
2894
|
-
value: "disable",
|
|
2895
|
-
label: "Disable coverage checks",
|
|
2896
|
-
hint: "missing-test checks still stay active"
|
|
3247
|
+
label: "Install (after final confirmation)",
|
|
3248
|
+
hint: planned.command
|
|
2897
3249
|
},
|
|
3250
|
+
{ value: "disable", label: "Disable coverage checks" },
|
|
2898
3251
|
{
|
|
2899
3252
|
value: "skip",
|
|
2900
3253
|
label: "Skip for now",
|
|
2901
|
-
hint: `install later: ${
|
|
3254
|
+
hint: `install later: ${planned.command}`
|
|
2902
3255
|
}
|
|
2903
3256
|
]
|
|
2904
3257
|
});
|
|
2905
3258
|
assertNotCancelled(choice);
|
|
3259
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
|
|
2906
3260
|
if (choice === "install") {
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
is.stop(`Installed ${m.label}`);
|
|
2912
|
-
} else {
|
|
2913
|
-
is.stop(`Failed to install ${m.label}`);
|
|
2914
|
-
clack7.log.warn(
|
|
2915
|
-
`Install manually: ${m.installCommand}
|
|
2916
|
-
Coverage percentage checks will not work until the dependency is installed.`
|
|
2917
|
-
);
|
|
2918
|
-
}
|
|
3261
|
+
planned.onFailure = () => {
|
|
3262
|
+
config.rules.testCoverage = 0;
|
|
3263
|
+
};
|
|
3264
|
+
state.deferredInstalls.push(planned);
|
|
2919
3265
|
} else if (choice === "disable") {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
} else {
|
|
2923
|
-
clack7.log.info(
|
|
2924
|
-
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
2925
|
-
Install later: ${m.installCommand}`
|
|
2926
|
-
);
|
|
3266
|
+
config.rules.testCoverage = 0;
|
|
3267
|
+
return;
|
|
2927
3268
|
}
|
|
2928
3269
|
}
|
|
2929
|
-
|
|
3270
|
+
const result = await clack10.text({
|
|
3271
|
+
message: "Test coverage target (0 = disable)?",
|
|
3272
|
+
initialValue: String(config.rules.testCoverage),
|
|
3273
|
+
validate: (v) => {
|
|
3274
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
3275
|
+
const n = Number.parseInt(v, 10);
|
|
3276
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
assertNotCancelled(result);
|
|
3280
|
+
config.rules.testCoverage = Number.parseInt(result, 10);
|
|
2930
3281
|
}
|
|
2931
|
-
function
|
|
3282
|
+
async function handlePackageOverrides(config) {
|
|
3283
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3284
|
+
config.packages = await promptPackageOverrides(config.packages, {
|
|
3285
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3286
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3287
|
+
testCoverage: config.rules.testCoverage,
|
|
3288
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3289
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3290
|
+
});
|
|
3291
|
+
normalizePackageOverrides(config.packages);
|
|
3292
|
+
}
|
|
3293
|
+
async function handleBoundaries(config, state, opts) {
|
|
3294
|
+
const shouldInfer = await clack10.confirm({
|
|
3295
|
+
message: "Infer boundary rules from current import patterns?",
|
|
3296
|
+
initialValue: false
|
|
3297
|
+
});
|
|
3298
|
+
assertNotCancelled(shouldInfer);
|
|
3299
|
+
state.visited.boundaries = true;
|
|
3300
|
+
if (!shouldInfer) {
|
|
3301
|
+
config.rules.enforceBoundaries = false;
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
const bs = clack10.spinner();
|
|
3305
|
+
bs.start("Building import graph...");
|
|
2932
3306
|
try {
|
|
2933
|
-
const
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3307
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3308
|
+
const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
|
|
3309
|
+
const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
|
|
3310
|
+
const inferred = inferBoundaries(graph);
|
|
3311
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3312
|
+
if (denyCount > 0) {
|
|
3313
|
+
config.boundaries = inferred;
|
|
3314
|
+
config.rules.enforceBoundaries = true;
|
|
3315
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3316
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3317
|
+
} else {
|
|
3318
|
+
bs.stop("No boundary rules inferred");
|
|
3319
|
+
}
|
|
3320
|
+
} catch (err) {
|
|
3321
|
+
bs.stop("Failed to build import graph");
|
|
3322
|
+
clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
|
|
2938
3323
|
}
|
|
2939
3324
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3325
|
+
async function handleIntegrations(state, opts) {
|
|
3326
|
+
const result = await promptIntegrationsDeferred(
|
|
3327
|
+
state.hookManager,
|
|
3328
|
+
opts.tools,
|
|
3329
|
+
opts.tools.packageManager,
|
|
3330
|
+
opts.tools.isWorkspace,
|
|
3331
|
+
opts.projectRoot
|
|
3332
|
+
);
|
|
3333
|
+
state.visited.integrations = true;
|
|
3334
|
+
state.integrations = result.choice;
|
|
3335
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
|
|
3336
|
+
if (result.lefthookInstall) {
|
|
3337
|
+
state.deferredInstalls.push(result.lefthookInstall);
|
|
2951
3338
|
}
|
|
2952
|
-
return filtered;
|
|
2953
3339
|
}
|
|
2954
3340
|
|
|
2955
3341
|
// src/utils/update-gitignore.ts
|
|
2956
|
-
var
|
|
2957
|
-
var
|
|
3342
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
3343
|
+
var path16 = __toESM(require("path"), 1);
|
|
2958
3344
|
function updateGitignore(projectRoot) {
|
|
2959
|
-
const gitignorePath =
|
|
3345
|
+
const gitignorePath = path16.join(projectRoot, ".gitignore");
|
|
2960
3346
|
let content = "";
|
|
2961
|
-
if (
|
|
2962
|
-
content =
|
|
3347
|
+
if (fs16.existsSync(gitignorePath)) {
|
|
3348
|
+
content = fs16.readFileSync(gitignorePath, "utf-8");
|
|
2963
3349
|
}
|
|
2964
3350
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
2965
3351
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
2966
3352
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
2967
3353
|
`;
|
|
2968
|
-
|
|
3354
|
+
fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
2969
3355
|
}
|
|
2970
3356
|
}
|
|
2971
3357
|
|
|
2972
3358
|
// src/commands/init-hooks.ts
|
|
2973
|
-
var
|
|
2974
|
-
var
|
|
3359
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
3360
|
+
var path18 = __toESM(require("path"), 1);
|
|
2975
3361
|
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
2976
3362
|
var import_yaml = require("yaml");
|
|
2977
3363
|
|
|
2978
3364
|
// src/commands/resolve-typecheck.ts
|
|
2979
|
-
var
|
|
2980
|
-
var
|
|
3365
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
3366
|
+
var path17 = __toESM(require("path"), 1);
|
|
2981
3367
|
function hasTurboTask(projectRoot, taskName) {
|
|
2982
|
-
const turboPath =
|
|
2983
|
-
if (!
|
|
3368
|
+
const turboPath = path17.join(projectRoot, "turbo.json");
|
|
3369
|
+
if (!fs17.existsSync(turboPath)) return false;
|
|
2984
3370
|
try {
|
|
2985
|
-
const turbo = JSON.parse(
|
|
3371
|
+
const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
|
|
2986
3372
|
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
2987
3373
|
return taskName in tasks;
|
|
2988
3374
|
} catch {
|
|
@@ -2993,10 +3379,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
2993
3379
|
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
2994
3380
|
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
2995
3381
|
}
|
|
2996
|
-
const pkgJsonPath =
|
|
2997
|
-
if (
|
|
3382
|
+
const pkgJsonPath = path17.join(projectRoot, "package.json");
|
|
3383
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
2998
3384
|
try {
|
|
2999
|
-
const pkg = JSON.parse(
|
|
3385
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
3000
3386
|
if (pkg.scripts?.typecheck) {
|
|
3001
3387
|
const pm = packageManager ?? "npm";
|
|
3002
3388
|
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
@@ -3004,7 +3390,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3004
3390
|
} catch {
|
|
3005
3391
|
}
|
|
3006
3392
|
}
|
|
3007
|
-
if (
|
|
3393
|
+
if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
|
|
3008
3394
|
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
3009
3395
|
}
|
|
3010
3396
|
return {
|
|
@@ -3014,23 +3400,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3014
3400
|
|
|
3015
3401
|
// src/commands/init-hooks.ts
|
|
3016
3402
|
function setupPreCommitHook(projectRoot) {
|
|
3017
|
-
const lefthookPath =
|
|
3018
|
-
if (
|
|
3403
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3404
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
3019
3405
|
addLefthookPreCommit(lefthookPath);
|
|
3020
3406
|
console.log(` ${import_chalk10.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
3021
3407
|
return "lefthook.yml";
|
|
3022
3408
|
}
|
|
3023
|
-
const huskyDir =
|
|
3024
|
-
if (
|
|
3409
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3410
|
+
if (fs18.existsSync(huskyDir)) {
|
|
3025
3411
|
writeHuskyPreCommit(huskyDir);
|
|
3026
3412
|
console.log(` ${import_chalk10.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
3027
3413
|
return ".husky/pre-commit";
|
|
3028
3414
|
}
|
|
3029
|
-
const gitDir =
|
|
3030
|
-
if (
|
|
3031
|
-
const hooksDir =
|
|
3032
|
-
if (!
|
|
3033
|
-
|
|
3415
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3416
|
+
if (fs18.existsSync(gitDir)) {
|
|
3417
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3418
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
3419
|
+
fs18.mkdirSync(hooksDir, { recursive: true });
|
|
3034
3420
|
}
|
|
3035
3421
|
writeGitHookPreCommit(hooksDir);
|
|
3036
3422
|
console.log(` ${import_chalk10.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -3039,11 +3425,11 @@ function setupPreCommitHook(projectRoot) {
|
|
|
3039
3425
|
return void 0;
|
|
3040
3426
|
}
|
|
3041
3427
|
function writeGitHookPreCommit(hooksDir) {
|
|
3042
|
-
const hookPath =
|
|
3043
|
-
if (
|
|
3044
|
-
const existing =
|
|
3428
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3429
|
+
if (fs18.existsSync(hookPath)) {
|
|
3430
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3045
3431
|
if (existing.includes("viberails")) return;
|
|
3046
|
-
|
|
3432
|
+
fs18.writeFileSync(
|
|
3047
3433
|
hookPath,
|
|
3048
3434
|
`${existing.trimEnd()}
|
|
3049
3435
|
|
|
@@ -3060,10 +3446,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
|
|
|
3060
3446
|
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
3061
3447
|
""
|
|
3062
3448
|
].join("\n");
|
|
3063
|
-
|
|
3449
|
+
fs18.writeFileSync(hookPath, script, { mode: 493 });
|
|
3064
3450
|
}
|
|
3065
3451
|
function addLefthookPreCommit(lefthookPath) {
|
|
3066
|
-
const content =
|
|
3452
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
3067
3453
|
if (content.includes("viberails")) return;
|
|
3068
3454
|
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
3069
3455
|
if (!doc["pre-commit"]) {
|
|
@@ -3075,23 +3461,23 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
3075
3461
|
doc["pre-commit"].commands.viberails = {
|
|
3076
3462
|
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
3077
3463
|
};
|
|
3078
|
-
|
|
3464
|
+
fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
3079
3465
|
}
|
|
3080
3466
|
function detectHookManager(projectRoot) {
|
|
3081
|
-
if (
|
|
3082
|
-
if (
|
|
3467
|
+
if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
3468
|
+
if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
|
|
3083
3469
|
return void 0;
|
|
3084
3470
|
}
|
|
3085
3471
|
function setupClaudeCodeHook(projectRoot) {
|
|
3086
|
-
const claudeDir =
|
|
3087
|
-
if (!
|
|
3088
|
-
|
|
3472
|
+
const claudeDir = path18.join(projectRoot, ".claude");
|
|
3473
|
+
if (!fs18.existsSync(claudeDir)) {
|
|
3474
|
+
fs18.mkdirSync(claudeDir, { recursive: true });
|
|
3089
3475
|
}
|
|
3090
|
-
const settingsPath =
|
|
3476
|
+
const settingsPath = path18.join(claudeDir, "settings.json");
|
|
3091
3477
|
let settings = {};
|
|
3092
|
-
if (
|
|
3478
|
+
if (fs18.existsSync(settingsPath)) {
|
|
3093
3479
|
try {
|
|
3094
|
-
settings = JSON.parse(
|
|
3480
|
+
settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
|
|
3095
3481
|
} catch {
|
|
3096
3482
|
console.warn(
|
|
3097
3483
|
` ${import_chalk10.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
@@ -3117,30 +3503,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3117
3503
|
}
|
|
3118
3504
|
];
|
|
3119
3505
|
settings.hooks = hooks;
|
|
3120
|
-
|
|
3506
|
+
fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3121
3507
|
`);
|
|
3122
3508
|
console.log(` ${import_chalk10.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
3123
3509
|
}
|
|
3124
3510
|
function setupClaudeMdReference(projectRoot) {
|
|
3125
|
-
const claudeMdPath =
|
|
3511
|
+
const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
|
|
3126
3512
|
let content = "";
|
|
3127
|
-
if (
|
|
3128
|
-
content =
|
|
3513
|
+
if (fs18.existsSync(claudeMdPath)) {
|
|
3514
|
+
content = fs18.readFileSync(claudeMdPath, "utf-8");
|
|
3129
3515
|
}
|
|
3130
3516
|
if (content.includes("@.viberails/context.md")) return;
|
|
3131
3517
|
const ref = "\n@.viberails/context.md\n";
|
|
3132
3518
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
3133
|
-
|
|
3519
|
+
fs18.writeFileSync(claudeMdPath, prefix + ref);
|
|
3134
3520
|
console.log(` ${import_chalk10.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
3135
3521
|
}
|
|
3136
3522
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
3137
|
-
const workflowDir =
|
|
3138
|
-
const workflowPath =
|
|
3139
|
-
if (
|
|
3140
|
-
const existing =
|
|
3523
|
+
const workflowDir = path18.join(projectRoot, ".github", "workflows");
|
|
3524
|
+
const workflowPath = path18.join(workflowDir, "viberails.yml");
|
|
3525
|
+
if (fs18.existsSync(workflowPath)) {
|
|
3526
|
+
const existing = fs18.readFileSync(workflowPath, "utf-8");
|
|
3141
3527
|
if (existing.includes("viberails")) return void 0;
|
|
3142
3528
|
}
|
|
3143
|
-
|
|
3529
|
+
fs18.mkdirSync(workflowDir, { recursive: true });
|
|
3144
3530
|
const pm = packageManager || "npm";
|
|
3145
3531
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
3146
3532
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -3194,74 +3580,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
3194
3580
|
""
|
|
3195
3581
|
);
|
|
3196
3582
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
3197
|
-
|
|
3583
|
+
fs18.writeFileSync(workflowPath, content);
|
|
3198
3584
|
return ".github/workflows/viberails.yml";
|
|
3199
3585
|
}
|
|
3200
3586
|
function writeHuskyPreCommit(huskyDir) {
|
|
3201
|
-
const hookPath =
|
|
3587
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
3202
3588
|
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3203
|
-
if (
|
|
3204
|
-
const existing =
|
|
3589
|
+
if (fs18.existsSync(hookPath)) {
|
|
3590
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3205
3591
|
if (!existing.includes("viberails")) {
|
|
3206
|
-
|
|
3592
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3207
3593
|
${cmd}
|
|
3208
3594
|
`);
|
|
3209
3595
|
}
|
|
3210
3596
|
return;
|
|
3211
3597
|
}
|
|
3212
|
-
|
|
3598
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
3213
3599
|
${cmd}
|
|
3214
3600
|
`, { mode: 493 });
|
|
3215
3601
|
}
|
|
3216
3602
|
|
|
3217
3603
|
// src/commands/init-hooks-extra.ts
|
|
3218
|
-
var
|
|
3219
|
-
var
|
|
3604
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
3605
|
+
var path19 = __toESM(require("path"), 1);
|
|
3220
3606
|
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
3221
3607
|
var import_yaml2 = require("yaml");
|
|
3222
3608
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3223
|
-
const lefthookPath =
|
|
3224
|
-
if (
|
|
3225
|
-
const content =
|
|
3609
|
+
const lefthookPath = path19.join(projectRoot, "lefthook.yml");
|
|
3610
|
+
if (fs19.existsSync(lefthookPath)) {
|
|
3611
|
+
const content = fs19.readFileSync(lefthookPath, "utf-8");
|
|
3226
3612
|
if (content.includes(marker)) return void 0;
|
|
3227
3613
|
const doc = (0, import_yaml2.parse)(content) ?? {};
|
|
3228
3614
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
3229
3615
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
3230
3616
|
doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
|
|
3231
|
-
|
|
3617
|
+
fs19.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
|
|
3232
3618
|
return "lefthook.yml";
|
|
3233
3619
|
}
|
|
3234
|
-
const huskyDir =
|
|
3235
|
-
if (
|
|
3236
|
-
const hookPath =
|
|
3237
|
-
if (
|
|
3238
|
-
const existing =
|
|
3620
|
+
const huskyDir = path19.join(projectRoot, ".husky");
|
|
3621
|
+
if (fs19.existsSync(huskyDir)) {
|
|
3622
|
+
const hookPath = path19.join(huskyDir, "pre-commit");
|
|
3623
|
+
if (fs19.existsSync(hookPath)) {
|
|
3624
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3239
3625
|
if (existing.includes(marker)) return void 0;
|
|
3240
|
-
|
|
3626
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3241
3627
|
${command}
|
|
3242
3628
|
`);
|
|
3243
3629
|
} else {
|
|
3244
|
-
|
|
3630
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3245
3631
|
${command}
|
|
3246
3632
|
`, { mode: 493 });
|
|
3247
3633
|
}
|
|
3248
3634
|
return ".husky/pre-commit";
|
|
3249
3635
|
}
|
|
3250
|
-
const gitDir =
|
|
3251
|
-
if (
|
|
3252
|
-
const hooksDir =
|
|
3253
|
-
if (!
|
|
3254
|
-
const hookPath =
|
|
3255
|
-
if (
|
|
3256
|
-
const existing =
|
|
3636
|
+
const gitDir = path19.join(projectRoot, ".git");
|
|
3637
|
+
if (fs19.existsSync(gitDir)) {
|
|
3638
|
+
const hooksDir = path19.join(gitDir, "hooks");
|
|
3639
|
+
if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
|
|
3640
|
+
const hookPath = path19.join(hooksDir, "pre-commit");
|
|
3641
|
+
if (fs19.existsSync(hookPath)) {
|
|
3642
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3257
3643
|
if (existing.includes(marker)) return void 0;
|
|
3258
|
-
|
|
3644
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3259
3645
|
|
|
3260
3646
|
# ${name}
|
|
3261
3647
|
${command}
|
|
3262
3648
|
`);
|
|
3263
3649
|
} else {
|
|
3264
|
-
|
|
3650
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3265
3651
|
# Generated by viberails
|
|
3266
3652
|
|
|
3267
3653
|
# ${name}
|
|
@@ -3287,7 +3673,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
|
|
|
3287
3673
|
return target;
|
|
3288
3674
|
}
|
|
3289
3675
|
function setupLintHook(projectRoot, linter) {
|
|
3290
|
-
const isLefthook =
|
|
3676
|
+
const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
|
|
3291
3677
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
3292
3678
|
let command;
|
|
3293
3679
|
let lefthookExtra;
|
|
@@ -3311,6 +3697,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3311
3697
|
const created = [];
|
|
3312
3698
|
if (integrations.preCommitHook) {
|
|
3313
3699
|
const t = setupPreCommitHook(projectRoot);
|
|
3700
|
+
if (t && opts.lefthookExpected && !t.includes("lefthook")) {
|
|
3701
|
+
console.log(` ${import_chalk11.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
|
|
3702
|
+
}
|
|
3314
3703
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
3315
3704
|
}
|
|
3316
3705
|
if (integrations.typecheckHook) {
|
|
@@ -3339,34 +3728,34 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3339
3728
|
return created;
|
|
3340
3729
|
}
|
|
3341
3730
|
|
|
3342
|
-
// src/commands/init.ts
|
|
3343
|
-
var
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
}
|
|
3354
|
-
const
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3731
|
+
// src/commands/init-non-interactive.ts
|
|
3732
|
+
var fs20 = __toESM(require("fs"), 1);
|
|
3733
|
+
var path20 = __toESM(require("path"), 1);
|
|
3734
|
+
var clack11 = __toESM(require("@clack/prompts"), 1);
|
|
3735
|
+
var import_config8 = require("@viberails/config");
|
|
3736
|
+
var import_scanner2 = require("@viberails/scanner");
|
|
3737
|
+
var import_chalk12 = __toESM(require("chalk"), 1);
|
|
3738
|
+
|
|
3739
|
+
// src/utils/filter-confidence.ts
|
|
3740
|
+
function filterHighConfidence(conventions, meta) {
|
|
3741
|
+
if (!meta) return conventions;
|
|
3742
|
+
const filtered = {};
|
|
3743
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
3744
|
+
if (value === void 0) continue;
|
|
3745
|
+
const convMeta = meta[key];
|
|
3746
|
+
if (!convMeta || convMeta.confidence === "high") {
|
|
3747
|
+
filtered[key] = value;
|
|
3358
3748
|
}
|
|
3359
|
-
console.log(
|
|
3360
|
-
`${import_chalk12.default.yellow("!")} viberails is already initialized.
|
|
3361
|
-
Run ${import_chalk12.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk12.default.cyan("viberails sync")} to update generated files, or ${import_chalk12.default.cyan("viberails init --force")} to replace it.`
|
|
3362
|
-
);
|
|
3363
|
-
return;
|
|
3364
3749
|
}
|
|
3365
|
-
|
|
3366
|
-
|
|
3750
|
+
return filtered;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// src/commands/init-non-interactive.ts
|
|
3754
|
+
function getExemptedPackages(config) {
|
|
3755
|
+
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3367
3756
|
}
|
|
3368
3757
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3369
|
-
const s =
|
|
3758
|
+
const s = clack11.spinner();
|
|
3370
3759
|
s.start("Scanning project...");
|
|
3371
3760
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
3372
3761
|
const config = (0, import_config8.generateConfig)(scanResult);
|
|
@@ -3385,7 +3774,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3385
3774
|
);
|
|
3386
3775
|
}
|
|
3387
3776
|
if (config.packages.length > 1) {
|
|
3388
|
-
const bs =
|
|
3777
|
+
const bs = clack11.spinner();
|
|
3389
3778
|
bs.start("Building import graph...");
|
|
3390
3779
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3391
3780
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3401,7 +3790,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3401
3790
|
}
|
|
3402
3791
|
}
|
|
3403
3792
|
const compacted = (0, import_config8.compactConfig)(config);
|
|
3404
|
-
|
|
3793
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3405
3794
|
`);
|
|
3406
3795
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3407
3796
|
updateGitignore(projectRoot);
|
|
@@ -3420,7 +3809,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3420
3809
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3421
3810
|
const ok = import_chalk12.default.green("\u2713");
|
|
3422
3811
|
const created = [
|
|
3423
|
-
`${ok} ${
|
|
3812
|
+
`${ok} ${path20.basename(configPath)}`,
|
|
3424
3813
|
`${ok} .viberails/context.md`,
|
|
3425
3814
|
`${ok} .viberails/scan-result.json`,
|
|
3426
3815
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
@@ -3434,13 +3823,36 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3434
3823
|
Created:
|
|
3435
3824
|
${created.map((f) => ` ${f}`).join("\n")}`);
|
|
3436
3825
|
}
|
|
3826
|
+
|
|
3827
|
+
// src/commands/init.ts
|
|
3828
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
3829
|
+
async function initCommand(options, cwd) {
|
|
3830
|
+
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
3831
|
+
if (!projectRoot) {
|
|
3832
|
+
throw new Error(
|
|
3833
|
+
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE5);
|
|
3837
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3838
|
+
if (!options.yes) {
|
|
3839
|
+
return initInteractive(projectRoot, configPath, options);
|
|
3840
|
+
}
|
|
3841
|
+
console.log(
|
|
3842
|
+
`${import_chalk13.default.yellow("!")} viberails is already initialized.
|
|
3843
|
+
Run ${import_chalk13.default.cyan("viberails")} to review or edit the existing setup, ${import_chalk13.default.cyan("viberails sync")} to update generated files, or ${import_chalk13.default.cyan("viberails init --force")} to replace it.`
|
|
3844
|
+
);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
if (options.yes) return initNonInteractive(projectRoot, configPath);
|
|
3848
|
+
await initInteractive(projectRoot, configPath, options);
|
|
3849
|
+
}
|
|
3437
3850
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
const action = await promptExistingConfigAction(path19.basename(configPath));
|
|
3851
|
+
clack12.intro("viberails");
|
|
3852
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3853
|
+
const action = await promptExistingConfigAction(path21.basename(configPath));
|
|
3442
3854
|
if (action === "cancel") {
|
|
3443
|
-
|
|
3855
|
+
clack12.outro("Aborted. No files were written.");
|
|
3444
3856
|
return;
|
|
3445
3857
|
}
|
|
3446
3858
|
if (action === "edit") {
|
|
@@ -3449,136 +3861,93 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3449
3861
|
}
|
|
3450
3862
|
options.force = true;
|
|
3451
3863
|
}
|
|
3452
|
-
if (
|
|
3864
|
+
if (fs21.existsSync(configPath) && options.force) {
|
|
3453
3865
|
const replace = await confirmDangerous(
|
|
3454
|
-
`${
|
|
3866
|
+
`${path21.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3455
3867
|
);
|
|
3456
3868
|
if (!replace) {
|
|
3457
|
-
|
|
3869
|
+
clack12.outro("Aborted. No files were written.");
|
|
3458
3870
|
return;
|
|
3459
3871
|
}
|
|
3460
3872
|
}
|
|
3461
|
-
const s =
|
|
3873
|
+
const s = clack12.spinner();
|
|
3462
3874
|
s.start("Scanning project...");
|
|
3463
|
-
const scanResult = await (0,
|
|
3464
|
-
const config = (0,
|
|
3875
|
+
const scanResult = await (0, import_scanner3.scan)(projectRoot);
|
|
3876
|
+
const config = (0, import_config9.generateConfig)(scanResult);
|
|
3465
3877
|
s.stop("Scan complete");
|
|
3466
3878
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3467
|
-
|
|
3879
|
+
clack12.log.warn(
|
|
3468
3880
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3469
3881
|
);
|
|
3470
3882
|
}
|
|
3471
|
-
const
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
const nextDecision = await promptInitDecision();
|
|
3476
|
-
if (nextDecision === "review") {
|
|
3477
|
-
clack8.note(formatScanResultsText(scanResult), "Detected details");
|
|
3478
|
-
continue;
|
|
3479
|
-
}
|
|
3480
|
-
decision = nextDecision;
|
|
3481
|
-
break;
|
|
3482
|
-
}
|
|
3483
|
-
if (decision === "customize") {
|
|
3484
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3485
|
-
const overrides = await promptRuleMenu({
|
|
3486
|
-
maxFileLines: config.rules.maxFileLines,
|
|
3487
|
-
testCoverage: config.rules.testCoverage,
|
|
3488
|
-
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3489
|
-
enforceNaming: config.rules.enforceNaming,
|
|
3490
|
-
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3491
|
-
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
3492
|
-
coverageCommand: config.defaults?.coverage?.command,
|
|
3493
|
-
packageOverrides: config.packages
|
|
3494
|
-
});
|
|
3495
|
-
applyRuleOverrides(config, overrides);
|
|
3496
|
-
}
|
|
3497
|
-
if (config.packages.length > 1) {
|
|
3498
|
-
clack8.note(
|
|
3499
|
-
"Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
|
|
3500
|
-
"Boundaries"
|
|
3883
|
+
const hasTestRunner = !!scanResult.stack.testRunner;
|
|
3884
|
+
if (!hasTestRunner) {
|
|
3885
|
+
clack12.log.info(
|
|
3886
|
+
"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."
|
|
3501
3887
|
);
|
|
3502
|
-
const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
|
|
3503
|
-
if (shouldInfer) {
|
|
3504
|
-
const bs = clack8.spinner();
|
|
3505
|
-
bs.start("Building import graph...");
|
|
3506
|
-
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3507
|
-
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
3508
|
-
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
3509
|
-
const inferred = inferBoundaries(graph);
|
|
3510
|
-
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3511
|
-
if (denyCount > 0) {
|
|
3512
|
-
config.boundaries = inferred;
|
|
3513
|
-
config.rules.enforceBoundaries = true;
|
|
3514
|
-
const pkgCount = Object.keys(inferred.deny).length;
|
|
3515
|
-
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3516
|
-
} else {
|
|
3517
|
-
bs.stop("No boundary rules inferred");
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
3888
|
}
|
|
3521
3889
|
const hookManager = detectHookManager(projectRoot);
|
|
3522
3890
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3523
|
-
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3524
|
-
if (hasMissingPrereqs) {
|
|
3525
|
-
clack8.log.info("Some dependencies are needed for full functionality.");
|
|
3526
|
-
}
|
|
3527
|
-
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3528
|
-
if (prereqResult.disableCoverage) {
|
|
3529
|
-
config.rules.testCoverage = 0;
|
|
3530
|
-
}
|
|
3531
3891
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
3532
|
-
const
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3892
|
+
const state = await promptMainMenu(config, scanResult, {
|
|
3893
|
+
hasTestRunner,
|
|
3894
|
+
hookManager,
|
|
3895
|
+
coveragePrereqs,
|
|
3896
|
+
projectRoot,
|
|
3897
|
+
tools: {
|
|
3898
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3899
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3900
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3901
|
+
isWorkspace: config.packages.length > 1
|
|
3902
|
+
}
|
|
3541
3903
|
});
|
|
3542
3904
|
const shouldWrite = await confirm3("Apply this setup?");
|
|
3543
3905
|
if (!shouldWrite) {
|
|
3544
|
-
|
|
3906
|
+
clack12.outro("Aborted. No files were written.");
|
|
3545
3907
|
return;
|
|
3546
3908
|
}
|
|
3547
|
-
|
|
3909
|
+
if (state.deferredInstalls.length > 0) {
|
|
3910
|
+
await executeDeferredInstalls(projectRoot, state.deferredInstalls);
|
|
3911
|
+
}
|
|
3912
|
+
const ws = clack12.spinner();
|
|
3548
3913
|
ws.start("Writing configuration...");
|
|
3549
|
-
const compacted = (0,
|
|
3550
|
-
|
|
3914
|
+
const compacted = (0, import_config9.compactConfig)(config);
|
|
3915
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3551
3916
|
`);
|
|
3552
3917
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3553
3918
|
updateGitignore(projectRoot);
|
|
3554
3919
|
ws.stop("Configuration written");
|
|
3555
|
-
const ok =
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3920
|
+
const ok = import_chalk13.default.green("\u2713");
|
|
3921
|
+
clack12.log.step(`${ok} ${path21.basename(configPath)}`);
|
|
3922
|
+
clack12.log.step(`${ok} .viberails/context.md`);
|
|
3923
|
+
clack12.log.step(`${ok} .viberails/scan-result.json`);
|
|
3924
|
+
if (state.visited.integrations && state.integrations) {
|
|
3925
|
+
const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
|
|
3926
|
+
setupSelectedIntegrations(projectRoot, state.integrations, {
|
|
3927
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3928
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3929
|
+
lefthookExpected
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
clack12.outro(
|
|
3564
3933
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3565
|
-
${
|
|
3934
|
+
${import_chalk13.default.dim("Tip: use")} ${import_chalk13.default.cyan("viberails check --enforce")} ${import_chalk13.default.dim("in CI to block PRs on violations.")}`
|
|
3566
3935
|
);
|
|
3567
3936
|
}
|
|
3568
3937
|
|
|
3569
3938
|
// src/commands/sync.ts
|
|
3570
|
-
var
|
|
3571
|
-
var
|
|
3572
|
-
var
|
|
3573
|
-
var
|
|
3574
|
-
var
|
|
3575
|
-
var
|
|
3939
|
+
var fs22 = __toESM(require("fs"), 1);
|
|
3940
|
+
var path22 = __toESM(require("path"), 1);
|
|
3941
|
+
var clack13 = __toESM(require("@clack/prompts"), 1);
|
|
3942
|
+
var import_config11 = require("@viberails/config");
|
|
3943
|
+
var import_scanner4 = require("@viberails/scanner");
|
|
3944
|
+
var import_chalk14 = __toESM(require("chalk"), 1);
|
|
3576
3945
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3577
3946
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3578
3947
|
function loadPreviousStats(projectRoot) {
|
|
3579
|
-
const scanResultPath =
|
|
3948
|
+
const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3580
3949
|
try {
|
|
3581
|
-
const raw =
|
|
3950
|
+
const raw = fs22.readFileSync(scanResultPath, "utf-8");
|
|
3582
3951
|
const parsed = JSON.parse(raw);
|
|
3583
3952
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3584
3953
|
return parsed.statistics;
|
|
@@ -3595,17 +3964,17 @@ async function syncCommand(options, cwd) {
|
|
|
3595
3964
|
"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"
|
|
3596
3965
|
);
|
|
3597
3966
|
}
|
|
3598
|
-
const configPath =
|
|
3599
|
-
const existing = await (0,
|
|
3967
|
+
const configPath = path22.join(projectRoot, CONFIG_FILE6);
|
|
3968
|
+
const existing = await (0, import_config11.loadConfig)(configPath);
|
|
3600
3969
|
const previousStats = loadPreviousStats(projectRoot);
|
|
3601
|
-
const s =
|
|
3970
|
+
const s = clack13.spinner();
|
|
3602
3971
|
s.start("Scanning project...");
|
|
3603
|
-
const scanResult = await (0,
|
|
3972
|
+
const scanResult = await (0, import_scanner4.scan)(projectRoot);
|
|
3604
3973
|
s.stop("Scan complete");
|
|
3605
|
-
const merged = (0,
|
|
3606
|
-
const compacted = (0,
|
|
3974
|
+
const merged = (0, import_config11.mergeConfig)(existing, scanResult);
|
|
3975
|
+
const compacted = (0, import_config11.compactConfig)(merged);
|
|
3607
3976
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
3608
|
-
const rawDisk =
|
|
3977
|
+
const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
|
|
3609
3978
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3610
3979
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
3611
3980
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -3613,19 +3982,19 @@ async function syncCommand(options, cwd) {
|
|
|
3613
3982
|
const statsDelta = previousStats ? formatStatsDelta(previousStats, scanResult.statistics) : void 0;
|
|
3614
3983
|
if (changes.length > 0 || statsDelta) {
|
|
3615
3984
|
console.log(`
|
|
3616
|
-
${
|
|
3985
|
+
${import_chalk14.default.bold("Changes:")}`);
|
|
3617
3986
|
for (const change of changes) {
|
|
3618
|
-
const icon = change.type === "removed" ?
|
|
3987
|
+
const icon = change.type === "removed" ? import_chalk14.default.red("-") : import_chalk14.default.green("+");
|
|
3619
3988
|
console.log(` ${icon} ${change.description}`);
|
|
3620
3989
|
}
|
|
3621
3990
|
if (statsDelta) {
|
|
3622
|
-
console.log(` ${
|
|
3991
|
+
console.log(` ${import_chalk14.default.dim(statsDelta)}`);
|
|
3623
3992
|
}
|
|
3624
3993
|
}
|
|
3625
3994
|
if (options?.interactive) {
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
const decision = await
|
|
3995
|
+
clack13.intro("viberails sync (interactive)");
|
|
3996
|
+
clack13.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
3997
|
+
const decision = await clack13.select({
|
|
3629
3998
|
message: "How would you like to proceed?",
|
|
3630
3999
|
options: [
|
|
3631
4000
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -3635,47 +4004,51 @@ ${import_chalk13.default.bold("Changes:")}`);
|
|
|
3635
4004
|
});
|
|
3636
4005
|
assertNotCancelled(decision);
|
|
3637
4006
|
if (decision === "cancel") {
|
|
3638
|
-
|
|
4007
|
+
clack13.outro("Sync cancelled. No files were written.");
|
|
3639
4008
|
return;
|
|
3640
4009
|
}
|
|
3641
4010
|
if (decision === "customize") {
|
|
3642
4011
|
const rootPkg = merged.packages.find((p) => p.path === ".") ?? merged.packages[0];
|
|
3643
4012
|
const overrides = await promptRuleMenu({
|
|
3644
4013
|
maxFileLines: merged.rules.maxFileLines,
|
|
4014
|
+
maxTestFileLines: merged.rules.maxTestFileLines,
|
|
3645
4015
|
testCoverage: merged.rules.testCoverage,
|
|
3646
4016
|
enforceMissingTests: merged.rules.enforceMissingTests,
|
|
3647
4017
|
enforceNaming: merged.rules.enforceNaming,
|
|
3648
4018
|
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
4019
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
4020
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
4021
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3649
4022
|
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3650
4023
|
coverageCommand: merged.defaults?.coverage?.command,
|
|
3651
4024
|
packageOverrides: merged.packages
|
|
3652
4025
|
});
|
|
3653
4026
|
applyRuleOverrides(merged, overrides);
|
|
3654
|
-
const recompacted = (0,
|
|
3655
|
-
|
|
4027
|
+
const recompacted = (0, import_config11.compactConfig)(merged);
|
|
4028
|
+
fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
3656
4029
|
`);
|
|
3657
4030
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3658
|
-
|
|
3659
|
-
|
|
4031
|
+
clack13.log.success("Updated config with your customizations.");
|
|
4032
|
+
clack13.outro("Done! Run viberails check to verify.");
|
|
3660
4033
|
return;
|
|
3661
4034
|
}
|
|
3662
4035
|
}
|
|
3663
|
-
|
|
4036
|
+
fs22.writeFileSync(configPath, `${compactedJson}
|
|
3664
4037
|
`);
|
|
3665
4038
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
3666
4039
|
console.log(`
|
|
3667
|
-
${
|
|
4040
|
+
${import_chalk14.default.bold("Synced:")}`);
|
|
3668
4041
|
if (configChanged) {
|
|
3669
|
-
console.log(` ${
|
|
4042
|
+
console.log(` ${import_chalk14.default.yellow("!")} ${CONFIG_FILE6} \u2014 updated (review changes)`);
|
|
3670
4043
|
} else {
|
|
3671
|
-
console.log(` ${
|
|
4044
|
+
console.log(` ${import_chalk14.default.green("\u2713")} ${CONFIG_FILE6} \u2014 unchanged`);
|
|
3672
4045
|
}
|
|
3673
|
-
console.log(` ${
|
|
3674
|
-
console.log(` ${
|
|
4046
|
+
console.log(` ${import_chalk14.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
4047
|
+
console.log(` ${import_chalk14.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
3675
4048
|
}
|
|
3676
4049
|
|
|
3677
4050
|
// src/index.ts
|
|
3678
|
-
var VERSION = "0.6.
|
|
4051
|
+
var VERSION = "0.6.6";
|
|
3679
4052
|
var program = new import_commander.Command();
|
|
3680
4053
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
3681
4054
|
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) => {
|
|
@@ -3683,7 +4056,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
3683
4056
|
await initCommand(options);
|
|
3684
4057
|
} catch (err) {
|
|
3685
4058
|
const message = err instanceof Error ? err.message : String(err);
|
|
3686
|
-
console.error(`${
|
|
4059
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3687
4060
|
process.exit(1);
|
|
3688
4061
|
}
|
|
3689
4062
|
});
|
|
@@ -3692,7 +4065,7 @@ program.command("sync").description("Re-scan and update generated files").option
|
|
|
3692
4065
|
await syncCommand(options);
|
|
3693
4066
|
} catch (err) {
|
|
3694
4067
|
const message = err instanceof Error ? err.message : String(err);
|
|
3695
|
-
console.error(`${
|
|
4068
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3696
4069
|
process.exit(1);
|
|
3697
4070
|
}
|
|
3698
4071
|
});
|
|
@@ -3701,7 +4074,7 @@ program.command("config").description("Interactively edit existing config rules"
|
|
|
3701
4074
|
await configCommand(options);
|
|
3702
4075
|
} catch (err) {
|
|
3703
4076
|
const message = err instanceof Error ? err.message : String(err);
|
|
3704
|
-
console.error(`${
|
|
4077
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3705
4078
|
process.exit(1);
|
|
3706
4079
|
}
|
|
3707
4080
|
});
|
|
@@ -3722,7 +4095,7 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
3722
4095
|
process.exit(exitCode);
|
|
3723
4096
|
} catch (err) {
|
|
3724
4097
|
const message = err instanceof Error ? err.message : String(err);
|
|
3725
|
-
console.error(`${
|
|
4098
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3726
4099
|
process.exit(1);
|
|
3727
4100
|
}
|
|
3728
4101
|
}
|
|
@@ -3733,7 +4106,7 @@ program.command("fix").description("Auto-fix file naming violations and generate
|
|
|
3733
4106
|
process.exit(exitCode);
|
|
3734
4107
|
} catch (err) {
|
|
3735
4108
|
const message = err instanceof Error ? err.message : String(err);
|
|
3736
|
-
console.error(`${
|
|
4109
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3737
4110
|
process.exit(1);
|
|
3738
4111
|
}
|
|
3739
4112
|
});
|
|
@@ -3742,7 +4115,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
3742
4115
|
await boundariesCommand(options);
|
|
3743
4116
|
} catch (err) {
|
|
3744
4117
|
const message = err instanceof Error ? err.message : String(err);
|
|
3745
|
-
console.error(`${
|
|
4118
|
+
console.error(`${import_chalk15.default.red("Error:")} ${message}`);
|
|
3746
4119
|
process.exit(1);
|
|
3747
4120
|
}
|
|
3748
4121
|
});
|