viberails 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +847 -844
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1363 -432
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-XQKOK3FU.js +0 -821
- package/dist/chunk-XQKOK3FU.js.map +0 -1
- package/dist/prompt-naming-default-AH54HEBC.js +0 -57
- package/dist/prompt-naming-default-AH54HEBC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -6,9 +6,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __esm = (fn, res) => function __init() {
|
|
10
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
-
};
|
|
12
9
|
var __export = (target, all) => {
|
|
13
10
|
for (var name in all)
|
|
14
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -31,178 +28,85 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
28
|
));
|
|
32
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
30
|
|
|
34
|
-
// src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let stdout = "";
|
|
39
|
-
let stderr = "";
|
|
40
|
-
child.stdout.on("data", (d) => {
|
|
41
|
-
stdout += d.toString();
|
|
42
|
-
});
|
|
43
|
-
child.stderr.on("data", (d) => {
|
|
44
|
-
stderr += d.toString();
|
|
45
|
-
});
|
|
46
|
-
child.on("close", (status) => {
|
|
47
|
-
resolve4({ status, stdout, stderr });
|
|
48
|
-
});
|
|
49
|
-
child.on("error", () => {
|
|
50
|
-
resolve4({ status: 1, stdout, stderr });
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
var import_node_child_process;
|
|
55
|
-
var init_spawn_async = __esm({
|
|
56
|
-
"src/utils/spawn-async.ts"() {
|
|
57
|
-
"use strict";
|
|
58
|
-
import_node_child_process = require("child_process");
|
|
59
|
-
}
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
VERSION: () => VERSION
|
|
60
35
|
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_chalk16 = __toESM(require("chalk"), 1);
|
|
38
|
+
var import_commander = require("commander");
|
|
61
39
|
|
|
62
|
-
// src/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
});
|
|
79
|
-
assertNotCancelled(choice);
|
|
80
|
-
if (choice !== "install") return void 0;
|
|
81
|
-
const pm = packageManager || "npm";
|
|
82
|
-
const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
|
|
83
|
-
const s = clack.spinner();
|
|
84
|
-
s.start("Installing Lefthook...");
|
|
85
|
-
const result = await spawnAsync(installCmd, projectRoot);
|
|
86
|
-
if (result.status === 0) {
|
|
87
|
-
const fs22 = await import("fs");
|
|
88
|
-
const path22 = await import("path");
|
|
89
|
-
const lefthookPath = path22.join(projectRoot, "lefthook.yml");
|
|
90
|
-
if (!fs22.existsSync(lefthookPath)) {
|
|
91
|
-
fs22.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
|
|
92
|
-
}
|
|
93
|
-
s.stop("Installed Lefthook");
|
|
94
|
-
return "Lefthook";
|
|
95
|
-
}
|
|
96
|
-
s.stop("Failed to install Lefthook");
|
|
97
|
-
clack.log.warn(`Install manually: ${installCmd}`);
|
|
98
|
-
return void 0;
|
|
99
|
-
}
|
|
100
|
-
async function promptIntegrations(projectRoot, hookManager, tools) {
|
|
101
|
-
let resolvedHookManager = hookManager;
|
|
102
|
-
if (!resolvedHookManager) {
|
|
103
|
-
resolvedHookManager = await promptHookManagerInstall(
|
|
104
|
-
projectRoot,
|
|
105
|
-
tools?.packageManager ?? "npm",
|
|
106
|
-
tools?.isWorkspace
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
const isBareHook = !resolvedHookManager;
|
|
110
|
-
const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
|
|
111
|
-
const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
|
|
112
|
-
const options = [
|
|
113
|
-
{
|
|
114
|
-
value: "preCommit",
|
|
115
|
-
label: hookLabel,
|
|
116
|
-
hint: hookHint
|
|
40
|
+
// src/commands/boundaries.ts
|
|
41
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
42
|
+
var path3 = __toESM(require("path"), 1);
|
|
43
|
+
var import_config = require("@viberails/config");
|
|
44
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
45
|
+
|
|
46
|
+
// src/utils/find-project-root.ts
|
|
47
|
+
var fs = __toESM(require("fs"), 1);
|
|
48
|
+
var path = __toESM(require("path"), 1);
|
|
49
|
+
function findProjectRoot(startDir) {
|
|
50
|
+
let dir = path.resolve(startDir);
|
|
51
|
+
while (true) {
|
|
52
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
53
|
+
return dir;
|
|
117
54
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
value: "typecheck",
|
|
122
|
-
label: "Typecheck (tsc --noEmit)",
|
|
123
|
-
hint: "pre-commit hook + CI check"
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (tools?.linter) {
|
|
127
|
-
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
128
|
-
options.push({
|
|
129
|
-
value: "lint",
|
|
130
|
-
label: `Lint check (${linterName})`,
|
|
131
|
-
hint: "pre-commit hook + CI check"
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
options.push(
|
|
135
|
-
{
|
|
136
|
-
value: "claude",
|
|
137
|
-
label: "Claude Code hook",
|
|
138
|
-
hint: "checks files when Claude edits them"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
value: "claudeMd",
|
|
142
|
-
label: "CLAUDE.md reference",
|
|
143
|
-
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
value: "githubAction",
|
|
147
|
-
label: "GitHub Actions workflow",
|
|
148
|
-
hint: "blocks PRs that fail viberails check"
|
|
55
|
+
const parent = path.dirname(dir);
|
|
56
|
+
if (parent === dir) {
|
|
57
|
+
return null;
|
|
149
58
|
}
|
|
150
|
-
|
|
151
|
-
const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
|
|
152
|
-
const result = await clack.multiselect({
|
|
153
|
-
message: "Optional integrations",
|
|
154
|
-
options,
|
|
155
|
-
initialValues,
|
|
156
|
-
required: false
|
|
157
|
-
});
|
|
158
|
-
assertNotCancelled(result);
|
|
159
|
-
return {
|
|
160
|
-
preCommitHook: result.includes("preCommit"),
|
|
161
|
-
claudeCodeHook: result.includes("claude"),
|
|
162
|
-
claudeMdRef: result.includes("claudeMd"),
|
|
163
|
-
githubAction: result.includes("githubAction"),
|
|
164
|
-
typecheckHook: result.includes("typecheck"),
|
|
165
|
-
lintHook: result.includes("lint")
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
var clack;
|
|
169
|
-
var init_prompt_integrations = __esm({
|
|
170
|
-
"src/utils/prompt-integrations.ts"() {
|
|
171
|
-
"use strict";
|
|
172
|
-
clack = __toESM(require("@clack/prompts"), 1);
|
|
173
|
-
init_prompt();
|
|
174
|
-
init_spawn_async();
|
|
59
|
+
dir = parent;
|
|
175
60
|
}
|
|
176
|
-
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/utils/prompt.ts
|
|
64
|
+
var clack5 = __toESM(require("@clack/prompts"), 1);
|
|
65
|
+
|
|
66
|
+
// src/utils/prompt-rules.ts
|
|
67
|
+
var clack4 = __toESM(require("@clack/prompts"), 1);
|
|
177
68
|
|
|
178
69
|
// src/utils/get-root-package.ts
|
|
179
70
|
function getRootPackage(packages) {
|
|
180
71
|
return packages.find((pkg) => pkg.path === ".") ?? packages[0];
|
|
181
72
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
73
|
+
|
|
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);
|
|
187
79
|
|
|
188
80
|
// src/utils/prompt-constants.ts
|
|
189
|
-
var SENTINEL_DONE
|
|
190
|
-
var
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
SENTINEL_SKIP = "__skip__";
|
|
199
|
-
}
|
|
200
|
-
});
|
|
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
|
+
var HINT_NOT_SET = "not set";
|
|
88
|
+
var HINT_NO_OVERRIDES = "no overrides";
|
|
89
|
+
var HINT_AUTO_DETECT = "auto-detect";
|
|
201
90
|
|
|
202
91
|
// src/utils/prompt-submenus.ts
|
|
92
|
+
var clack = __toESM(require("@clack/prompts"), 1);
|
|
93
|
+
var FILE_NAMING_OPTIONS = [
|
|
94
|
+
{ value: "kebab-case", label: "kebab-case" },
|
|
95
|
+
{ value: "camelCase", label: "camelCase" },
|
|
96
|
+
{ value: "PascalCase", label: "PascalCase" },
|
|
97
|
+
{ value: "snake_case", label: "snake_case" }
|
|
98
|
+
];
|
|
99
|
+
var COMPONENT_NAMING_OPTIONS = [
|
|
100
|
+
{ value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
|
|
101
|
+
{ value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
|
|
102
|
+
];
|
|
103
|
+
var HOOK_NAMING_OPTIONS = [
|
|
104
|
+
{ value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
|
|
105
|
+
{ value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
|
|
106
|
+
];
|
|
203
107
|
async function promptFileLimitsMenu(state) {
|
|
204
108
|
while (true) {
|
|
205
|
-
const choice = await
|
|
109
|
+
const choice = await clack.select({
|
|
206
110
|
message: "File limits",
|
|
207
111
|
options: [
|
|
208
112
|
{ value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
|
|
@@ -214,10 +118,9 @@ async function promptFileLimitsMenu(state) {
|
|
|
214
118
|
{ value: "back", label: "Back" }
|
|
215
119
|
]
|
|
216
120
|
});
|
|
217
|
-
|
|
218
|
-
if (choice === "back") return;
|
|
121
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
219
122
|
if (choice === "maxFileLines") {
|
|
220
|
-
const result = await
|
|
123
|
+
const result = await clack.text({
|
|
221
124
|
message: "Maximum lines per source file?",
|
|
222
125
|
initialValue: String(state.maxFileLines),
|
|
223
126
|
validate: (v) => {
|
|
@@ -226,11 +129,11 @@ async function promptFileLimitsMenu(state) {
|
|
|
226
129
|
if (Number.isNaN(n) || n < 1) return "Enter a positive number";
|
|
227
130
|
}
|
|
228
131
|
});
|
|
229
|
-
|
|
132
|
+
if (isCancelled(result)) continue;
|
|
230
133
|
state.maxFileLines = Number.parseInt(result, 10);
|
|
231
134
|
}
|
|
232
135
|
if (choice === "maxTestFileLines") {
|
|
233
|
-
const result = await
|
|
136
|
+
const result = await clack.text({
|
|
234
137
|
message: "Maximum lines per test file (0 to disable)?",
|
|
235
138
|
initialValue: String(state.maxTestFileLines),
|
|
236
139
|
validate: (v) => {
|
|
@@ -239,7 +142,7 @@ async function promptFileLimitsMenu(state) {
|
|
|
239
142
|
if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
|
|
240
143
|
}
|
|
241
144
|
});
|
|
242
|
-
|
|
145
|
+
if (isCancelled(result)) continue;
|
|
243
146
|
state.maxTestFileLines = Number.parseInt(result, 10);
|
|
244
147
|
}
|
|
245
148
|
}
|
|
@@ -257,57 +160,56 @@ async function promptNamingMenu(state) {
|
|
|
257
160
|
options.push({
|
|
258
161
|
value: "fileNaming",
|
|
259
162
|
label: "File naming convention",
|
|
260
|
-
hint: state.fileNamingValue ??
|
|
163
|
+
hint: state.fileNamingValue ?? HINT_NOT_SET
|
|
261
164
|
});
|
|
262
165
|
}
|
|
263
166
|
options.push(
|
|
264
167
|
{
|
|
265
168
|
value: "componentNaming",
|
|
266
169
|
label: "Component naming",
|
|
267
|
-
hint: state.componentNaming ??
|
|
170
|
+
hint: state.componentNaming ?? HINT_NOT_SET
|
|
268
171
|
},
|
|
269
172
|
{
|
|
270
173
|
value: "hookNaming",
|
|
271
174
|
label: "Hook naming",
|
|
272
|
-
hint: state.hookNaming ??
|
|
175
|
+
hint: state.hookNaming ?? HINT_NOT_SET
|
|
273
176
|
},
|
|
274
177
|
{
|
|
275
178
|
value: "importAlias",
|
|
276
179
|
label: "Import alias",
|
|
277
|
-
hint: state.importAlias ??
|
|
180
|
+
hint: state.importAlias ?? HINT_NOT_SET
|
|
278
181
|
},
|
|
279
182
|
{ value: "back", label: "Back" }
|
|
280
183
|
);
|
|
281
|
-
const choice = await
|
|
282
|
-
|
|
283
|
-
if (choice === "back") return;
|
|
184
|
+
const choice = await clack.select({ message: "Naming & conventions", options });
|
|
185
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
284
186
|
if (choice === "enforceNaming") {
|
|
285
|
-
const result = await
|
|
187
|
+
const result = await clack.confirm({
|
|
286
188
|
message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
|
|
287
189
|
initialValue: state.enforceNaming
|
|
288
190
|
});
|
|
289
|
-
|
|
191
|
+
if (isCancelled(result)) continue;
|
|
290
192
|
if (result && !state.fileNamingValue) {
|
|
291
|
-
const selected = await
|
|
193
|
+
const selected = await clack.select({
|
|
292
194
|
message: "Which file naming convention should be enforced?",
|
|
293
195
|
options: [...FILE_NAMING_OPTIONS]
|
|
294
196
|
});
|
|
295
|
-
|
|
197
|
+
if (isCancelled(selected)) continue;
|
|
296
198
|
state.fileNamingValue = selected;
|
|
297
199
|
}
|
|
298
200
|
state.enforceNaming = result;
|
|
299
201
|
}
|
|
300
202
|
if (choice === "fileNaming") {
|
|
301
|
-
const selected = await
|
|
203
|
+
const selected = await clack.select({
|
|
302
204
|
message: "Which file naming convention should be enforced?",
|
|
303
205
|
options: [...FILE_NAMING_OPTIONS],
|
|
304
206
|
initialValue: state.fileNamingValue
|
|
305
207
|
});
|
|
306
|
-
|
|
208
|
+
if (isCancelled(selected)) continue;
|
|
307
209
|
state.fileNamingValue = selected;
|
|
308
210
|
}
|
|
309
211
|
if (choice === "componentNaming") {
|
|
310
|
-
const selected = await
|
|
212
|
+
const selected = await clack.select({
|
|
311
213
|
message: "Component naming convention",
|
|
312
214
|
options: [
|
|
313
215
|
...COMPONENT_NAMING_OPTIONS,
|
|
@@ -315,11 +217,11 @@ async function promptNamingMenu(state) {
|
|
|
315
217
|
],
|
|
316
218
|
initialValue: state.componentNaming ?? SENTINEL_CLEAR
|
|
317
219
|
});
|
|
318
|
-
|
|
220
|
+
if (isCancelled(selected)) continue;
|
|
319
221
|
state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
320
222
|
}
|
|
321
223
|
if (choice === "hookNaming") {
|
|
322
|
-
const selected = await
|
|
224
|
+
const selected = await clack.select({
|
|
323
225
|
message: "Hook naming convention",
|
|
324
226
|
options: [
|
|
325
227
|
...HOOK_NAMING_OPTIONS,
|
|
@@ -327,11 +229,11 @@ async function promptNamingMenu(state) {
|
|
|
327
229
|
],
|
|
328
230
|
initialValue: state.hookNaming ?? SENTINEL_CLEAR
|
|
329
231
|
});
|
|
330
|
-
|
|
232
|
+
if (isCancelled(selected)) continue;
|
|
331
233
|
state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
|
|
332
234
|
}
|
|
333
235
|
if (choice === "importAlias") {
|
|
334
|
-
const selected = await
|
|
236
|
+
const selected = await clack.select({
|
|
335
237
|
message: "Import alias pattern",
|
|
336
238
|
options: [
|
|
337
239
|
{ value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
|
|
@@ -341,11 +243,11 @@ async function promptNamingMenu(state) {
|
|
|
341
243
|
],
|
|
342
244
|
initialValue: state.importAlias ?? SENTINEL_CLEAR
|
|
343
245
|
});
|
|
344
|
-
|
|
246
|
+
if (isCancelled(selected)) continue;
|
|
345
247
|
if (selected === SENTINEL_CLEAR) {
|
|
346
248
|
state.importAlias = void 0;
|
|
347
249
|
} else if (selected === SENTINEL_CUSTOM) {
|
|
348
|
-
const result = await
|
|
250
|
+
const result = await clack.text({
|
|
349
251
|
message: "Custom import alias (e.g. #/*)?",
|
|
350
252
|
initialValue: state.importAlias ?? "",
|
|
351
253
|
placeholder: "e.g. #/*",
|
|
@@ -355,7 +257,7 @@ async function promptNamingMenu(state) {
|
|
|
355
257
|
return "Must match pattern like @/*, ~/*, or #src/*";
|
|
356
258
|
}
|
|
357
259
|
});
|
|
358
|
-
|
|
260
|
+
if (isCancelled(result)) continue;
|
|
359
261
|
state.importAlias = result.trim();
|
|
360
262
|
} else {
|
|
361
263
|
state.importAlias = selected;
|
|
@@ -392,19 +294,18 @@ async function promptTestingMenu(state) {
|
|
|
392
294
|
);
|
|
393
295
|
}
|
|
394
296
|
options.push({ value: "back", label: "Back" });
|
|
395
|
-
const choice = await
|
|
396
|
-
|
|
397
|
-
if (choice === "back") return;
|
|
297
|
+
const choice = await clack.select({ message: "Testing & coverage", options });
|
|
298
|
+
if (isCancelled(choice) || choice === "back") return;
|
|
398
299
|
if (choice === "enforceMissingTests") {
|
|
399
|
-
const result = await
|
|
300
|
+
const result = await clack.confirm({
|
|
400
301
|
message: "Require every source file to have a corresponding test file?",
|
|
401
302
|
initialValue: state.enforceMissingTests
|
|
402
303
|
});
|
|
403
|
-
|
|
304
|
+
if (isCancelled(result)) continue;
|
|
404
305
|
state.enforceMissingTests = result;
|
|
405
306
|
}
|
|
406
307
|
if (choice === "testCoverage") {
|
|
407
|
-
const result = await
|
|
308
|
+
const result = await clack.text({
|
|
408
309
|
message: "Test coverage target (0 disables coverage checks)?",
|
|
409
310
|
initialValue: String(state.testCoverage),
|
|
410
311
|
validate: (v) => {
|
|
@@ -413,55 +314,32 @@ async function promptTestingMenu(state) {
|
|
|
413
314
|
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
414
315
|
}
|
|
415
316
|
});
|
|
416
|
-
|
|
317
|
+
if (isCancelled(result)) continue;
|
|
417
318
|
state.testCoverage = Number.parseInt(result, 10);
|
|
418
319
|
}
|
|
419
320
|
if (choice === "coverageSummaryPath") {
|
|
420
|
-
const result = await
|
|
321
|
+
const result = await clack.text({
|
|
421
322
|
message: "Coverage summary path (relative to package root)?",
|
|
422
323
|
initialValue: state.coverageSummaryPath,
|
|
423
324
|
validate: (v) => {
|
|
424
325
|
if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
|
|
425
326
|
}
|
|
426
327
|
});
|
|
427
|
-
|
|
328
|
+
if (isCancelled(result)) continue;
|
|
428
329
|
state.coverageSummaryPath = result.trim();
|
|
429
330
|
}
|
|
430
331
|
if (choice === "coverageCommand") {
|
|
431
|
-
const result = await
|
|
332
|
+
const result = await clack.text({
|
|
432
333
|
message: "Coverage command (blank to auto-detect from package.json)?",
|
|
433
334
|
initialValue: state.coverageCommand ?? "",
|
|
434
335
|
placeholder: "(auto-detect from package.json test runner)"
|
|
435
336
|
});
|
|
436
|
-
|
|
337
|
+
if (isCancelled(result)) continue;
|
|
437
338
|
const trimmed = result.trim();
|
|
438
339
|
state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
|
|
439
340
|
}
|
|
440
341
|
}
|
|
441
342
|
}
|
|
442
|
-
var clack2, FILE_NAMING_OPTIONS, COMPONENT_NAMING_OPTIONS, HOOK_NAMING_OPTIONS;
|
|
443
|
-
var init_prompt_submenus = __esm({
|
|
444
|
-
"src/utils/prompt-submenus.ts"() {
|
|
445
|
-
"use strict";
|
|
446
|
-
clack2 = __toESM(require("@clack/prompts"), 1);
|
|
447
|
-
init_prompt();
|
|
448
|
-
init_prompt_constants();
|
|
449
|
-
FILE_NAMING_OPTIONS = [
|
|
450
|
-
{ value: "kebab-case", label: "kebab-case" },
|
|
451
|
-
{ value: "camelCase", label: "camelCase" },
|
|
452
|
-
{ value: "PascalCase", label: "PascalCase" },
|
|
453
|
-
{ value: "snake_case", label: "snake_case" }
|
|
454
|
-
];
|
|
455
|
-
COMPONENT_NAMING_OPTIONS = [
|
|
456
|
-
{ value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
|
|
457
|
-
{ value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
|
|
458
|
-
];
|
|
459
|
-
HOOK_NAMING_OPTIONS = [
|
|
460
|
-
{ value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
|
|
461
|
-
{ value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
|
|
462
|
-
];
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
343
|
|
|
466
344
|
// src/utils/prompt-package-overrides.ts
|
|
467
345
|
function normalizePackageOverrides(packages) {
|
|
@@ -500,13 +378,13 @@ function packageOverrideHint(pkg, defaults) {
|
|
|
500
378
|
const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
|
|
501
379
|
if (hasSummaryOverride) tags.push("summary override");
|
|
502
380
|
if (hasCommandOverride) tags.push("command override");
|
|
503
|
-
return tags.length > 0 ? tags.join(", ") :
|
|
381
|
+
return tags.length > 0 ? tags.join(", ") : HINT_NO_OVERRIDES;
|
|
504
382
|
}
|
|
505
383
|
async function promptPackageOverrides(packages, defaults) {
|
|
506
384
|
const editablePackages = packages.filter((pkg) => pkg.path !== ".");
|
|
507
385
|
if (editablePackages.length === 0) return packages;
|
|
508
386
|
while (true) {
|
|
509
|
-
const selectedPath = await
|
|
387
|
+
const selectedPath = await clack2.select({
|
|
510
388
|
message: "Select package to edit overrides",
|
|
511
389
|
options: [
|
|
512
390
|
...editablePackages.map((pkg) => ({
|
|
@@ -517,8 +395,7 @@ async function promptPackageOverrides(packages, defaults) {
|
|
|
517
395
|
{ value: SENTINEL_DONE, label: "Done" }
|
|
518
396
|
]
|
|
519
397
|
});
|
|
520
|
-
|
|
521
|
-
if (selectedPath === SENTINEL_DONE) break;
|
|
398
|
+
if (isCancelled(selectedPath) || selectedPath === SENTINEL_DONE) break;
|
|
522
399
|
const target = editablePackages.find((pkg) => pkg.path === selectedPath);
|
|
523
400
|
if (!target) continue;
|
|
524
401
|
await promptSinglePackageOverrides(target, defaults);
|
|
@@ -532,12 +409,12 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
532
409
|
const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
|
|
533
410
|
const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
|
|
534
411
|
const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
|
|
535
|
-
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ??
|
|
412
|
+
const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? HINT_AUTO_DETECT;
|
|
536
413
|
const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
|
|
537
414
|
const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
|
|
538
|
-
const namingHint = hasNamingOverride ? String(effectiveNaming) : `
|
|
539
|
-
const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `
|
|
540
|
-
const choice = await
|
|
415
|
+
const namingHint = hasNamingOverride ? String(effectiveNaming) : `inherits: ${effectiveNaming ?? "not set"}`;
|
|
416
|
+
const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `inherits: ${effectiveMaxLines}`;
|
|
417
|
+
const choice = await clack2.select({
|
|
541
418
|
message: `Edit overrides for ${target.path}`,
|
|
542
419
|
options: [
|
|
543
420
|
{ value: "fileNaming", label: "File naming", hint: namingHint },
|
|
@@ -549,10 +426,9 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
549
426
|
{ value: "back", label: "Back to package list" }
|
|
550
427
|
]
|
|
551
428
|
});
|
|
552
|
-
|
|
553
|
-
if (choice === "back") break;
|
|
429
|
+
if (isCancelled(choice) || choice === "back") break;
|
|
554
430
|
if (choice === "fileNaming") {
|
|
555
|
-
const selected = await
|
|
431
|
+
const selected = await clack2.select({
|
|
556
432
|
message: `File naming for ${target.path}`,
|
|
557
433
|
options: [
|
|
558
434
|
...FILE_NAMING_OPTIONS,
|
|
@@ -564,7 +440,7 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
564
440
|
],
|
|
565
441
|
initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
|
|
566
442
|
});
|
|
567
|
-
|
|
443
|
+
if (isCancelled(selected)) continue;
|
|
568
444
|
if (selected === SENTINEL_INHERIT) {
|
|
569
445
|
if (target.conventions) delete target.conventions.fileNaming;
|
|
570
446
|
} else if (selected === SENTINEL_NONE) {
|
|
@@ -574,12 +450,12 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
574
450
|
}
|
|
575
451
|
}
|
|
576
452
|
if (choice === "maxFileLines") {
|
|
577
|
-
const result = await
|
|
453
|
+
const result = await clack2.text({
|
|
578
454
|
message: `Max file lines for ${target.path} (blank to inherit default)?`,
|
|
579
455
|
initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
|
|
580
456
|
placeholder: String(defaults.maxFileLines)
|
|
581
457
|
});
|
|
582
|
-
|
|
458
|
+
if (isCancelled(result)) continue;
|
|
583
459
|
const value = result.trim();
|
|
584
460
|
if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
|
|
585
461
|
if (target.rules) delete target.rules.maxFileLines;
|
|
@@ -588,7 +464,7 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
588
464
|
}
|
|
589
465
|
}
|
|
590
466
|
if (choice === "testCoverage") {
|
|
591
|
-
const result = await
|
|
467
|
+
const result = await clack2.text({
|
|
592
468
|
message: "Package testCoverage (0 to exempt package)?",
|
|
593
469
|
initialValue: String(effectiveCoverage),
|
|
594
470
|
validate: (v) => {
|
|
@@ -597,7 +473,7 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
597
473
|
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
598
474
|
}
|
|
599
475
|
});
|
|
600
|
-
|
|
476
|
+
if (isCancelled(result)) continue;
|
|
601
477
|
const nextCoverage = Number.parseInt(result, 10);
|
|
602
478
|
if (nextCoverage === defaults.testCoverage) {
|
|
603
479
|
if (target.rules) delete target.rules.testCoverage;
|
|
@@ -606,12 +482,12 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
606
482
|
}
|
|
607
483
|
}
|
|
608
484
|
if (choice === "summaryPath") {
|
|
609
|
-
const result = await
|
|
485
|
+
const result = await clack2.text({
|
|
610
486
|
message: "Path to coverage summary file (blank to inherit default)?",
|
|
611
487
|
initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
|
|
612
488
|
placeholder: defaults.coverageSummaryPath
|
|
613
489
|
});
|
|
614
|
-
|
|
490
|
+
if (isCancelled(result)) continue;
|
|
615
491
|
const value = result.trim();
|
|
616
492
|
if (value.length === 0 || value === defaults.coverageSummaryPath) {
|
|
617
493
|
if (target.coverage) delete target.coverage.summaryPath;
|
|
@@ -620,12 +496,12 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
620
496
|
}
|
|
621
497
|
}
|
|
622
498
|
if (choice === "command") {
|
|
623
|
-
const result = await
|
|
499
|
+
const result = await clack2.text({
|
|
624
500
|
message: "Coverage command (blank to auto-detect)?",
|
|
625
501
|
initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
|
|
626
502
|
placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
|
|
627
503
|
});
|
|
628
|
-
|
|
504
|
+
if (isCancelled(result)) continue;
|
|
629
505
|
const value = result.trim();
|
|
630
506
|
const defaultCommand = defaults.coverageCommand ?? "";
|
|
631
507
|
if (value.length === 0 || value === defaultCommand) {
|
|
@@ -644,16 +520,6 @@ async function promptSinglePackageOverrides(target, defaults) {
|
|
|
644
520
|
}
|
|
645
521
|
}
|
|
646
522
|
}
|
|
647
|
-
var clack3;
|
|
648
|
-
var init_prompt_package_overrides = __esm({
|
|
649
|
-
"src/utils/prompt-package-overrides.ts"() {
|
|
650
|
-
"use strict";
|
|
651
|
-
clack3 = __toESM(require("@clack/prompts"), 1);
|
|
652
|
-
init_prompt();
|
|
653
|
-
init_prompt_constants();
|
|
654
|
-
init_prompt_submenus();
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
523
|
|
|
658
524
|
// src/utils/prompt-menu-handlers.ts
|
|
659
525
|
function getPackageDiffs(pkg, root) {
|
|
@@ -695,11 +561,11 @@ function getPackageDiffs(pkg, root) {
|
|
|
695
561
|
return diffs;
|
|
696
562
|
}
|
|
697
563
|
function buildMenuOptions(state, packageCount) {
|
|
698
|
-
const
|
|
564
|
+
const fileLimitsHint2 = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
|
|
699
565
|
const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
|
|
700
566
|
const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
|
|
701
567
|
const options = [
|
|
702
|
-
{ value: "fileLimits", label: "File limits", hint:
|
|
568
|
+
{ value: "fileLimits", label: "File limits", hint: fileLimitsHint2 },
|
|
703
569
|
{ value: "naming", label: "Naming & conventions", hint: namingHint },
|
|
704
570
|
{ value: "testing", label: "Testing & coverage", hint: testingHint }
|
|
705
571
|
];
|
|
@@ -733,7 +599,7 @@ async function handleMenuChoice(choice, state, defaults, root) {
|
|
|
733
599
|
state.coverageSummaryPath = defaults.coverageSummaryPath;
|
|
734
600
|
state.coverageCommand = defaults.coverageCommand;
|
|
735
601
|
state.packageOverrides = clonePackages(defaults.packageOverrides);
|
|
736
|
-
|
|
602
|
+
clack3.log.info("Reset all rules to detected defaults.");
|
|
737
603
|
return;
|
|
738
604
|
}
|
|
739
605
|
if (choice === "fileLimits") {
|
|
@@ -761,21 +627,12 @@ async function handleMenuChoice(choice, state, defaults, root) {
|
|
|
761
627
|
const lines = packageDiffs.map((entry) => `${entry.pkg.path}
|
|
762
628
|
${entry.diffs.join(", ")}`);
|
|
763
629
|
if (lines.length > 0) {
|
|
764
|
-
|
|
630
|
+
clack3.note(lines.join("\n\n"), "Existing package differences");
|
|
765
631
|
}
|
|
766
632
|
}
|
|
767
633
|
return;
|
|
768
634
|
}
|
|
769
635
|
}
|
|
770
|
-
var clack4;
|
|
771
|
-
var init_prompt_menu_handlers = __esm({
|
|
772
|
-
"src/utils/prompt-menu-handlers.ts"() {
|
|
773
|
-
"use strict";
|
|
774
|
-
clack4 = __toESM(require("@clack/prompts"), 1);
|
|
775
|
-
init_prompt_package_overrides();
|
|
776
|
-
init_prompt_submenus();
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
636
|
|
|
780
637
|
// src/utils/prompt-rules.ts
|
|
781
638
|
async function promptRuleMenu(defaults) {
|
|
@@ -787,7 +644,7 @@ async function promptRuleMenu(defaults) {
|
|
|
787
644
|
const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
|
|
788
645
|
while (true) {
|
|
789
646
|
const options = buildMenuOptions(state, packageCount);
|
|
790
|
-
const choice = await
|
|
647
|
+
const choice = await clack4.select({ message: "Customize rules", options });
|
|
791
648
|
assertNotCancelled(choice);
|
|
792
649
|
if (choice === "done") break;
|
|
793
650
|
await handleMenuChoice(choice, state, defaults, root);
|
|
@@ -807,36 +664,29 @@ async function promptRuleMenu(defaults) {
|
|
|
807
664
|
packageOverrides: state.packageOverrides
|
|
808
665
|
};
|
|
809
666
|
}
|
|
810
|
-
var clack5;
|
|
811
|
-
var init_prompt_rules = __esm({
|
|
812
|
-
"src/utils/prompt-rules.ts"() {
|
|
813
|
-
"use strict";
|
|
814
|
-
clack5 = __toESM(require("@clack/prompts"), 1);
|
|
815
|
-
init_get_root_package();
|
|
816
|
-
init_prompt();
|
|
817
|
-
init_prompt_menu_handlers();
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
667
|
|
|
821
668
|
// src/utils/prompt.ts
|
|
822
669
|
function assertNotCancelled(value) {
|
|
823
|
-
if (
|
|
824
|
-
|
|
670
|
+
if (clack5.isCancel(value)) {
|
|
671
|
+
clack5.cancel("Setup cancelled.");
|
|
825
672
|
process.exit(0);
|
|
826
673
|
}
|
|
827
674
|
}
|
|
675
|
+
function isCancelled(value) {
|
|
676
|
+
return clack5.isCancel(value);
|
|
677
|
+
}
|
|
828
678
|
async function confirm3(message) {
|
|
829
|
-
const result = await
|
|
679
|
+
const result = await clack5.confirm({ message, initialValue: true });
|
|
830
680
|
assertNotCancelled(result);
|
|
831
681
|
return result;
|
|
832
682
|
}
|
|
833
683
|
async function confirmDangerous(message) {
|
|
834
|
-
const result = await
|
|
684
|
+
const result = await clack5.confirm({ message, initialValue: false });
|
|
835
685
|
assertNotCancelled(result);
|
|
836
686
|
return result;
|
|
837
687
|
}
|
|
838
688
|
async function promptExistingConfigAction(configFile) {
|
|
839
|
-
const result = await
|
|
689
|
+
const result = await clack5.select({
|
|
840
690
|
message: `${configFile} already exists. What do you want to do?`,
|
|
841
691
|
options: [
|
|
842
692
|
{
|
|
@@ -859,134 +709,6 @@ async function promptExistingConfigAction(configFile) {
|
|
|
859
709
|
assertNotCancelled(result);
|
|
860
710
|
return result;
|
|
861
711
|
}
|
|
862
|
-
async function promptInitDecision() {
|
|
863
|
-
const result = await clack6.select({
|
|
864
|
-
message: "How do you want to proceed?",
|
|
865
|
-
options: [
|
|
866
|
-
{
|
|
867
|
-
value: "accept",
|
|
868
|
-
label: "Accept defaults",
|
|
869
|
-
hint: "writes the config with these defaults; use --enforce in CI to block"
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
value: "customize",
|
|
873
|
-
label: "Customize rules",
|
|
874
|
-
hint: "edit limits, naming, test coverage, and package overrides"
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
value: "review",
|
|
878
|
-
label: "Review detected details",
|
|
879
|
-
hint: "show the full scan report with package and structure details"
|
|
880
|
-
}
|
|
881
|
-
]
|
|
882
|
-
});
|
|
883
|
-
assertNotCancelled(result);
|
|
884
|
-
return result;
|
|
885
|
-
}
|
|
886
|
-
var clack6;
|
|
887
|
-
var init_prompt = __esm({
|
|
888
|
-
"src/utils/prompt.ts"() {
|
|
889
|
-
"use strict";
|
|
890
|
-
clack6 = __toESM(require("@clack/prompts"), 1);
|
|
891
|
-
init_prompt_integrations();
|
|
892
|
-
init_prompt_rules();
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
// src/utils/prompt-naming-default.ts
|
|
897
|
-
var prompt_naming_default_exports = {};
|
|
898
|
-
__export(prompt_naming_default_exports, {
|
|
899
|
-
resolveNamingDefault: () => resolveNamingDefault
|
|
900
|
-
});
|
|
901
|
-
async function resolveNamingDefault(config, scanResult) {
|
|
902
|
-
const rootPkg = getRootPackage(config.packages);
|
|
903
|
-
if (!config.rules.enforceNaming || rootPkg?.conventions?.fileNaming) return false;
|
|
904
|
-
const isMonorepo = config.packages.length > 1;
|
|
905
|
-
const pkgNamingData = isMonorepo ? scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
|
|
906
|
-
path: p.relativePath,
|
|
907
|
-
naming: p.conventions.fileNaming
|
|
908
|
-
})) : [];
|
|
909
|
-
const chosen = await promptNamingDefault(pkgNamingData, isMonorepo);
|
|
910
|
-
if (chosen === SENTINEL_SKIP) {
|
|
911
|
-
config.rules.enforceNaming = false;
|
|
912
|
-
} else if (rootPkg) {
|
|
913
|
-
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
914
|
-
rootPkg.conventions.fileNaming = chosen;
|
|
915
|
-
}
|
|
916
|
-
return true;
|
|
917
|
-
}
|
|
918
|
-
async function promptNamingDefault(pkgNamingData, isMonorepo) {
|
|
919
|
-
if (isMonorepo && pkgNamingData.length > 0) {
|
|
920
|
-
const lines = pkgNamingData.map(
|
|
921
|
-
(p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
|
|
922
|
-
);
|
|
923
|
-
clack10.note(lines.join("\n"), "Per-package file naming detected");
|
|
924
|
-
}
|
|
925
|
-
const message = isMonorepo ? "Which convention should be the default? You can override per-package later." : "Which file naming convention should be used?";
|
|
926
|
-
const options = FILE_NAMING_OPTIONS.map((opt) => {
|
|
927
|
-
if (isMonorepo && pkgNamingData.length > 0) {
|
|
928
|
-
const count = pkgNamingData.filter((p) => p.naming.value === opt.value).length;
|
|
929
|
-
return {
|
|
930
|
-
value: opt.value,
|
|
931
|
-
label: opt.label,
|
|
932
|
-
hint: count > 0 ? `${count} package${count > 1 ? "s" : ""}` : void 0
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
return { value: opt.value, label: opt.label };
|
|
936
|
-
});
|
|
937
|
-
const selected = await clack10.select({
|
|
938
|
-
message,
|
|
939
|
-
options: [...options, { value: SENTINEL_SKIP, label: "Don't enforce naming" }]
|
|
940
|
-
});
|
|
941
|
-
assertNotCancelled(selected);
|
|
942
|
-
return selected;
|
|
943
|
-
}
|
|
944
|
-
var clack10;
|
|
945
|
-
var init_prompt_naming_default = __esm({
|
|
946
|
-
"src/utils/prompt-naming-default.ts"() {
|
|
947
|
-
"use strict";
|
|
948
|
-
clack10 = __toESM(require("@clack/prompts"), 1);
|
|
949
|
-
init_get_root_package();
|
|
950
|
-
init_prompt();
|
|
951
|
-
init_prompt_constants();
|
|
952
|
-
init_prompt_submenus();
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
// src/index.ts
|
|
957
|
-
var index_exports = {};
|
|
958
|
-
__export(index_exports, {
|
|
959
|
-
VERSION: () => VERSION
|
|
960
|
-
});
|
|
961
|
-
module.exports = __toCommonJS(index_exports);
|
|
962
|
-
var import_chalk16 = __toESM(require("chalk"), 1);
|
|
963
|
-
var import_commander = require("commander");
|
|
964
|
-
|
|
965
|
-
// src/commands/boundaries.ts
|
|
966
|
-
var fs3 = __toESM(require("fs"), 1);
|
|
967
|
-
var path3 = __toESM(require("path"), 1);
|
|
968
|
-
var import_config = require("@viberails/config");
|
|
969
|
-
var import_chalk = __toESM(require("chalk"), 1);
|
|
970
|
-
|
|
971
|
-
// src/utils/find-project-root.ts
|
|
972
|
-
var fs = __toESM(require("fs"), 1);
|
|
973
|
-
var path = __toESM(require("path"), 1);
|
|
974
|
-
function findProjectRoot(startDir) {
|
|
975
|
-
let dir = path.resolve(startDir);
|
|
976
|
-
while (true) {
|
|
977
|
-
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
978
|
-
return dir;
|
|
979
|
-
}
|
|
980
|
-
const parent = path.dirname(dir);
|
|
981
|
-
if (parent === dir) {
|
|
982
|
-
return null;
|
|
983
|
-
}
|
|
984
|
-
dir = parent;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// src/commands/boundaries.ts
|
|
989
|
-
init_prompt();
|
|
990
712
|
|
|
991
713
|
// src/utils/resolve-workspace-packages.ts
|
|
992
714
|
var fs2 = __toESM(require("fs"), 1);
|
|
@@ -1179,7 +901,7 @@ function resolveIgnoreForFile(relPath, config) {
|
|
|
1179
901
|
}
|
|
1180
902
|
|
|
1181
903
|
// src/commands/check-coverage.ts
|
|
1182
|
-
var
|
|
904
|
+
var import_node_child_process = require("child_process");
|
|
1183
905
|
var fs4 = __toESM(require("fs"), 1);
|
|
1184
906
|
var path4 = __toESM(require("path"), 1);
|
|
1185
907
|
var import_config3 = require("@viberails/config");
|
|
@@ -1222,7 +944,7 @@ function readCoveragePercentage(summaryPath) {
|
|
|
1222
944
|
}
|
|
1223
945
|
}
|
|
1224
946
|
function runCoverageCommand(pkgRoot, command) {
|
|
1225
|
-
const result = (0,
|
|
947
|
+
const result = (0, import_node_child_process.spawnSync)(command, {
|
|
1226
948
|
cwd: pkgRoot,
|
|
1227
949
|
shell: true,
|
|
1228
950
|
encoding: "utf-8",
|
|
@@ -1317,7 +1039,7 @@ function checkCoverage(projectRoot, config, filesToCheck, options) {
|
|
|
1317
1039
|
}
|
|
1318
1040
|
|
|
1319
1041
|
// src/commands/check-files.ts
|
|
1320
|
-
var
|
|
1042
|
+
var import_node_child_process2 = require("child_process");
|
|
1321
1043
|
var fs5 = __toESM(require("fs"), 1);
|
|
1322
1044
|
var path5 = __toESM(require("path"), 1);
|
|
1323
1045
|
var import_config4 = require("@viberails/config");
|
|
@@ -1390,7 +1112,7 @@ function checkNaming(relPath, conventions) {
|
|
|
1390
1112
|
}
|
|
1391
1113
|
function getStagedFiles(projectRoot) {
|
|
1392
1114
|
try {
|
|
1393
|
-
const output = (0,
|
|
1115
|
+
const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=ACMR", {
|
|
1394
1116
|
cwd: projectRoot,
|
|
1395
1117
|
encoding: "utf-8",
|
|
1396
1118
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1402,12 +1124,12 @@ function getStagedFiles(projectRoot) {
|
|
|
1402
1124
|
}
|
|
1403
1125
|
function getDiffFiles(projectRoot, base) {
|
|
1404
1126
|
try {
|
|
1405
|
-
const allOutput = (0,
|
|
1127
|
+
const allOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=ACMR ${base}...HEAD`, {
|
|
1406
1128
|
cwd: projectRoot,
|
|
1407
1129
|
encoding: "utf-8",
|
|
1408
1130
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1409
1131
|
});
|
|
1410
|
-
const addedOutput = (0,
|
|
1132
|
+
const addedOutput = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=A ${base}...HEAD`, {
|
|
1411
1133
|
cwd: projectRoot,
|
|
1412
1134
|
encoding: "utf-8",
|
|
1413
1135
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1453,7 +1175,7 @@ function deletedTestFileToSourceFile(deletedTestFile, config) {
|
|
|
1453
1175
|
}
|
|
1454
1176
|
function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
1455
1177
|
try {
|
|
1456
|
-
const output = (0,
|
|
1178
|
+
const output = (0, import_node_child_process2.execSync)("git diff --cached --name-only --diff-filter=D", {
|
|
1457
1179
|
cwd: projectRoot,
|
|
1458
1180
|
encoding: "utf-8",
|
|
1459
1181
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1465,7 +1187,7 @@ function getStagedDeletedTestSourceFiles(projectRoot, config) {
|
|
|
1465
1187
|
}
|
|
1466
1188
|
function getDiffDeletedTestSourceFiles(projectRoot, base, config) {
|
|
1467
1189
|
try {
|
|
1468
|
-
const output = (0,
|
|
1190
|
+
const output = (0, import_node_child_process2.execSync)(`git diff --name-only --diff-filter=D ${base}...HEAD`, {
|
|
1469
1191
|
cwd: projectRoot,
|
|
1470
1192
|
encoding: "utf-8",
|
|
1471
1193
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1691,9 +1413,9 @@ async function checkCommand(options, cwd) {
|
|
|
1691
1413
|
}
|
|
1692
1414
|
const violations = [];
|
|
1693
1415
|
const severity = options.enforce ? "error" : "warn";
|
|
1694
|
-
const
|
|
1416
|
+
const log9 = options.format !== "json" && !options.hook && !options.quiet ? (msg) => process.stderr.write(import_chalk3.default.dim(msg)) : () => {
|
|
1695
1417
|
};
|
|
1696
|
-
|
|
1418
|
+
log9(" Checking files...");
|
|
1697
1419
|
for (const file of filesToCheck) {
|
|
1698
1420
|
const absPath = path7.isAbsolute(file) ? file : path7.join(projectRoot, file);
|
|
1699
1421
|
const relPath = path7.relative(projectRoot, absPath);
|
|
@@ -1726,9 +1448,9 @@ async function checkCommand(options, cwd) {
|
|
|
1726
1448
|
}
|
|
1727
1449
|
}
|
|
1728
1450
|
}
|
|
1729
|
-
|
|
1451
|
+
log9(" done\n");
|
|
1730
1452
|
if (!options.files) {
|
|
1731
|
-
|
|
1453
|
+
log9(" Checking missing tests...");
|
|
1732
1454
|
const testViolations = checkMissingTests(projectRoot, config, severity);
|
|
1733
1455
|
if (options.staged) {
|
|
1734
1456
|
const stagedSet = new Set(filesToCheck);
|
|
@@ -1741,14 +1463,14 @@ async function checkCommand(options, cwd) {
|
|
|
1741
1463
|
} else {
|
|
1742
1464
|
violations.push(...testViolations);
|
|
1743
1465
|
}
|
|
1744
|
-
|
|
1466
|
+
log9(" done\n");
|
|
1745
1467
|
}
|
|
1746
1468
|
if (!options.files && !options.staged && !options.diffBase) {
|
|
1747
|
-
|
|
1469
|
+
log9(" Running test coverage...\n");
|
|
1748
1470
|
const coverageViolations = checkCoverage(projectRoot, config, filesToCheck, {
|
|
1749
1471
|
staged: options.staged,
|
|
1750
1472
|
enforce: options.enforce,
|
|
1751
|
-
onProgress: (pkg) =>
|
|
1473
|
+
onProgress: (pkg) => log9(` Coverage: ${pkg}...
|
|
1752
1474
|
`)
|
|
1753
1475
|
});
|
|
1754
1476
|
violations.push(...coverageViolations);
|
|
@@ -1773,7 +1495,7 @@ async function checkCommand(options, cwd) {
|
|
|
1773
1495
|
severity
|
|
1774
1496
|
});
|
|
1775
1497
|
}
|
|
1776
|
-
|
|
1498
|
+
log9(` Boundary check: ${graph.nodes.length} files in ${Date.now() - startTime}ms
|
|
1777
1499
|
`);
|
|
1778
1500
|
}
|
|
1779
1501
|
if (options.format === "json") {
|
|
@@ -1849,7 +1571,7 @@ async function hookCheckCommand(cwd) {
|
|
|
1849
1571
|
// src/commands/config.ts
|
|
1850
1572
|
var fs10 = __toESM(require("fs"), 1);
|
|
1851
1573
|
var path9 = __toESM(require("path"), 1);
|
|
1852
|
-
var
|
|
1574
|
+
var clack6 = __toESM(require("@clack/prompts"), 1);
|
|
1853
1575
|
var import_config6 = require("@viberails/config");
|
|
1854
1576
|
var import_scanner = require("@viberails/scanner");
|
|
1855
1577
|
var import_chalk6 = __toESM(require("chalk"), 1);
|
|
@@ -2273,7 +1995,6 @@ function formatScanResultsText(scanResult) {
|
|
|
2273
1995
|
}
|
|
2274
1996
|
|
|
2275
1997
|
// src/utils/apply-rule-overrides.ts
|
|
2276
|
-
init_get_root_package();
|
|
2277
1998
|
function applyRuleOverrides(config, overrides) {
|
|
2278
1999
|
if (overrides.packageOverrides) config.packages = overrides.packageOverrides;
|
|
2279
2000
|
const rootPkg = getRootPackage(config.packages);
|
|
@@ -2444,9 +2165,6 @@ function formatStatsDelta(oldStats, newStats) {
|
|
|
2444
2165
|
return `${parts.join(", ")} since last sync`;
|
|
2445
2166
|
}
|
|
2446
2167
|
|
|
2447
|
-
// src/commands/config.ts
|
|
2448
|
-
init_prompt();
|
|
2449
|
-
|
|
2450
2168
|
// src/utils/write-generated-files.ts
|
|
2451
2169
|
var fs9 = __toESM(require("fs"), 1);
|
|
2452
2170
|
var path8 = __toESM(require("path"), 1);
|
|
@@ -2486,11 +2204,11 @@ async function configCommand(options, cwd) {
|
|
|
2486
2204
|
return;
|
|
2487
2205
|
}
|
|
2488
2206
|
if (!options.suppressIntro) {
|
|
2489
|
-
|
|
2207
|
+
clack6.intro("viberails config");
|
|
2490
2208
|
}
|
|
2491
2209
|
const config = await (0, import_config6.loadConfig)(configPath);
|
|
2492
2210
|
let scanResult = options.rescan ? await rescanAndMerge(projectRoot, config) : void 0;
|
|
2493
|
-
|
|
2211
|
+
clack6.note(formatRulesText(config).join("\n"), "Current rules");
|
|
2494
2212
|
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
2495
2213
|
const overrides = await promptRuleMenu({
|
|
2496
2214
|
maxFileLines: config.rules.maxFileLines,
|
|
@@ -2510,7 +2228,7 @@ async function configCommand(options, cwd) {
|
|
|
2510
2228
|
if (options.rescan && config.packages.length > 1) {
|
|
2511
2229
|
const shouldInfer = await confirm3("Re-infer boundary rules from import patterns?");
|
|
2512
2230
|
if (shouldInfer) {
|
|
2513
|
-
const bs =
|
|
2231
|
+
const bs = clack6.spinner();
|
|
2514
2232
|
bs.start("Building import graph...");
|
|
2515
2233
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
2516
2234
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -2528,29 +2246,29 @@ async function configCommand(options, cwd) {
|
|
|
2528
2246
|
}
|
|
2529
2247
|
const shouldWrite = await confirm3("Save updated configuration?");
|
|
2530
2248
|
if (!shouldWrite) {
|
|
2531
|
-
|
|
2249
|
+
clack6.outro("No changes written.");
|
|
2532
2250
|
return;
|
|
2533
2251
|
}
|
|
2534
2252
|
const compacted = (0, import_config6.compactConfig)(config);
|
|
2535
2253
|
fs10.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
2536
2254
|
`);
|
|
2537
2255
|
if (!scanResult) {
|
|
2538
|
-
const s =
|
|
2256
|
+
const s = clack6.spinner();
|
|
2539
2257
|
s.start("Scanning for context generation...");
|
|
2540
2258
|
scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
2541
2259
|
s.stop("Scan complete");
|
|
2542
2260
|
}
|
|
2543
2261
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
2544
|
-
|
|
2262
|
+
clack6.log.success(
|
|
2545
2263
|
`Updated:
|
|
2546
2264
|
${CONFIG_FILE3}
|
|
2547
2265
|
.viberails/context.md
|
|
2548
2266
|
.viberails/scan-result.json`
|
|
2549
2267
|
);
|
|
2550
|
-
|
|
2268
|
+
clack6.outro("Done! Run viberails check to verify.");
|
|
2551
2269
|
}
|
|
2552
2270
|
async function rescanAndMerge(projectRoot, config) {
|
|
2553
|
-
const s =
|
|
2271
|
+
const s = clack6.spinner();
|
|
2554
2272
|
s.start("Re-scanning project...");
|
|
2555
2273
|
const scanResult = await (0, import_scanner.scan)(projectRoot);
|
|
2556
2274
|
const merged = (0, import_config6.mergeConfig)(config, scanResult);
|
|
@@ -2561,9 +2279,9 @@ async function rescanAndMerge(projectRoot, config) {
|
|
|
2561
2279
|
const icon = c.type === "removed" ? "-" : "+";
|
|
2562
2280
|
return `${icon} ${c.description}`;
|
|
2563
2281
|
}).join("\n");
|
|
2564
|
-
|
|
2282
|
+
clack6.note(changeLines, "Changes detected");
|
|
2565
2283
|
} else {
|
|
2566
|
-
|
|
2284
|
+
clack6.log.info("No new changes detected from scan.");
|
|
2567
2285
|
}
|
|
2568
2286
|
Object.assign(config, merged);
|
|
2569
2287
|
return scanResult;
|
|
@@ -2574,10 +2292,9 @@ var fs13 = __toESM(require("fs"), 1);
|
|
|
2574
2292
|
var path13 = __toESM(require("path"), 1);
|
|
2575
2293
|
var import_config7 = require("@viberails/config");
|
|
2576
2294
|
var import_chalk8 = __toESM(require("chalk"), 1);
|
|
2577
|
-
init_prompt();
|
|
2578
2295
|
|
|
2579
2296
|
// src/commands/fix-helpers.ts
|
|
2580
|
-
var
|
|
2297
|
+
var import_node_child_process3 = require("child_process");
|
|
2581
2298
|
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
2582
2299
|
function printPlan(renames, stubs) {
|
|
2583
2300
|
if (renames.length > 0) {
|
|
@@ -2595,7 +2312,7 @@ function printPlan(renames, stubs) {
|
|
|
2595
2312
|
}
|
|
2596
2313
|
function checkGitDirty(projectRoot) {
|
|
2597
2314
|
try {
|
|
2598
|
-
const output = (0,
|
|
2315
|
+
const output = (0, import_node_child_process3.execSync)("git status --porcelain", {
|
|
2599
2316
|
cwd: projectRoot,
|
|
2600
2317
|
encoding: "utf-8",
|
|
2601
2318
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3058,161 +2775,42 @@ ${import_chalk8.default.yellow("!")} No safe fixes to apply. Resolve aliased imp
|
|
|
3058
2775
|
}
|
|
3059
2776
|
|
|
3060
2777
|
// src/commands/init.ts
|
|
3061
|
-
var
|
|
3062
|
-
var
|
|
3063
|
-
var
|
|
2778
|
+
var fs21 = __toESM(require("fs"), 1);
|
|
2779
|
+
var path21 = __toESM(require("path"), 1);
|
|
2780
|
+
var clack13 = __toESM(require("@clack/prompts"), 1);
|
|
3064
2781
|
var import_config9 = require("@viberails/config");
|
|
3065
2782
|
var import_scanner3 = require("@viberails/scanner");
|
|
3066
2783
|
var import_chalk14 = __toESM(require("chalk"), 1);
|
|
3067
2784
|
|
|
3068
|
-
// src/
|
|
3069
|
-
var
|
|
2785
|
+
// src/utils/check-prerequisites.ts
|
|
2786
|
+
var fs14 = __toESM(require("fs"), 1);
|
|
2787
|
+
var path14 = __toESM(require("path"), 1);
|
|
2788
|
+
var clack7 = __toESM(require("@clack/prompts"), 1);
|
|
3070
2789
|
var import_chalk9 = __toESM(require("chalk"), 1);
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
primaryParts.push("single package");
|
|
3092
|
-
}
|
|
3093
|
-
primaryParts.push(formatOverviewItem(stack.language));
|
|
3094
|
-
if (stack.styling) {
|
|
3095
|
-
primaryParts.push(formatOverviewItem(stack.styling, import_types6.STYLING_NAMES));
|
|
3096
|
-
}
|
|
3097
|
-
if (stack.packageManager) secondaryParts.push(formatOverviewItem(stack.packageManager));
|
|
3098
|
-
if (stack.linter) secondaryParts.push(formatOverviewItem(stack.linter));
|
|
3099
|
-
if (stack.formatter) secondaryParts.push(formatOverviewItem(stack.formatter));
|
|
3100
|
-
if (stack.testRunner) secondaryParts.push(formatOverviewItem(stack.testRunner));
|
|
3101
|
-
const primary = primaryParts.map((part) => import_chalk9.default.cyan(part)).join(import_chalk9.default.dim(" \xB7 "));
|
|
3102
|
-
const secondary = secondaryParts.join(import_chalk9.default.dim(" \xB7 "));
|
|
3103
|
-
return secondary ? `${primary}
|
|
3104
|
-
${import_chalk9.default.dim(secondary)}` : primary;
|
|
3105
|
-
}
|
|
3106
|
-
function displayInitOverview(scanResult, config, exemptedPackages) {
|
|
3107
|
-
const root = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3108
|
-
const isMonorepo = config.packages.length > 1;
|
|
3109
|
-
const ok = import_chalk9.default.green("\u2713");
|
|
3110
|
-
const info = import_chalk9.default.yellow("~");
|
|
3111
|
-
console.log("");
|
|
3112
|
-
console.log(` ${import_chalk9.default.bold("Ready to initialize:")}`);
|
|
3113
|
-
console.log(` ${formatDetectedOverview(scanResult)}`);
|
|
3114
|
-
console.log("");
|
|
3115
|
-
console.log(` ${import_chalk9.default.bold("Rules to apply:")}`);
|
|
3116
|
-
console.log(` ${ok} Max file size: ${import_chalk9.default.cyan(`${config.rules.maxFileLines} lines`)}`);
|
|
3117
|
-
const fileNaming = root?.conventions?.fileNaming ?? config.packages.find((p) => p.conventions?.fileNaming)?.conventions?.fileNaming;
|
|
3118
|
-
if (config.rules.enforceNaming && fileNaming) {
|
|
3119
|
-
console.log(` ${ok} File naming: ${import_chalk9.default.cyan(fileNaming)}`);
|
|
3120
|
-
} else {
|
|
3121
|
-
console.log(` ${info} File naming: ${import_chalk9.default.dim("not enforced")}`);
|
|
3122
|
-
}
|
|
3123
|
-
const testPattern = root?.structure?.testPattern ?? config.packages.find((p) => p.structure?.testPattern)?.structure?.testPattern;
|
|
3124
|
-
if (config.rules.enforceMissingTests && testPattern) {
|
|
3125
|
-
console.log(` ${ok} Missing tests: ${import_chalk9.default.cyan(`enforced (${testPattern})`)}`);
|
|
3126
|
-
} else if (config.rules.enforceMissingTests) {
|
|
3127
|
-
console.log(` ${ok} Missing tests: ${import_chalk9.default.cyan("enforced")}`);
|
|
3128
|
-
} else {
|
|
3129
|
-
console.log(` ${info} Missing tests: ${import_chalk9.default.dim("not enforced")}`);
|
|
3130
|
-
}
|
|
3131
|
-
if (config.rules.testCoverage > 0) {
|
|
3132
|
-
if (isMonorepo) {
|
|
3133
|
-
const withCoverage = config.packages.filter(
|
|
3134
|
-
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
3135
|
-
);
|
|
3136
|
-
console.log(
|
|
3137
|
-
` ${ok} Coverage: ${import_chalk9.default.cyan(`${config.rules.testCoverage}%`)} default ${import_chalk9.default.dim(`(${withCoverage.length}/${config.packages.length} packages)`)}`
|
|
3138
|
-
);
|
|
3139
|
-
} else {
|
|
3140
|
-
console.log(` ${ok} Coverage: ${import_chalk9.default.cyan(`${config.rules.testCoverage}%`)}`);
|
|
3141
|
-
}
|
|
3142
|
-
} else {
|
|
3143
|
-
console.log(` ${info} Coverage: ${import_chalk9.default.dim("disabled")}`);
|
|
3144
|
-
}
|
|
3145
|
-
if (exemptedPackages.length > 0) {
|
|
3146
|
-
console.log(
|
|
3147
|
-
` ${import_chalk9.default.dim(" exempted:")} ${import_chalk9.default.dim(exemptedPackages.join(", "))} ${import_chalk9.default.dim("(types-only)")}`
|
|
3148
|
-
);
|
|
3149
|
-
}
|
|
3150
|
-
console.log("");
|
|
3151
|
-
console.log(` ${import_chalk9.default.bold("Also available:")}`);
|
|
3152
|
-
if (isMonorepo) {
|
|
3153
|
-
console.log(` ${info} Infer boundaries from current imports`);
|
|
3154
|
-
}
|
|
3155
|
-
console.log(` ${info} Set up hooks, Claude integration, and CI checks`);
|
|
3156
|
-
console.log(
|
|
3157
|
-
`
|
|
3158
|
-
${import_chalk9.default.dim("Defaults warn locally. Use --enforce in CI when you want failures to block.")}`
|
|
3159
|
-
);
|
|
3160
|
-
console.log("");
|
|
3161
|
-
}
|
|
3162
|
-
function summarizeSelectedIntegrations(integrations, opts) {
|
|
3163
|
-
const lines = [];
|
|
3164
|
-
if (opts.hasBoundaries) {
|
|
3165
|
-
lines.push("\u2713 Boundary rules: inferred from current imports");
|
|
3166
|
-
} else {
|
|
3167
|
-
lines.push("~ Boundary rules: not enabled");
|
|
3168
|
-
}
|
|
3169
|
-
if (opts.hasCoverage) {
|
|
3170
|
-
lines.push("\u2713 Coverage checks: enabled");
|
|
3171
|
-
} else {
|
|
3172
|
-
lines.push("~ Coverage checks: disabled");
|
|
3173
|
-
}
|
|
3174
|
-
const selectedIntegrations = [
|
|
3175
|
-
integrations.preCommitHook ? "pre-commit hook" : void 0,
|
|
3176
|
-
integrations.typecheckHook ? "typecheck" : void 0,
|
|
3177
|
-
integrations.lintHook ? "lint check" : void 0,
|
|
3178
|
-
integrations.claudeCodeHook ? "Claude Code hook" : void 0,
|
|
3179
|
-
integrations.claudeMdRef ? "CLAUDE.md reference" : void 0,
|
|
3180
|
-
integrations.githubAction ? "GitHub Actions workflow" : void 0
|
|
3181
|
-
].filter(Boolean);
|
|
3182
|
-
if (selectedIntegrations.length > 0) {
|
|
3183
|
-
lines.push(`\u2713 Integrations: ${selectedIntegrations.join(" \xB7 ")}`);
|
|
3184
|
-
} else {
|
|
3185
|
-
lines.push("~ Integrations: none selected");
|
|
3186
|
-
}
|
|
3187
|
-
return lines;
|
|
3188
|
-
}
|
|
3189
|
-
function displaySetupPlan(config, integrations, opts = {}) {
|
|
3190
|
-
const configFile = opts.configFile ?? "viberails.config.json";
|
|
3191
|
-
const lines = summarizeSelectedIntegrations(integrations, {
|
|
3192
|
-
hasBoundaries: config.rules.enforceBoundaries,
|
|
3193
|
-
hasCoverage: config.rules.testCoverage > 0
|
|
2790
|
+
|
|
2791
|
+
// src/utils/spawn-async.ts
|
|
2792
|
+
var import_node_child_process4 = require("child_process");
|
|
2793
|
+
function spawnAsync(command, cwd) {
|
|
2794
|
+
return new Promise((resolve4) => {
|
|
2795
|
+
const child = (0, import_node_child_process4.spawn)(command, { cwd, shell: true, stdio: "pipe" });
|
|
2796
|
+
let stdout = "";
|
|
2797
|
+
let stderr = "";
|
|
2798
|
+
child.stdout.on("data", (d) => {
|
|
2799
|
+
stdout += d.toString();
|
|
2800
|
+
});
|
|
2801
|
+
child.stderr.on("data", (d) => {
|
|
2802
|
+
stderr += d.toString();
|
|
2803
|
+
});
|
|
2804
|
+
child.on("close", (status) => {
|
|
2805
|
+
resolve4({ status, stdout, stderr });
|
|
2806
|
+
});
|
|
2807
|
+
child.on("error", () => {
|
|
2808
|
+
resolve4({ status: 1, stdout, stderr });
|
|
2809
|
+
});
|
|
3194
2810
|
});
|
|
3195
|
-
console.log("");
|
|
3196
|
-
console.log(` ${import_chalk9.default.bold("Ready to write:")}`);
|
|
3197
|
-
console.log(
|
|
3198
|
-
` ${opts.replacingExistingConfig ? import_chalk9.default.yellow("!") : import_chalk9.default.green("\u2713")} ${configFile}${opts.replacingExistingConfig ? import_chalk9.default.dim(" (replacing existing config)") : ""}`
|
|
3199
|
-
);
|
|
3200
|
-
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
|
|
3201
|
-
console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
|
|
3202
|
-
for (const line of lines) {
|
|
3203
|
-
const icon = line.startsWith("\u2713") ? import_chalk9.default.green("\u2713") : import_chalk9.default.yellow("~");
|
|
3204
|
-
console.log(` ${icon} ${line.slice(2)}`);
|
|
3205
|
-
}
|
|
3206
|
-
console.log("");
|
|
3207
2811
|
}
|
|
3208
2812
|
|
|
3209
2813
|
// src/utils/check-prerequisites.ts
|
|
3210
|
-
var fs14 = __toESM(require("fs"), 1);
|
|
3211
|
-
var path14 = __toESM(require("path"), 1);
|
|
3212
|
-
var clack8 = __toESM(require("@clack/prompts"), 1);
|
|
3213
|
-
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
3214
|
-
init_prompt();
|
|
3215
|
-
init_spawn_async();
|
|
3216
2814
|
function checkCoveragePrereqs(projectRoot, scanResult) {
|
|
3217
2815
|
const pm = scanResult.stack.packageManager.name;
|
|
3218
2816
|
const vitestPackages = scanResult.packages.filter((pkg) => pkg.stack.testRunner?.name === "vitest").map((pkg) => pkg.relativePath);
|
|
@@ -3243,116 +2841,572 @@ function displayMissingPrereqs(prereqs) {
|
|
|
3243
2841
|
const missing = prereqs.filter((p) => !p.installed);
|
|
3244
2842
|
for (const m of missing) {
|
|
3245
2843
|
const suffix = m.affectedPackages ? ` \u2014 needed for coverage in: ${m.affectedPackages.join(", ")}` : ` \u2014 ${m.reason}`;
|
|
3246
|
-
console.log(` ${
|
|
2844
|
+
console.log(` ${import_chalk9.default.yellow("!")} ${m.label} not installed${suffix}`);
|
|
3247
2845
|
if (m.installCommand) {
|
|
3248
|
-
console.log(` Install: ${
|
|
2846
|
+
console.log(` Install: ${import_chalk9.default.cyan(m.installCommand)}`);
|
|
3249
2847
|
}
|
|
3250
2848
|
}
|
|
3251
2849
|
}
|
|
3252
|
-
|
|
3253
|
-
const missing = prereqs.
|
|
3254
|
-
if (missing
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
2850
|
+
function planCoverageInstall(prereqs) {
|
|
2851
|
+
const missing = prereqs.find((p) => !p.installed && p.installCommand);
|
|
2852
|
+
if (!missing?.installCommand) return void 0;
|
|
2853
|
+
return {
|
|
2854
|
+
label: missing.label,
|
|
2855
|
+
command: missing.installCommand
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
function hasDependency(projectRoot, name) {
|
|
2859
|
+
try {
|
|
2860
|
+
const pkgPath = path14.join(projectRoot, "package.json");
|
|
2861
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
2862
|
+
return !!(pkg.devDependencies?.[name] || pkg.dependencies?.[name]);
|
|
2863
|
+
} catch {
|
|
2864
|
+
return false;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/utils/deferred-install.ts
|
|
2869
|
+
var clack8 = __toESM(require("@clack/prompts"), 1);
|
|
2870
|
+
async function executeDeferredInstalls(projectRoot, installs) {
|
|
2871
|
+
if (installs.length === 0) return 0;
|
|
2872
|
+
let successCount = 0;
|
|
2873
|
+
for (const install of installs) {
|
|
2874
|
+
const s = clack8.spinner();
|
|
2875
|
+
s.start(`Installing ${install.label}...`);
|
|
2876
|
+
const result = await spawnAsync(install.command, projectRoot);
|
|
2877
|
+
if (result.status === 0) {
|
|
2878
|
+
s.stop(`Installed ${install.label}`);
|
|
2879
|
+
install.onSuccess?.();
|
|
2880
|
+
successCount++;
|
|
2881
|
+
} else {
|
|
2882
|
+
s.stop(`Failed to install ${install.label}`);
|
|
2883
|
+
clack8.log.warn(`Install manually: ${install.command}`);
|
|
2884
|
+
install.onFailure?.();
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
return successCount;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/utils/prompt-main-menu.ts
|
|
2891
|
+
var clack11 = __toESM(require("@clack/prompts"), 1);
|
|
2892
|
+
|
|
2893
|
+
// src/utils/prompt-main-menu-handlers.ts
|
|
2894
|
+
var clack10 = __toESM(require("@clack/prompts"), 1);
|
|
2895
|
+
|
|
2896
|
+
// src/utils/prompt-integrations.ts
|
|
2897
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
2898
|
+
var path15 = __toESM(require("path"), 1);
|
|
2899
|
+
var clack9 = __toESM(require("@clack/prompts"), 1);
|
|
2900
|
+
function buildLefthookInstallCommand(pm, isWorkspace) {
|
|
2901
|
+
if (pm === "yarn") return "yarn add -D lefthook";
|
|
2902
|
+
if (pm === "pnpm") return `pnpm add -D${isWorkspace ? " -w" : ""} lefthook`;
|
|
2903
|
+
if (pm === "npm") return "npm install -D lefthook";
|
|
2904
|
+
return `${pm} add -D lefthook`;
|
|
2905
|
+
}
|
|
2906
|
+
async function promptIntegrationsDeferred(hookManager, tools, packageManager, isWorkspace, projectRoot) {
|
|
2907
|
+
const options = [];
|
|
2908
|
+
const needsLefthook = !hookManager;
|
|
2909
|
+
if (needsLefthook) {
|
|
2910
|
+
const pm = packageManager ?? "npm";
|
|
2911
|
+
options.push({
|
|
2912
|
+
value: "installLefthook",
|
|
2913
|
+
label: "Install Lefthook",
|
|
2914
|
+
hint: `after final confirmation \u2014 ${buildLefthookInstallCommand(pm, isWorkspace)}`
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
const hookLabel = hookManager ? `Pre-commit hook (${hookManager})` : "Pre-commit hook";
|
|
2918
|
+
const hookHint = needsLefthook ? "uses Lefthook if installed above, otherwise local git hook" : "runs viberails checks when you commit";
|
|
2919
|
+
options.push({ value: "preCommit", label: hookLabel, hint: hookHint });
|
|
2920
|
+
if (tools?.isTypeScript) {
|
|
2921
|
+
options.push({
|
|
2922
|
+
value: "typecheck",
|
|
2923
|
+
label: "Typecheck (tsc --noEmit)",
|
|
2924
|
+
hint: "pre-commit hook + CI check"
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
if (tools?.linter) {
|
|
2928
|
+
const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
|
|
2929
|
+
options.push({
|
|
2930
|
+
value: "lint",
|
|
2931
|
+
label: `Lint check (${linterName})`,
|
|
2932
|
+
hint: "pre-commit hook + CI check"
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
options.push(
|
|
2936
|
+
{
|
|
2937
|
+
value: "claude",
|
|
2938
|
+
label: "Claude Code hook",
|
|
2939
|
+
hint: "checks files when Claude edits them"
|
|
2940
|
+
},
|
|
2941
|
+
{
|
|
2942
|
+
value: "claudeMd",
|
|
2943
|
+
label: "CLAUDE.md reference",
|
|
2944
|
+
hint: "appends @.viberails/context.md so Claude loads rules automatically"
|
|
2945
|
+
},
|
|
2946
|
+
{
|
|
2947
|
+
value: "githubAction",
|
|
2948
|
+
label: "GitHub Actions workflow",
|
|
2949
|
+
hint: "blocks PRs that fail viberails check"
|
|
2950
|
+
}
|
|
2951
|
+
);
|
|
2952
|
+
const initialValues = options.map((o) => o.value);
|
|
2953
|
+
const result = await clack9.multiselect({
|
|
2954
|
+
message: "Integrations",
|
|
2955
|
+
options,
|
|
2956
|
+
initialValues,
|
|
2957
|
+
required: false
|
|
2958
|
+
});
|
|
2959
|
+
assertNotCancelled(result);
|
|
2960
|
+
let lefthookInstall;
|
|
2961
|
+
if (needsLefthook && result.includes("installLefthook")) {
|
|
2962
|
+
const pm = packageManager ?? "npm";
|
|
2963
|
+
lefthookInstall = {
|
|
2964
|
+
label: "Lefthook",
|
|
2965
|
+
command: buildLefthookInstallCommand(pm, isWorkspace),
|
|
2966
|
+
onSuccess: projectRoot ? () => {
|
|
2967
|
+
const ymlPath = path15.join(projectRoot, "lefthook.yml");
|
|
2968
|
+
if (!fs15.existsSync(ymlPath)) {
|
|
2969
|
+
fs15.writeFileSync(ymlPath, "# Generated by viberails\n");
|
|
2970
|
+
}
|
|
2971
|
+
} : void 0
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
return {
|
|
2975
|
+
choice: {
|
|
2976
|
+
preCommitHook: result.includes("preCommit"),
|
|
2977
|
+
claudeCodeHook: result.includes("claude"),
|
|
2978
|
+
claudeMdRef: result.includes("claudeMd"),
|
|
2979
|
+
githubAction: result.includes("githubAction"),
|
|
2980
|
+
typecheckHook: result.includes("typecheck"),
|
|
2981
|
+
lintHook: result.includes("lint")
|
|
2982
|
+
},
|
|
2983
|
+
lefthookInstall
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// src/utils/prompt-main-menu-handlers.ts
|
|
2988
|
+
async function handleAdvancedNaming(config) {
|
|
2989
|
+
const rootPkg = getRootPackage(config.packages);
|
|
2990
|
+
const state = {
|
|
2991
|
+
maxFileLines: config.rules.maxFileLines,
|
|
2992
|
+
maxTestFileLines: config.rules.maxTestFileLines,
|
|
2993
|
+
testCoverage: config.rules.testCoverage,
|
|
2994
|
+
enforceMissingTests: config.rules.enforceMissingTests,
|
|
2995
|
+
enforceNaming: config.rules.enforceNaming,
|
|
2996
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
2997
|
+
componentNaming: rootPkg.conventions?.componentNaming,
|
|
2998
|
+
hookNaming: rootPkg.conventions?.hookNaming,
|
|
2999
|
+
importAlias: rootPkg.conventions?.importAlias,
|
|
3000
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3001
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3002
|
+
};
|
|
3003
|
+
await promptNamingMenu(state);
|
|
3004
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3005
|
+
config.rules.enforceNaming = state.enforceNaming;
|
|
3006
|
+
if (state.fileNamingValue) {
|
|
3007
|
+
rootPkg.conventions.fileNaming = state.fileNamingValue;
|
|
3008
|
+
} else {
|
|
3009
|
+
delete rootPkg.conventions.fileNaming;
|
|
3010
|
+
}
|
|
3011
|
+
rootPkg.conventions.componentNaming = state.componentNaming || void 0;
|
|
3012
|
+
rootPkg.conventions.hookNaming = state.hookNaming || void 0;
|
|
3013
|
+
rootPkg.conventions.importAlias = state.importAlias || void 0;
|
|
3014
|
+
}
|
|
3015
|
+
async function handleFileNaming(config, scanResult) {
|
|
3016
|
+
const isMonorepo = config.packages.length > 1;
|
|
3017
|
+
if (isMonorepo) {
|
|
3018
|
+
const pkgData = scanResult.packages.filter((p) => p.conventions.fileNaming && p.conventions.fileNaming.confidence !== "low").map((p) => ({
|
|
3019
|
+
path: p.relativePath,
|
|
3020
|
+
naming: p.conventions.fileNaming
|
|
3021
|
+
}));
|
|
3022
|
+
if (pkgData.length > 0) {
|
|
3023
|
+
const lines = pkgData.map(
|
|
3024
|
+
(p) => `${p.path}: ${p.naming.value} (${Math.round(p.naming.consistency)}%)`
|
|
3025
|
+
);
|
|
3026
|
+
clack10.note(lines.join("\n"), "Per-package file naming detected");
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
const namingOptions = FILE_NAMING_OPTIONS.map((opt) => {
|
|
3030
|
+
if (isMonorepo) {
|
|
3031
|
+
const pkgs = scanResult.packages.filter((p) => p.conventions.fileNaming?.value === opt.value);
|
|
3032
|
+
const hint = pkgs.length > 0 ? `${pkgs.length} package${pkgs.length > 1 ? "s" : ""}` : void 0;
|
|
3033
|
+
return { value: opt.value, label: opt.label, hint };
|
|
3034
|
+
}
|
|
3035
|
+
return { value: opt.value, label: opt.label };
|
|
3036
|
+
});
|
|
3037
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3038
|
+
const selected = await clack10.select({
|
|
3039
|
+
message: isMonorepo ? "Default file naming convention" : "File naming convention",
|
|
3040
|
+
options: [...namingOptions, { value: SENTINEL_SKIP, label: "Don't enforce" }],
|
|
3041
|
+
initialValue: rootPkg.conventions?.fileNaming ?? SENTINEL_SKIP
|
|
3042
|
+
});
|
|
3043
|
+
if (isCancelled(selected)) return;
|
|
3044
|
+
if (selected === SENTINEL_SKIP) {
|
|
3045
|
+
config.rules.enforceNaming = false;
|
|
3046
|
+
if (rootPkg.conventions) delete rootPkg.conventions.fileNaming;
|
|
3047
|
+
} else {
|
|
3048
|
+
config.rules.enforceNaming = true;
|
|
3049
|
+
rootPkg.conventions = rootPkg.conventions ?? {};
|
|
3050
|
+
rootPkg.conventions.fileNaming = selected;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
async function handleMissingTests(config) {
|
|
3054
|
+
const result = await clack10.confirm({
|
|
3055
|
+
message: "Require every source file to have a test file?",
|
|
3056
|
+
initialValue: config.rules.enforceMissingTests
|
|
3057
|
+
});
|
|
3058
|
+
if (isCancelled(result)) return;
|
|
3059
|
+
config.rules.enforceMissingTests = result;
|
|
3060
|
+
}
|
|
3061
|
+
async function handleCoverage(config, state, opts) {
|
|
3062
|
+
if (!opts.hasTestRunner) {
|
|
3063
|
+
clack10.note(
|
|
3064
|
+
"No test runner (vitest, jest, etc.) was detected.\nInstall one, then re-run viberails init to configure coverage.",
|
|
3065
|
+
"Coverage inactive"
|
|
3066
|
+
);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
const planned = planCoverageInstall(opts.coveragePrereqs);
|
|
3070
|
+
if (planned) {
|
|
3071
|
+
const choice = await clack10.select({
|
|
3072
|
+
message: `${planned.label} is not installed. Needed for coverage checks.`,
|
|
3268
3073
|
options: [
|
|
3269
3074
|
{
|
|
3270
3075
|
value: "install",
|
|
3271
|
-
label: "Install
|
|
3272
|
-
hint:
|
|
3273
|
-
},
|
|
3274
|
-
{
|
|
3275
|
-
value: "disable",
|
|
3276
|
-
label: "Disable coverage checks",
|
|
3277
|
-
hint: "missing-test checks still stay active"
|
|
3076
|
+
label: "Install (after final confirmation)",
|
|
3077
|
+
hint: planned.command
|
|
3278
3078
|
},
|
|
3079
|
+
{ value: "disable", label: "Disable coverage checks" },
|
|
3279
3080
|
{
|
|
3280
3081
|
value: "skip",
|
|
3281
3082
|
label: "Skip for now",
|
|
3282
|
-
hint: `install later: ${
|
|
3083
|
+
hint: `install later: ${planned.command}`
|
|
3283
3084
|
}
|
|
3284
3085
|
]
|
|
3285
3086
|
});
|
|
3286
|
-
|
|
3087
|
+
if (isCancelled(choice)) return;
|
|
3088
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => d.command !== planned.command);
|
|
3287
3089
|
if (choice === "install") {
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
is.stop(`Installed ${m.label}`);
|
|
3293
|
-
} else {
|
|
3294
|
-
is.stop(`Failed to install ${m.label}`);
|
|
3295
|
-
clack8.log.warn(
|
|
3296
|
-
`Install manually: ${m.installCommand}
|
|
3297
|
-
Coverage percentage checks will not work until the dependency is installed.`
|
|
3298
|
-
);
|
|
3299
|
-
}
|
|
3090
|
+
planned.onFailure = () => {
|
|
3091
|
+
config.rules.testCoverage = 0;
|
|
3092
|
+
};
|
|
3093
|
+
state.deferredInstalls.push(planned);
|
|
3300
3094
|
} else if (choice === "disable") {
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
} else {
|
|
3304
|
-
clack8.log.info(
|
|
3305
|
-
`Coverage percentage checks will fail until ${m.label} is installed.
|
|
3306
|
-
Install later: ${m.installCommand}`
|
|
3307
|
-
);
|
|
3095
|
+
config.rules.testCoverage = 0;
|
|
3096
|
+
return;
|
|
3308
3097
|
}
|
|
3309
3098
|
}
|
|
3310
|
-
|
|
3099
|
+
const result = await clack10.text({
|
|
3100
|
+
message: "Test coverage target (0 = disable)?",
|
|
3101
|
+
initialValue: String(config.rules.testCoverage),
|
|
3102
|
+
validate: (v) => {
|
|
3103
|
+
if (typeof v !== "string") return "Enter a number between 0 and 100";
|
|
3104
|
+
const n = Number.parseInt(v, 10);
|
|
3105
|
+
if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
if (isCancelled(result)) return;
|
|
3109
|
+
config.rules.testCoverage = Number.parseInt(result, 10);
|
|
3311
3110
|
}
|
|
3312
|
-
function
|
|
3111
|
+
async function handlePackageOverrides(config) {
|
|
3112
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3113
|
+
config.packages = await promptPackageOverrides(config.packages, {
|
|
3114
|
+
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3115
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3116
|
+
testCoverage: config.rules.testCoverage,
|
|
3117
|
+
coverageSummaryPath: rootPkg.coverage?.summaryPath ?? "coverage/coverage-summary.json",
|
|
3118
|
+
coverageCommand: config.defaults?.coverage?.command
|
|
3119
|
+
});
|
|
3120
|
+
normalizePackageOverrides(config.packages);
|
|
3121
|
+
}
|
|
3122
|
+
async function handleBoundaries(config, state, opts) {
|
|
3123
|
+
const shouldInfer = await clack10.confirm({
|
|
3124
|
+
message: "Infer boundary rules from current import patterns?",
|
|
3125
|
+
initialValue: false
|
|
3126
|
+
});
|
|
3127
|
+
if (isCancelled(shouldInfer)) return;
|
|
3128
|
+
state.visited.boundaries = true;
|
|
3129
|
+
if (!shouldInfer) {
|
|
3130
|
+
config.rules.enforceBoundaries = false;
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
const bs = clack10.spinner();
|
|
3134
|
+
bs.start("Building import graph...");
|
|
3313
3135
|
try {
|
|
3314
|
-
const
|
|
3315
|
-
const
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3136
|
+
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3137
|
+
const packages = resolveWorkspacePackages(opts.projectRoot, config.packages);
|
|
3138
|
+
const graph = await buildImportGraph(opts.projectRoot, { packages, ignore: config.ignore });
|
|
3139
|
+
const inferred = inferBoundaries(graph);
|
|
3140
|
+
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3141
|
+
if (denyCount > 0) {
|
|
3142
|
+
config.boundaries = inferred;
|
|
3143
|
+
config.rules.enforceBoundaries = true;
|
|
3144
|
+
const pkgCount = Object.keys(inferred.deny).length;
|
|
3145
|
+
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3146
|
+
} else {
|
|
3147
|
+
bs.stop("No boundary rules inferred");
|
|
3148
|
+
}
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
bs.stop("Failed to build import graph");
|
|
3151
|
+
clack10.log.warn(`Boundary inference failed: ${err instanceof Error ? err.message : err}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
async function handleIntegrations(state, opts) {
|
|
3155
|
+
const result = await promptIntegrationsDeferred(
|
|
3156
|
+
state.hookManager,
|
|
3157
|
+
opts.tools,
|
|
3158
|
+
opts.tools.packageManager,
|
|
3159
|
+
opts.tools.isWorkspace,
|
|
3160
|
+
opts.projectRoot
|
|
3161
|
+
);
|
|
3162
|
+
state.visited.integrations = true;
|
|
3163
|
+
state.integrations = result.choice;
|
|
3164
|
+
state.deferredInstalls = state.deferredInstalls.filter((d) => !d.command.includes("lefthook"));
|
|
3165
|
+
if (result.lefthookInstall) {
|
|
3166
|
+
state.deferredInstalls.push(result.lefthookInstall);
|
|
3319
3167
|
}
|
|
3320
3168
|
}
|
|
3321
3169
|
|
|
3322
|
-
// src/
|
|
3323
|
-
|
|
3170
|
+
// src/utils/prompt-main-menu-hints.ts
|
|
3171
|
+
var import_chalk10 = __toESM(require("chalk"), 1);
|
|
3172
|
+
function fileLimitsHint(config) {
|
|
3173
|
+
const max = config.rules.maxFileLines;
|
|
3174
|
+
const test = config.rules.maxTestFileLines;
|
|
3175
|
+
return test > 0 ? `${max} lines, tests ${test}` : `${max} lines`;
|
|
3176
|
+
}
|
|
3177
|
+
function fileNamingHint(config, scanResult) {
|
|
3178
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3179
|
+
const naming = rootPkg.conventions?.fileNaming;
|
|
3180
|
+
if (!config.rules.enforceNaming) return "not enforced";
|
|
3181
|
+
if (naming) {
|
|
3182
|
+
const detected = scanResult.packages.some(
|
|
3183
|
+
(p) => p.conventions.fileNaming?.value === naming && p.conventions.fileNaming.confidence === "high"
|
|
3184
|
+
);
|
|
3185
|
+
return detected ? `${naming} (detected)` : naming;
|
|
3186
|
+
}
|
|
3187
|
+
return "mixed \u2014 will not enforce if skipped";
|
|
3188
|
+
}
|
|
3189
|
+
function fileNamingStatus(config) {
|
|
3190
|
+
if (!config.rules.enforceNaming) return "disabled";
|
|
3191
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3192
|
+
return rootPkg.conventions?.fileNaming ? "ok" : "needs-input";
|
|
3193
|
+
}
|
|
3194
|
+
function missingTestsHint(config) {
|
|
3195
|
+
if (!config.rules.enforceMissingTests) return "not enforced";
|
|
3196
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3197
|
+
const pattern = rootPkg.structure?.testPattern;
|
|
3198
|
+
return pattern ? `enforced (${pattern})` : "enforced";
|
|
3199
|
+
}
|
|
3200
|
+
function coverageHint(config, hasTestRunner) {
|
|
3201
|
+
if (config.rules.testCoverage === 0) return "disabled";
|
|
3202
|
+
if (!hasTestRunner)
|
|
3203
|
+
return `${config.rules.testCoverage}% target (inactive \u2014 no test runner)`;
|
|
3204
|
+
const isMonorepo = config.packages.length > 1;
|
|
3205
|
+
if (isMonorepo) {
|
|
3206
|
+
const withCov = config.packages.filter(
|
|
3207
|
+
(p) => (p.rules?.testCoverage ?? config.rules.testCoverage) > 0
|
|
3208
|
+
);
|
|
3209
|
+
const exempt = config.packages.length - withCov.length;
|
|
3210
|
+
return exempt > 0 ? `${config.rules.testCoverage}% (${withCov.length}/${config.packages.length} packages, ${exempt} exempt)` : `${config.rules.testCoverage}%`;
|
|
3211
|
+
}
|
|
3212
|
+
return `${config.rules.testCoverage}%`;
|
|
3213
|
+
}
|
|
3214
|
+
function advancedNamingHint(config) {
|
|
3215
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3216
|
+
const parts = [];
|
|
3217
|
+
if (rootPkg.conventions?.componentNaming)
|
|
3218
|
+
parts.push(`${rootPkg.conventions.componentNaming} components`);
|
|
3219
|
+
if (rootPkg.conventions?.hookNaming) parts.push(`${rootPkg.conventions.hookNaming} hooks`);
|
|
3220
|
+
if (rootPkg.conventions?.importAlias) parts.push(rootPkg.conventions.importAlias);
|
|
3221
|
+
return parts.length > 0 ? parts.join(", ") : "component, hook, and alias conventions";
|
|
3222
|
+
}
|
|
3223
|
+
function integrationsHint(state) {
|
|
3224
|
+
if (!state.visited.integrations || !state.integrations)
|
|
3225
|
+
return "not configured \u2014 select to set up";
|
|
3226
|
+
const items = [];
|
|
3227
|
+
if (state.integrations.preCommitHook) items.push("pre-commit");
|
|
3228
|
+
if (state.integrations.typecheckHook) items.push("typecheck");
|
|
3229
|
+
if (state.integrations.lintHook) items.push("lint");
|
|
3230
|
+
if (state.integrations.claudeCodeHook) items.push("Claude");
|
|
3231
|
+
if (state.integrations.claudeMdRef) items.push("CLAUDE.md");
|
|
3232
|
+
if (state.integrations.githubAction) items.push("CI");
|
|
3233
|
+
return items.length > 0 ? items.join(" \xB7 ") : "none selected";
|
|
3234
|
+
}
|
|
3235
|
+
function packageOverridesHint(config) {
|
|
3236
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3237
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3238
|
+
const customized = editable.filter(
|
|
3239
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3240
|
+
).length;
|
|
3241
|
+
return customized > 0 ? `${editable.length} packages (${customized} customized)` : `${editable.length} packages`;
|
|
3242
|
+
}
|
|
3243
|
+
function boundariesHint(config, state) {
|
|
3244
|
+
if (!state.visited.boundaries || !config.rules.enforceBoundaries) return "not enabled";
|
|
3245
|
+
const deny = config.boundaries?.deny;
|
|
3246
|
+
if (!deny) return "enabled";
|
|
3247
|
+
const ruleCount = Object.values(deny).reduce((s, a) => s + a.length, 0);
|
|
3248
|
+
const pkgCount = Object.keys(deny).length;
|
|
3249
|
+
return `${ruleCount} rules across ${pkgCount} packages`;
|
|
3250
|
+
}
|
|
3251
|
+
function advancedNamingStatus(config) {
|
|
3252
|
+
const rootPkg = getRootPackage(config.packages);
|
|
3253
|
+
const hasAny = !!rootPkg.conventions?.componentNaming || !!rootPkg.conventions?.hookNaming || !!rootPkg.conventions?.importAlias;
|
|
3254
|
+
return hasAny ? "ok" : "unconfigured";
|
|
3255
|
+
}
|
|
3256
|
+
function packageOverridesStatus(config) {
|
|
3257
|
+
const rootNaming = getRootPackage(config.packages).conventions?.fileNaming;
|
|
3258
|
+
const editable = config.packages.filter((p) => p.path !== ".");
|
|
3259
|
+
const customized = editable.some(
|
|
3260
|
+
(p) => p.rules || p.coverage || p.conventions?.fileNaming !== void 0 && p.conventions.fileNaming !== rootNaming
|
|
3261
|
+
);
|
|
3262
|
+
return customized ? "ok" : "unconfigured";
|
|
3263
|
+
}
|
|
3264
|
+
function statusIcon(status) {
|
|
3265
|
+
if (status === "ok") return import_chalk10.default.green("\u2713");
|
|
3266
|
+
if (status === "needs-input") return import_chalk10.default.yellow("?");
|
|
3267
|
+
if (status === "unconfigured") return import_chalk10.default.dim("-");
|
|
3268
|
+
return import_chalk10.default.yellow("~");
|
|
3269
|
+
}
|
|
3270
|
+
function buildMainMenuOptions(config, scanResult, state) {
|
|
3271
|
+
const namingStatus = fileNamingStatus(config);
|
|
3272
|
+
const coverageStatus = config.rules.testCoverage === 0 ? "disabled" : !state.hasTestRunner ? "disabled" : "ok";
|
|
3273
|
+
const missingTestsStatus = config.rules.enforceMissingTests ? "ok" : "disabled";
|
|
3274
|
+
const options = [
|
|
3275
|
+
{
|
|
3276
|
+
value: "fileLimits",
|
|
3277
|
+
label: `${statusIcon("ok")} Max file size`,
|
|
3278
|
+
hint: fileLimitsHint(config)
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
value: "fileNaming",
|
|
3282
|
+
label: `${statusIcon(namingStatus)} Default file naming`,
|
|
3283
|
+
hint: fileNamingHint(config, scanResult)
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
value: "missingTests",
|
|
3287
|
+
label: `${statusIcon(missingTestsStatus)} Missing tests`,
|
|
3288
|
+
hint: missingTestsHint(config)
|
|
3289
|
+
},
|
|
3290
|
+
{
|
|
3291
|
+
value: "coverage",
|
|
3292
|
+
label: `${statusIcon(coverageStatus)} Coverage`,
|
|
3293
|
+
hint: coverageHint(config, state.hasTestRunner)
|
|
3294
|
+
},
|
|
3295
|
+
{
|
|
3296
|
+
value: "advancedNaming",
|
|
3297
|
+
label: `${statusIcon(advancedNamingStatus(config))} Advanced naming`,
|
|
3298
|
+
hint: advancedNamingHint(config)
|
|
3299
|
+
}
|
|
3300
|
+
];
|
|
3301
|
+
if (config.packages.length > 1) {
|
|
3302
|
+
const bIcon = statusIcon(
|
|
3303
|
+
state.visited.boundaries && config.rules.enforceBoundaries ? "ok" : "unconfigured"
|
|
3304
|
+
);
|
|
3305
|
+
const poIcon = statusIcon(packageOverridesStatus(config));
|
|
3306
|
+
options.push(
|
|
3307
|
+
{
|
|
3308
|
+
value: "packageOverrides",
|
|
3309
|
+
label: `${poIcon} Per-package overrides`,
|
|
3310
|
+
hint: packageOverridesHint(config)
|
|
3311
|
+
},
|
|
3312
|
+
{ value: "boundaries", label: `${bIcon} Boundaries`, hint: boundariesHint(config, state) }
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
const iIcon = state.visited.integrations ? statusIcon("ok") : statusIcon("unconfigured");
|
|
3316
|
+
options.push(
|
|
3317
|
+
{ value: "integrations", label: `${iIcon} Integrations`, hint: integrationsHint(state) },
|
|
3318
|
+
{ value: "reset", label: " Reset all to defaults" },
|
|
3319
|
+
{ value: "review", label: " Review scan details" },
|
|
3320
|
+
{ value: "done", label: " Done \u2014 write config" }
|
|
3321
|
+
);
|
|
3322
|
+
return options;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/utils/prompt-main-menu.ts
|
|
3326
|
+
async function promptMainMenu(config, scanResult, opts) {
|
|
3327
|
+
const originalConfig = structuredClone(config);
|
|
3328
|
+
const state = {
|
|
3329
|
+
visited: { integrations: false, boundaries: false },
|
|
3330
|
+
deferredInstalls: [],
|
|
3331
|
+
hasTestRunner: opts.hasTestRunner,
|
|
3332
|
+
hookManager: opts.hookManager
|
|
3333
|
+
};
|
|
3334
|
+
while (true) {
|
|
3335
|
+
const options = buildMainMenuOptions(config, scanResult, state);
|
|
3336
|
+
const choice = await clack11.select({ message: "Configure viberails", options });
|
|
3337
|
+
assertNotCancelled(choice);
|
|
3338
|
+
if (choice === "done") {
|
|
3339
|
+
if (config.rules.enforceNaming && !getRootPackage(config.packages).conventions?.fileNaming) {
|
|
3340
|
+
config.rules.enforceNaming = false;
|
|
3341
|
+
}
|
|
3342
|
+
break;
|
|
3343
|
+
}
|
|
3344
|
+
if (choice === "fileLimits") {
|
|
3345
|
+
const s = {
|
|
3346
|
+
maxFileLines: config.rules.maxFileLines,
|
|
3347
|
+
maxTestFileLines: config.rules.maxTestFileLines
|
|
3348
|
+
};
|
|
3349
|
+
await promptFileLimitsMenu(s);
|
|
3350
|
+
config.rules.maxFileLines = s.maxFileLines;
|
|
3351
|
+
config.rules.maxTestFileLines = s.maxTestFileLines;
|
|
3352
|
+
}
|
|
3353
|
+
if (choice === "fileNaming") await handleFileNaming(config, scanResult);
|
|
3354
|
+
if (choice === "missingTests") await handleMissingTests(config);
|
|
3355
|
+
if (choice === "coverage") await handleCoverage(config, state, opts);
|
|
3356
|
+
if (choice === "advancedNaming") await handleAdvancedNaming(config);
|
|
3357
|
+
if (choice === "packageOverrides") await handlePackageOverrides(config);
|
|
3358
|
+
if (choice === "boundaries") await handleBoundaries(config, state, opts);
|
|
3359
|
+
if (choice === "integrations") await handleIntegrations(state, opts);
|
|
3360
|
+
if (choice === "review") clack11.note(formatScanResultsText(scanResult), "Scan details");
|
|
3361
|
+
if (choice === "reset") {
|
|
3362
|
+
const confirmed = await clack11.confirm({
|
|
3363
|
+
message: "Reset all settings to scan-detected defaults?",
|
|
3364
|
+
initialValue: false
|
|
3365
|
+
});
|
|
3366
|
+
assertNotCancelled(confirmed);
|
|
3367
|
+
if (confirmed) {
|
|
3368
|
+
Object.assign(config, structuredClone(originalConfig));
|
|
3369
|
+
state.deferredInstalls = [];
|
|
3370
|
+
state.visited = { integrations: false, boundaries: false };
|
|
3371
|
+
state.integrations = void 0;
|
|
3372
|
+
clack11.log.info("Reset all settings to scan-detected defaults.");
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return state;
|
|
3377
|
+
}
|
|
3324
3378
|
|
|
3325
3379
|
// src/utils/update-gitignore.ts
|
|
3326
|
-
var
|
|
3327
|
-
var
|
|
3380
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
3381
|
+
var path16 = __toESM(require("path"), 1);
|
|
3328
3382
|
function updateGitignore(projectRoot) {
|
|
3329
|
-
const gitignorePath =
|
|
3383
|
+
const gitignorePath = path16.join(projectRoot, ".gitignore");
|
|
3330
3384
|
let content = "";
|
|
3331
|
-
if (
|
|
3332
|
-
content =
|
|
3385
|
+
if (fs16.existsSync(gitignorePath)) {
|
|
3386
|
+
content = fs16.readFileSync(gitignorePath, "utf-8");
|
|
3333
3387
|
}
|
|
3334
3388
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
3335
3389
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
3336
3390
|
const prefix = content.length === 0 ? "" : `${content.trimEnd()}
|
|
3337
3391
|
`;
|
|
3338
|
-
|
|
3392
|
+
fs16.writeFileSync(gitignorePath, `${prefix}${block}`);
|
|
3339
3393
|
}
|
|
3340
3394
|
}
|
|
3341
3395
|
|
|
3342
3396
|
// src/commands/init-hooks.ts
|
|
3343
|
-
var
|
|
3344
|
-
var
|
|
3397
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
3398
|
+
var path18 = __toESM(require("path"), 1);
|
|
3345
3399
|
var import_chalk11 = __toESM(require("chalk"), 1);
|
|
3346
3400
|
var import_yaml = require("yaml");
|
|
3347
3401
|
|
|
3348
3402
|
// src/commands/resolve-typecheck.ts
|
|
3349
|
-
var
|
|
3350
|
-
var
|
|
3403
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
3404
|
+
var path17 = __toESM(require("path"), 1);
|
|
3351
3405
|
function hasTurboTask(projectRoot, taskName) {
|
|
3352
|
-
const turboPath =
|
|
3353
|
-
if (!
|
|
3406
|
+
const turboPath = path17.join(projectRoot, "turbo.json");
|
|
3407
|
+
if (!fs17.existsSync(turboPath)) return false;
|
|
3354
3408
|
try {
|
|
3355
|
-
const turbo = JSON.parse(
|
|
3409
|
+
const turbo = JSON.parse(fs17.readFileSync(turboPath, "utf-8"));
|
|
3356
3410
|
const tasks = turbo.tasks ?? turbo.pipeline ?? {};
|
|
3357
3411
|
return taskName in tasks;
|
|
3358
3412
|
} catch {
|
|
@@ -3363,10 +3417,10 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3363
3417
|
if (hasTurboTask(projectRoot, "typecheck")) {
|
|
3364
3418
|
return { command: "npx turbo typecheck", label: "turbo typecheck" };
|
|
3365
3419
|
}
|
|
3366
|
-
const pkgJsonPath =
|
|
3367
|
-
if (
|
|
3420
|
+
const pkgJsonPath = path17.join(projectRoot, "package.json");
|
|
3421
|
+
if (fs17.existsSync(pkgJsonPath)) {
|
|
3368
3422
|
try {
|
|
3369
|
-
const pkg = JSON.parse(
|
|
3423
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgJsonPath, "utf-8"));
|
|
3370
3424
|
if (pkg.scripts?.typecheck) {
|
|
3371
3425
|
const pm = packageManager ?? "npm";
|
|
3372
3426
|
return { command: `${pm} run typecheck`, label: `${pm} run typecheck` };
|
|
@@ -3374,7 +3428,7 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3374
3428
|
} catch {
|
|
3375
3429
|
}
|
|
3376
3430
|
}
|
|
3377
|
-
if (
|
|
3431
|
+
if (fs17.existsSync(path17.join(projectRoot, "tsconfig.json"))) {
|
|
3378
3432
|
return { command: "npx tsc --noEmit", label: "tsc --noEmit" };
|
|
3379
3433
|
}
|
|
3380
3434
|
return {
|
|
@@ -3384,23 +3438,23 @@ function resolveTypecheckCommand(projectRoot, packageManager) {
|
|
|
3384
3438
|
|
|
3385
3439
|
// src/commands/init-hooks.ts
|
|
3386
3440
|
function setupPreCommitHook(projectRoot) {
|
|
3387
|
-
const lefthookPath =
|
|
3388
|
-
if (
|
|
3441
|
+
const lefthookPath = path18.join(projectRoot, "lefthook.yml");
|
|
3442
|
+
if (fs18.existsSync(lefthookPath)) {
|
|
3389
3443
|
addLefthookPreCommit(lefthookPath);
|
|
3390
3444
|
console.log(` ${import_chalk11.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
3391
3445
|
return "lefthook.yml";
|
|
3392
3446
|
}
|
|
3393
|
-
const huskyDir =
|
|
3394
|
-
if (
|
|
3447
|
+
const huskyDir = path18.join(projectRoot, ".husky");
|
|
3448
|
+
if (fs18.existsSync(huskyDir)) {
|
|
3395
3449
|
writeHuskyPreCommit(huskyDir);
|
|
3396
3450
|
console.log(` ${import_chalk11.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
3397
3451
|
return ".husky/pre-commit";
|
|
3398
3452
|
}
|
|
3399
|
-
const gitDir =
|
|
3400
|
-
if (
|
|
3401
|
-
const hooksDir =
|
|
3402
|
-
if (!
|
|
3403
|
-
|
|
3453
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3454
|
+
if (fs18.existsSync(gitDir)) {
|
|
3455
|
+
const hooksDir = path18.join(gitDir, "hooks");
|
|
3456
|
+
if (!fs18.existsSync(hooksDir)) {
|
|
3457
|
+
fs18.mkdirSync(hooksDir, { recursive: true });
|
|
3404
3458
|
}
|
|
3405
3459
|
writeGitHookPreCommit(hooksDir);
|
|
3406
3460
|
console.log(` ${import_chalk11.default.green("\u2713")} .git/hooks/pre-commit`);
|
|
@@ -3409,11 +3463,11 @@ function setupPreCommitHook(projectRoot) {
|
|
|
3409
3463
|
return void 0;
|
|
3410
3464
|
}
|
|
3411
3465
|
function writeGitHookPreCommit(hooksDir) {
|
|
3412
|
-
const hookPath =
|
|
3413
|
-
if (
|
|
3414
|
-
const existing =
|
|
3466
|
+
const hookPath = path18.join(hooksDir, "pre-commit");
|
|
3467
|
+
if (fs18.existsSync(hookPath)) {
|
|
3468
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3415
3469
|
if (existing.includes("viberails")) return;
|
|
3416
|
-
|
|
3470
|
+
fs18.writeFileSync(
|
|
3417
3471
|
hookPath,
|
|
3418
3472
|
`${existing.trimEnd()}
|
|
3419
3473
|
|
|
@@ -3430,10 +3484,10 @@ if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails chec
|
|
|
3430
3484
|
"if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi",
|
|
3431
3485
|
""
|
|
3432
3486
|
].join("\n");
|
|
3433
|
-
|
|
3487
|
+
fs18.writeFileSync(hookPath, script, { mode: 493 });
|
|
3434
3488
|
}
|
|
3435
3489
|
function addLefthookPreCommit(lefthookPath) {
|
|
3436
|
-
const content =
|
|
3490
|
+
const content = fs18.readFileSync(lefthookPath, "utf-8");
|
|
3437
3491
|
if (content.includes("viberails")) return;
|
|
3438
3492
|
const doc = (0, import_yaml.parse)(content) ?? {};
|
|
3439
3493
|
if (!doc["pre-commit"]) {
|
|
@@ -3445,23 +3499,23 @@ function addLefthookPreCommit(lefthookPath) {
|
|
|
3445
3499
|
doc["pre-commit"].commands.viberails = {
|
|
3446
3500
|
run: "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi"
|
|
3447
3501
|
};
|
|
3448
|
-
|
|
3502
|
+
fs18.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
|
|
3449
3503
|
}
|
|
3450
3504
|
function detectHookManager(projectRoot) {
|
|
3451
|
-
if (
|
|
3452
|
-
if (
|
|
3505
|
+
if (fs18.existsSync(path18.join(projectRoot, "lefthook.yml"))) return "Lefthook";
|
|
3506
|
+
if (fs18.existsSync(path18.join(projectRoot, ".husky"))) return "Husky";
|
|
3453
3507
|
return void 0;
|
|
3454
3508
|
}
|
|
3455
3509
|
function setupClaudeCodeHook(projectRoot) {
|
|
3456
|
-
const claudeDir =
|
|
3457
|
-
if (!
|
|
3458
|
-
|
|
3510
|
+
const claudeDir = path18.join(projectRoot, ".claude");
|
|
3511
|
+
if (!fs18.existsSync(claudeDir)) {
|
|
3512
|
+
fs18.mkdirSync(claudeDir, { recursive: true });
|
|
3459
3513
|
}
|
|
3460
|
-
const settingsPath =
|
|
3514
|
+
const settingsPath = path18.join(claudeDir, "settings.json");
|
|
3461
3515
|
let settings = {};
|
|
3462
|
-
if (
|
|
3516
|
+
if (fs18.existsSync(settingsPath)) {
|
|
3463
3517
|
try {
|
|
3464
|
-
settings = JSON.parse(
|
|
3518
|
+
settings = JSON.parse(fs18.readFileSync(settingsPath, "utf-8"));
|
|
3465
3519
|
} catch {
|
|
3466
3520
|
console.warn(
|
|
3467
3521
|
` ${import_chalk11.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
|
|
@@ -3487,30 +3541,30 @@ function setupClaudeCodeHook(projectRoot) {
|
|
|
3487
3541
|
}
|
|
3488
3542
|
];
|
|
3489
3543
|
settings.hooks = hooks;
|
|
3490
|
-
|
|
3544
|
+
fs18.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
3491
3545
|
`);
|
|
3492
3546
|
console.log(` ${import_chalk11.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
|
|
3493
3547
|
}
|
|
3494
3548
|
function setupClaudeMdReference(projectRoot) {
|
|
3495
|
-
const claudeMdPath =
|
|
3549
|
+
const claudeMdPath = path18.join(projectRoot, "CLAUDE.md");
|
|
3496
3550
|
let content = "";
|
|
3497
|
-
if (
|
|
3498
|
-
content =
|
|
3551
|
+
if (fs18.existsSync(claudeMdPath)) {
|
|
3552
|
+
content = fs18.readFileSync(claudeMdPath, "utf-8");
|
|
3499
3553
|
}
|
|
3500
3554
|
if (content.includes("@.viberails/context.md")) return;
|
|
3501
3555
|
const ref = "\n@.viberails/context.md\n";
|
|
3502
3556
|
const prefix = content.length === 0 ? "" : content.trimEnd();
|
|
3503
|
-
|
|
3557
|
+
fs18.writeFileSync(claudeMdPath, prefix + ref);
|
|
3504
3558
|
console.log(` ${import_chalk11.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
|
|
3505
3559
|
}
|
|
3506
3560
|
function setupGithubAction(projectRoot, packageManager, options) {
|
|
3507
|
-
const workflowDir =
|
|
3508
|
-
const workflowPath =
|
|
3509
|
-
if (
|
|
3510
|
-
const existing =
|
|
3561
|
+
const workflowDir = path18.join(projectRoot, ".github", "workflows");
|
|
3562
|
+
const workflowPath = path18.join(workflowDir, "viberails.yml");
|
|
3563
|
+
if (fs18.existsSync(workflowPath)) {
|
|
3564
|
+
const existing = fs18.readFileSync(workflowPath, "utf-8");
|
|
3511
3565
|
if (existing.includes("viberails")) return void 0;
|
|
3512
3566
|
}
|
|
3513
|
-
|
|
3567
|
+
fs18.mkdirSync(workflowDir, { recursive: true });
|
|
3514
3568
|
const pm = packageManager || "npm";
|
|
3515
3569
|
const installCmd = pm === "yarn" ? "yarn install --frozen-lockfile" : pm === "pnpm" ? "pnpm install --frozen-lockfile" : "npm ci";
|
|
3516
3570
|
const runPrefix = pm === "npm" ? "npx" : `${pm} exec`;
|
|
@@ -3564,74 +3618,74 @@ function setupGithubAction(projectRoot, packageManager, options) {
|
|
|
3564
3618
|
""
|
|
3565
3619
|
);
|
|
3566
3620
|
const content = lines.filter((l) => l !== void 0).join("\n");
|
|
3567
|
-
|
|
3621
|
+
fs18.writeFileSync(workflowPath, content);
|
|
3568
3622
|
return ".github/workflows/viberails.yml";
|
|
3569
3623
|
}
|
|
3570
3624
|
function writeHuskyPreCommit(huskyDir) {
|
|
3571
|
-
const hookPath =
|
|
3625
|
+
const hookPath = path18.join(huskyDir, "pre-commit");
|
|
3572
3626
|
const cmd = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --staged; else npx viberails check --staged; fi";
|
|
3573
|
-
if (
|
|
3574
|
-
const existing =
|
|
3627
|
+
if (fs18.existsSync(hookPath)) {
|
|
3628
|
+
const existing = fs18.readFileSync(hookPath, "utf-8");
|
|
3575
3629
|
if (!existing.includes("viberails")) {
|
|
3576
|
-
|
|
3630
|
+
fs18.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3577
3631
|
${cmd}
|
|
3578
3632
|
`);
|
|
3579
3633
|
}
|
|
3580
3634
|
return;
|
|
3581
3635
|
}
|
|
3582
|
-
|
|
3636
|
+
fs18.writeFileSync(hookPath, `#!/bin/sh
|
|
3583
3637
|
${cmd}
|
|
3584
3638
|
`, { mode: 493 });
|
|
3585
3639
|
}
|
|
3586
3640
|
|
|
3587
3641
|
// src/commands/init-hooks-extra.ts
|
|
3588
|
-
var
|
|
3589
|
-
var
|
|
3642
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
3643
|
+
var path19 = __toESM(require("path"), 1);
|
|
3590
3644
|
var import_chalk12 = __toESM(require("chalk"), 1);
|
|
3591
3645
|
var import_yaml2 = require("yaml");
|
|
3592
3646
|
function addPreCommitStep(projectRoot, name, command, marker, lefthookExtra) {
|
|
3593
|
-
const lefthookPath =
|
|
3594
|
-
if (
|
|
3595
|
-
const content =
|
|
3647
|
+
const lefthookPath = path19.join(projectRoot, "lefthook.yml");
|
|
3648
|
+
if (fs19.existsSync(lefthookPath)) {
|
|
3649
|
+
const content = fs19.readFileSync(lefthookPath, "utf-8");
|
|
3596
3650
|
if (content.includes(marker)) return void 0;
|
|
3597
3651
|
const doc = (0, import_yaml2.parse)(content) ?? {};
|
|
3598
3652
|
if (!doc["pre-commit"]) doc["pre-commit"] = { commands: {} };
|
|
3599
3653
|
if (!doc["pre-commit"].commands) doc["pre-commit"].commands = {};
|
|
3600
3654
|
doc["pre-commit"].commands[name] = { run: command, ...lefthookExtra };
|
|
3601
|
-
|
|
3655
|
+
fs19.writeFileSync(lefthookPath, (0, import_yaml2.stringify)(doc));
|
|
3602
3656
|
return "lefthook.yml";
|
|
3603
3657
|
}
|
|
3604
|
-
const huskyDir =
|
|
3605
|
-
if (
|
|
3606
|
-
const hookPath =
|
|
3607
|
-
if (
|
|
3608
|
-
const existing =
|
|
3658
|
+
const huskyDir = path19.join(projectRoot, ".husky");
|
|
3659
|
+
if (fs19.existsSync(huskyDir)) {
|
|
3660
|
+
const hookPath = path19.join(huskyDir, "pre-commit");
|
|
3661
|
+
if (fs19.existsSync(hookPath)) {
|
|
3662
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3609
3663
|
if (existing.includes(marker)) return void 0;
|
|
3610
|
-
|
|
3664
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3611
3665
|
${command}
|
|
3612
3666
|
`);
|
|
3613
3667
|
} else {
|
|
3614
|
-
|
|
3668
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3615
3669
|
${command}
|
|
3616
3670
|
`, { mode: 493 });
|
|
3617
3671
|
}
|
|
3618
3672
|
return ".husky/pre-commit";
|
|
3619
3673
|
}
|
|
3620
|
-
const gitDir =
|
|
3621
|
-
if (
|
|
3622
|
-
const hooksDir =
|
|
3623
|
-
if (!
|
|
3624
|
-
const hookPath =
|
|
3625
|
-
if (
|
|
3626
|
-
const existing =
|
|
3674
|
+
const gitDir = path19.join(projectRoot, ".git");
|
|
3675
|
+
if (fs19.existsSync(gitDir)) {
|
|
3676
|
+
const hooksDir = path19.join(gitDir, "hooks");
|
|
3677
|
+
if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
|
|
3678
|
+
const hookPath = path19.join(hooksDir, "pre-commit");
|
|
3679
|
+
if (fs19.existsSync(hookPath)) {
|
|
3680
|
+
const existing = fs19.readFileSync(hookPath, "utf-8");
|
|
3627
3681
|
if (existing.includes(marker)) return void 0;
|
|
3628
|
-
|
|
3682
|
+
fs19.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
3629
3683
|
|
|
3630
3684
|
# ${name}
|
|
3631
3685
|
${command}
|
|
3632
3686
|
`);
|
|
3633
3687
|
} else {
|
|
3634
|
-
|
|
3688
|
+
fs19.writeFileSync(hookPath, `#!/bin/sh
|
|
3635
3689
|
# Generated by viberails
|
|
3636
3690
|
|
|
3637
3691
|
# ${name}
|
|
@@ -3657,7 +3711,7 @@ function setupTypecheckHook(projectRoot, packageManager) {
|
|
|
3657
3711
|
return target;
|
|
3658
3712
|
}
|
|
3659
3713
|
function setupLintHook(projectRoot, linter) {
|
|
3660
|
-
const isLefthook =
|
|
3714
|
+
const isLefthook = fs19.existsSync(path19.join(projectRoot, "lefthook.yml"));
|
|
3661
3715
|
const linterName = linter === "biome" ? "Biome" : "ESLint";
|
|
3662
3716
|
let command;
|
|
3663
3717
|
let lefthookExtra;
|
|
@@ -3681,6 +3735,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3681
3735
|
const created = [];
|
|
3682
3736
|
if (integrations.preCommitHook) {
|
|
3683
3737
|
const t = setupPreCommitHook(projectRoot);
|
|
3738
|
+
if (t && opts.lefthookExpected && !t.includes("lefthook")) {
|
|
3739
|
+
console.log(` ${import_chalk12.default.yellow("!")} Lefthook install failed \u2014 fell back to ${t}`);
|
|
3740
|
+
}
|
|
3684
3741
|
created.push(t ? `${t} \u2014 added viberails pre-commit` : "pre-commit hook skipped");
|
|
3685
3742
|
}
|
|
3686
3743
|
if (integrations.typecheckHook) {
|
|
@@ -3710,9 +3767,9 @@ function setupSelectedIntegrations(projectRoot, integrations, opts) {
|
|
|
3710
3767
|
}
|
|
3711
3768
|
|
|
3712
3769
|
// src/commands/init-non-interactive.ts
|
|
3713
|
-
var
|
|
3714
|
-
var
|
|
3715
|
-
var
|
|
3770
|
+
var fs20 = __toESM(require("fs"), 1);
|
|
3771
|
+
var path20 = __toESM(require("path"), 1);
|
|
3772
|
+
var clack12 = __toESM(require("@clack/prompts"), 1);
|
|
3716
3773
|
var import_config8 = require("@viberails/config");
|
|
3717
3774
|
var import_scanner2 = require("@viberails/scanner");
|
|
3718
3775
|
var import_chalk13 = __toESM(require("chalk"), 1);
|
|
@@ -3736,7 +3793,7 @@ function getExemptedPackages(config) {
|
|
|
3736
3793
|
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3737
3794
|
}
|
|
3738
3795
|
async function initNonInteractive(projectRoot, configPath) {
|
|
3739
|
-
const s =
|
|
3796
|
+
const s = clack12.spinner();
|
|
3740
3797
|
s.start("Scanning project...");
|
|
3741
3798
|
const scanResult = await (0, import_scanner2.scan)(projectRoot);
|
|
3742
3799
|
const config = (0, import_config8.generateConfig)(scanResult);
|
|
@@ -3755,7 +3812,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3755
3812
|
);
|
|
3756
3813
|
}
|
|
3757
3814
|
if (config.packages.length > 1) {
|
|
3758
|
-
const bs =
|
|
3815
|
+
const bs = clack12.spinner();
|
|
3759
3816
|
bs.start("Building import graph...");
|
|
3760
3817
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3761
3818
|
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
@@ -3771,7 +3828,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3771
3828
|
}
|
|
3772
3829
|
}
|
|
3773
3830
|
const compacted = (0, import_config8.compactConfig)(config);
|
|
3774
|
-
|
|
3831
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3775
3832
|
`);
|
|
3776
3833
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3777
3834
|
updateGitignore(projectRoot);
|
|
@@ -3790,7 +3847,7 @@ async function initNonInteractive(projectRoot, configPath) {
|
|
|
3790
3847
|
const preCommitTarget = hasHookManager ? setupPreCommitHook(projectRoot) : void 0;
|
|
3791
3848
|
const ok = import_chalk13.default.green("\u2713");
|
|
3792
3849
|
const created = [
|
|
3793
|
-
`${ok} ${
|
|
3850
|
+
`${ok} ${path20.basename(configPath)}`,
|
|
3794
3851
|
`${ok} .viberails/context.md`,
|
|
3795
3852
|
`${ok} .viberails/scan-result.json`,
|
|
3796
3853
|
`${ok} .claude/settings.json \u2014 added viberails hook`,
|
|
@@ -3807,9 +3864,6 @@ ${created.map((f) => ` ${f}`).join("\n")}`);
|
|
|
3807
3864
|
|
|
3808
3865
|
// src/commands/init.ts
|
|
3809
3866
|
var CONFIG_FILE5 = "viberails.config.json";
|
|
3810
|
-
function getExemptedPackages2(config) {
|
|
3811
|
-
return config.packages.filter((pkg) => pkg.rules?.testCoverage === 0 && pkg.path !== ".").map((pkg) => pkg.path);
|
|
3812
|
-
}
|
|
3813
3867
|
async function initCommand(options, cwd) {
|
|
3814
3868
|
const projectRoot = findProjectRoot(cwd ?? process.cwd());
|
|
3815
3869
|
if (!projectRoot) {
|
|
@@ -3817,8 +3871,8 @@ async function initCommand(options, cwd) {
|
|
|
3817
3871
|
"No package.json found. Make sure you are inside a JS/TS project, then run:\n npx viberails"
|
|
3818
3872
|
);
|
|
3819
3873
|
}
|
|
3820
|
-
const configPath =
|
|
3821
|
-
if (
|
|
3874
|
+
const configPath = path21.join(projectRoot, CONFIG_FILE5);
|
|
3875
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3822
3876
|
if (!options.yes) {
|
|
3823
3877
|
return initInteractive(projectRoot, configPath, options);
|
|
3824
3878
|
}
|
|
@@ -3832,12 +3886,11 @@ async function initCommand(options, cwd) {
|
|
|
3832
3886
|
await initInteractive(projectRoot, configPath, options);
|
|
3833
3887
|
}
|
|
3834
3888
|
async function initInteractive(projectRoot, configPath, options) {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
const action = await promptExistingConfigAction(path20.basename(configPath));
|
|
3889
|
+
clack13.intro("viberails");
|
|
3890
|
+
if (fs21.existsSync(configPath) && !options.force) {
|
|
3891
|
+
const action = await promptExistingConfigAction(path21.basename(configPath));
|
|
3839
3892
|
if (action === "cancel") {
|
|
3840
|
-
|
|
3893
|
+
clack13.outro("Aborted. No files were written.");
|
|
3841
3894
|
return;
|
|
3842
3895
|
}
|
|
3843
3896
|
if (action === "edit") {
|
|
@@ -3846,143 +3899,93 @@ async function initInteractive(projectRoot, configPath, options) {
|
|
|
3846
3899
|
}
|
|
3847
3900
|
options.force = true;
|
|
3848
3901
|
}
|
|
3849
|
-
if (
|
|
3902
|
+
if (fs21.existsSync(configPath) && options.force) {
|
|
3850
3903
|
const replace = await confirmDangerous(
|
|
3851
|
-
`${
|
|
3904
|
+
`${path21.basename(configPath)} already exists and will be replaced. Continue?`
|
|
3852
3905
|
);
|
|
3853
3906
|
if (!replace) {
|
|
3854
|
-
|
|
3907
|
+
clack13.outro("Aborted. No files were written.");
|
|
3855
3908
|
return;
|
|
3856
3909
|
}
|
|
3857
3910
|
}
|
|
3858
|
-
const s =
|
|
3911
|
+
const s = clack13.spinner();
|
|
3859
3912
|
s.start("Scanning project...");
|
|
3860
3913
|
const scanResult = await (0, import_scanner3.scan)(projectRoot);
|
|
3861
3914
|
const config = (0, import_config9.generateConfig)(scanResult);
|
|
3862
3915
|
s.stop("Scan complete");
|
|
3863
3916
|
if (scanResult.statistics.totalFiles === 0) {
|
|
3864
|
-
|
|
3917
|
+
clack13.log.warn(
|
|
3865
3918
|
"No source files detected. Try running from the project root,\nor check that source files exist. Run viberails sync after adding files."
|
|
3866
3919
|
);
|
|
3867
3920
|
}
|
|
3868
|
-
const
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
const nextDecision = await promptInitDecision();
|
|
3873
|
-
if (nextDecision === "review") {
|
|
3874
|
-
clack11.note(formatScanResultsText(scanResult), "Detected details");
|
|
3875
|
-
continue;
|
|
3876
|
-
}
|
|
3877
|
-
decision = nextDecision;
|
|
3878
|
-
break;
|
|
3879
|
-
}
|
|
3880
|
-
if (decision === "customize") {
|
|
3881
|
-
const { resolveNamingDefault: resolveNamingDefault2 } = await Promise.resolve().then(() => (init_prompt_naming_default(), prompt_naming_default_exports));
|
|
3882
|
-
await resolveNamingDefault2(config, scanResult);
|
|
3883
|
-
const rootPkg = config.packages.find((p) => p.path === ".") ?? config.packages[0];
|
|
3884
|
-
const overrides = await promptRuleMenu({
|
|
3885
|
-
maxFileLines: config.rules.maxFileLines,
|
|
3886
|
-
maxTestFileLines: config.rules.maxTestFileLines,
|
|
3887
|
-
testCoverage: config.rules.testCoverage,
|
|
3888
|
-
enforceMissingTests: config.rules.enforceMissingTests,
|
|
3889
|
-
enforceNaming: config.rules.enforceNaming,
|
|
3890
|
-
fileNamingValue: rootPkg.conventions?.fileNaming,
|
|
3891
|
-
componentNaming: rootPkg.conventions?.componentNaming,
|
|
3892
|
-
hookNaming: rootPkg.conventions?.hookNaming,
|
|
3893
|
-
importAlias: rootPkg.conventions?.importAlias,
|
|
3894
|
-
coverageSummaryPath: "coverage/coverage-summary.json",
|
|
3895
|
-
coverageCommand: config.defaults?.coverage?.command,
|
|
3896
|
-
packageOverrides: config.packages
|
|
3897
|
-
});
|
|
3898
|
-
applyRuleOverrides(config, overrides);
|
|
3899
|
-
}
|
|
3900
|
-
if (config.packages.length > 1) {
|
|
3901
|
-
clack11.note(
|
|
3902
|
-
"Optional for monorepos. viberails can infer package boundaries\nfrom imports that already work today, so you start with rules\nthat match the current codebase.",
|
|
3903
|
-
"Boundaries"
|
|
3921
|
+
const hasTestRunner = !!scanResult.stack.testRunner;
|
|
3922
|
+
if (!hasTestRunner) {
|
|
3923
|
+
clack13.log.info(
|
|
3924
|
+
"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."
|
|
3904
3925
|
);
|
|
3905
|
-
const shouldInfer = await confirm3("Infer boundary rules from current import patterns?");
|
|
3906
|
-
if (shouldInfer) {
|
|
3907
|
-
const bs = clack11.spinner();
|
|
3908
|
-
bs.start("Building import graph...");
|
|
3909
|
-
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
3910
|
-
const packages = resolveWorkspacePackages(projectRoot, config.packages);
|
|
3911
|
-
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
3912
|
-
const inferred = inferBoundaries(graph);
|
|
3913
|
-
const denyCount = Object.values(inferred.deny).reduce((sum, arr) => sum + arr.length, 0);
|
|
3914
|
-
if (denyCount > 0) {
|
|
3915
|
-
config.boundaries = inferred;
|
|
3916
|
-
config.rules.enforceBoundaries = true;
|
|
3917
|
-
const pkgCount = Object.keys(inferred.deny).length;
|
|
3918
|
-
bs.stop(`Inferred ${denyCount} boundary rules across ${pkgCount} packages`);
|
|
3919
|
-
} else {
|
|
3920
|
-
bs.stop("No boundary rules inferred");
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
3926
|
}
|
|
3924
3927
|
const hookManager = detectHookManager(projectRoot);
|
|
3925
3928
|
const coveragePrereqs = checkCoveragePrereqs(projectRoot, scanResult);
|
|
3926
|
-
const hasMissingPrereqs = coveragePrereqs.some((p) => !p.installed) || !hookManager;
|
|
3927
|
-
if (hasMissingPrereqs) {
|
|
3928
|
-
clack11.log.info("Some dependencies are needed for full functionality.");
|
|
3929
|
-
}
|
|
3930
|
-
const prereqResult = await promptMissingPrereqs(projectRoot, coveragePrereqs);
|
|
3931
|
-
if (prereqResult.disableCoverage) {
|
|
3932
|
-
config.rules.testCoverage = 0;
|
|
3933
|
-
}
|
|
3934
3929
|
const rootPkgStack = (config.packages.find((p) => p.path === ".") ?? config.packages[0])?.stack;
|
|
3935
|
-
const
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3930
|
+
const state = await promptMainMenu(config, scanResult, {
|
|
3931
|
+
hasTestRunner,
|
|
3932
|
+
hookManager,
|
|
3933
|
+
coveragePrereqs,
|
|
3934
|
+
projectRoot,
|
|
3935
|
+
tools: {
|
|
3936
|
+
isTypeScript: rootPkgStack?.language?.split("@")[0] === "typescript",
|
|
3937
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3938
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3939
|
+
isWorkspace: config.packages.length > 1
|
|
3940
|
+
}
|
|
3944
3941
|
});
|
|
3945
3942
|
const shouldWrite = await confirm3("Apply this setup?");
|
|
3946
3943
|
if (!shouldWrite) {
|
|
3947
|
-
|
|
3944
|
+
clack13.outro("Aborted. No files were written.");
|
|
3948
3945
|
return;
|
|
3949
3946
|
}
|
|
3950
|
-
|
|
3947
|
+
if (state.deferredInstalls.length > 0) {
|
|
3948
|
+
await executeDeferredInstalls(projectRoot, state.deferredInstalls);
|
|
3949
|
+
}
|
|
3950
|
+
const ws = clack13.spinner();
|
|
3951
3951
|
ws.start("Writing configuration...");
|
|
3952
3952
|
const compacted = (0, import_config9.compactConfig)(config);
|
|
3953
|
-
|
|
3953
|
+
fs21.writeFileSync(configPath, `${JSON.stringify(compacted, null, 2)}
|
|
3954
3954
|
`);
|
|
3955
3955
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
3956
3956
|
updateGitignore(projectRoot);
|
|
3957
3957
|
ws.stop("Configuration written");
|
|
3958
3958
|
const ok = import_chalk14.default.green("\u2713");
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3959
|
+
clack13.log.step(`${ok} ${path21.basename(configPath)}`);
|
|
3960
|
+
clack13.log.step(`${ok} .viberails/context.md`);
|
|
3961
|
+
clack13.log.step(`${ok} .viberails/scan-result.json`);
|
|
3962
|
+
if (state.visited.integrations && state.integrations) {
|
|
3963
|
+
const lefthookExpected = state.deferredInstalls.some((d) => d.command.includes("lefthook"));
|
|
3964
|
+
setupSelectedIntegrations(projectRoot, state.integrations, {
|
|
3965
|
+
linter: rootPkgStack?.linter?.split("@")[0],
|
|
3966
|
+
packageManager: rootPkgStack?.packageManager?.split("@")[0],
|
|
3967
|
+
lefthookExpected
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
clack13.outro(
|
|
3967
3971
|
`Done! Next: review viberails.config.json, then run viberails check
|
|
3968
3972
|
${import_chalk14.default.dim("Tip: use")} ${import_chalk14.default.cyan("viberails check --enforce")} ${import_chalk14.default.dim("in CI to block PRs on violations.")}`
|
|
3969
3973
|
);
|
|
3970
3974
|
}
|
|
3971
3975
|
|
|
3972
3976
|
// src/commands/sync.ts
|
|
3973
|
-
var
|
|
3974
|
-
var
|
|
3975
|
-
var
|
|
3977
|
+
var fs22 = __toESM(require("fs"), 1);
|
|
3978
|
+
var path22 = __toESM(require("path"), 1);
|
|
3979
|
+
var clack14 = __toESM(require("@clack/prompts"), 1);
|
|
3976
3980
|
var import_config11 = require("@viberails/config");
|
|
3977
3981
|
var import_scanner4 = require("@viberails/scanner");
|
|
3978
3982
|
var import_chalk15 = __toESM(require("chalk"), 1);
|
|
3979
|
-
init_prompt();
|
|
3980
3983
|
var CONFIG_FILE6 = "viberails.config.json";
|
|
3981
3984
|
var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
|
|
3982
3985
|
function loadPreviousStats(projectRoot) {
|
|
3983
|
-
const scanResultPath =
|
|
3986
|
+
const scanResultPath = path22.join(projectRoot, SCAN_RESULT_FILE2);
|
|
3984
3987
|
try {
|
|
3985
|
-
const raw =
|
|
3988
|
+
const raw = fs22.readFileSync(scanResultPath, "utf-8");
|
|
3986
3989
|
const parsed = JSON.parse(raw);
|
|
3987
3990
|
if (parsed?.statistics?.totalFiles !== void 0) {
|
|
3988
3991
|
return parsed.statistics;
|
|
@@ -3999,17 +4002,17 @@ async function syncCommand(options, cwd) {
|
|
|
3999
4002
|
"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"
|
|
4000
4003
|
);
|
|
4001
4004
|
}
|
|
4002
|
-
const configPath =
|
|
4005
|
+
const configPath = path22.join(projectRoot, CONFIG_FILE6);
|
|
4003
4006
|
const existing = await (0, import_config11.loadConfig)(configPath);
|
|
4004
4007
|
const previousStats = loadPreviousStats(projectRoot);
|
|
4005
|
-
const s =
|
|
4008
|
+
const s = clack14.spinner();
|
|
4006
4009
|
s.start("Scanning project...");
|
|
4007
4010
|
const scanResult = await (0, import_scanner4.scan)(projectRoot);
|
|
4008
4011
|
s.stop("Scan complete");
|
|
4009
4012
|
const merged = (0, import_config11.mergeConfig)(existing, scanResult);
|
|
4010
4013
|
const compacted = (0, import_config11.compactConfig)(merged);
|
|
4011
4014
|
const compactedJson = JSON.stringify(compacted, null, 2);
|
|
4012
|
-
const rawDisk =
|
|
4015
|
+
const rawDisk = fs22.readFileSync(configPath, "utf-8").trim();
|
|
4013
4016
|
const diskWithoutSync = rawDisk.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
4014
4017
|
const mergedWithoutSync = compactedJson.replace(/"lastSync":\s*"[^"]*"/, '"lastSync": ""');
|
|
4015
4018
|
const configChanged = diskWithoutSync !== mergedWithoutSync;
|
|
@@ -4027,9 +4030,9 @@ ${import_chalk15.default.bold("Changes:")}`);
|
|
|
4027
4030
|
}
|
|
4028
4031
|
}
|
|
4029
4032
|
if (options?.interactive) {
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
const decision = await
|
|
4033
|
+
clack14.intro("viberails sync (interactive)");
|
|
4034
|
+
clack14.note(formatRulesText(merged).join("\n"), "Rules after sync");
|
|
4035
|
+
const decision = await clack14.select({
|
|
4033
4036
|
message: "How would you like to proceed?",
|
|
4034
4037
|
options: [
|
|
4035
4038
|
{ value: "accept", label: "Accept changes" },
|
|
@@ -4039,7 +4042,7 @@ ${import_chalk15.default.bold("Changes:")}`);
|
|
|
4039
4042
|
});
|
|
4040
4043
|
assertNotCancelled(decision);
|
|
4041
4044
|
if (decision === "cancel") {
|
|
4042
|
-
|
|
4045
|
+
clack14.outro("Sync cancelled. No files were written.");
|
|
4043
4046
|
return;
|
|
4044
4047
|
}
|
|
4045
4048
|
if (decision === "customize") {
|
|
@@ -4060,15 +4063,15 @@ ${import_chalk15.default.bold("Changes:")}`);
|
|
|
4060
4063
|
});
|
|
4061
4064
|
applyRuleOverrides(merged, overrides);
|
|
4062
4065
|
const recompacted = (0, import_config11.compactConfig)(merged);
|
|
4063
|
-
|
|
4066
|
+
fs22.writeFileSync(configPath, `${JSON.stringify(recompacted, null, 2)}
|
|
4064
4067
|
`);
|
|
4065
4068
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
4066
|
-
|
|
4067
|
-
|
|
4069
|
+
clack14.log.success("Updated config with your customizations.");
|
|
4070
|
+
clack14.outro("Done! Run viberails check to verify.");
|
|
4068
4071
|
return;
|
|
4069
4072
|
}
|
|
4070
4073
|
}
|
|
4071
|
-
|
|
4074
|
+
fs22.writeFileSync(configPath, `${compactedJson}
|
|
4072
4075
|
`);
|
|
4073
4076
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
4074
4077
|
console.log(`
|
|
@@ -4083,7 +4086,7 @@ ${import_chalk15.default.bold("Synced:")}`);
|
|
|
4083
4086
|
}
|
|
4084
4087
|
|
|
4085
4088
|
// src/index.ts
|
|
4086
|
-
var VERSION = "0.6.
|
|
4089
|
+
var VERSION = "0.6.7";
|
|
4087
4090
|
var program = new import_commander.Command();
|
|
4088
4091
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
4089
4092
|
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) => {
|