viberails 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,821 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/get-root-package.ts
4
+ function getRootPackage(packages) {
5
+ return packages.find((pkg) => pkg.path === ".") ?? packages[0];
6
+ }
7
+
8
+ // src/utils/prompt-constants.ts
9
+ var SENTINEL_DONE = "__done__";
10
+ var SENTINEL_CLEAR = "__clear__";
11
+ var SENTINEL_CUSTOM = "__custom__";
12
+ var SENTINEL_NONE = "__none__";
13
+ var SENTINEL_INHERIT = "__inherit__";
14
+ var SENTINEL_SKIP = "__skip__";
15
+
16
+ // src/utils/prompt-submenus.ts
17
+ import * as clack6 from "@clack/prompts";
18
+
19
+ // src/utils/prompt.ts
20
+ import * as clack5 from "@clack/prompts";
21
+
22
+ // src/utils/prompt-integrations.ts
23
+ import * as clack from "@clack/prompts";
24
+
25
+ // src/utils/spawn-async.ts
26
+ import { spawn } from "child_process";
27
+ function spawnAsync(command, cwd) {
28
+ return new Promise((resolve) => {
29
+ const child = spawn(command, { cwd, shell: true, stdio: "pipe" });
30
+ let stdout = "";
31
+ let stderr = "";
32
+ child.stdout.on("data", (d) => {
33
+ stdout += d.toString();
34
+ });
35
+ child.stderr.on("data", (d) => {
36
+ stderr += d.toString();
37
+ });
38
+ child.on("close", (status) => {
39
+ resolve({ status, stdout, stderr });
40
+ });
41
+ child.on("error", () => {
42
+ resolve({ status: 1, stdout, stderr });
43
+ });
44
+ });
45
+ }
46
+
47
+ // src/utils/prompt-integrations.ts
48
+ async function promptHookManagerInstall(projectRoot, packageManager, isWorkspace) {
49
+ const choice = await clack.select({
50
+ message: "No shared git hook manager detected. Install Lefthook?",
51
+ options: [
52
+ {
53
+ value: "install",
54
+ label: "Yes, install Lefthook",
55
+ hint: "recommended \u2014 hooks are committed to the repo and shared with your team"
56
+ },
57
+ {
58
+ value: "skip",
59
+ label: "No, skip",
60
+ hint: "pre-commit hooks will be local-only (.git/hooks) and not shared"
61
+ }
62
+ ]
63
+ });
64
+ assertNotCancelled(choice);
65
+ if (choice !== "install") return void 0;
66
+ const pm = packageManager || "npm";
67
+ const installCmd = pm === "yarn" ? "yarn add -D lefthook" : pm === "pnpm" ? `pnpm add -D${isWorkspace ? " -w" : ""} lefthook` : "npm install -D lefthook";
68
+ const s = clack.spinner();
69
+ s.start("Installing Lefthook...");
70
+ const result = await spawnAsync(installCmd, projectRoot);
71
+ if (result.status === 0) {
72
+ const fs = await import("fs");
73
+ const path = await import("path");
74
+ const lefthookPath = path.join(projectRoot, "lefthook.yml");
75
+ if (!fs.existsSync(lefthookPath)) {
76
+ fs.writeFileSync(lefthookPath, "# Managed by viberails \u2014 https://viberails.sh\n");
77
+ }
78
+ s.stop("Installed Lefthook");
79
+ return "Lefthook";
80
+ }
81
+ s.stop("Failed to install Lefthook");
82
+ clack.log.warn(`Install manually: ${installCmd}`);
83
+ return void 0;
84
+ }
85
+ async function promptIntegrations(projectRoot, hookManager, tools) {
86
+ let resolvedHookManager = hookManager;
87
+ if (!resolvedHookManager) {
88
+ resolvedHookManager = await promptHookManagerInstall(
89
+ projectRoot,
90
+ tools?.packageManager ?? "npm",
91
+ tools?.isWorkspace
92
+ );
93
+ }
94
+ const isBareHook = !resolvedHookManager;
95
+ const hookLabel = resolvedHookManager ? `Pre-commit hook (${resolvedHookManager})` : "Pre-commit hook (git hook \u2014 local only)";
96
+ const hookHint = isBareHook ? "local only \u2014 will NOT be committed or shared with collaborators" : "runs viberails checks when you commit";
97
+ const options = [
98
+ {
99
+ value: "preCommit",
100
+ label: hookLabel,
101
+ hint: hookHint
102
+ }
103
+ ];
104
+ if (tools?.isTypeScript) {
105
+ options.push({
106
+ value: "typecheck",
107
+ label: "Typecheck (tsc --noEmit)",
108
+ hint: "pre-commit hook + CI check"
109
+ });
110
+ }
111
+ if (tools?.linter) {
112
+ const linterName = tools.linter === "biome" ? "Biome" : "ESLint";
113
+ options.push({
114
+ value: "lint",
115
+ label: `Lint check (${linterName})`,
116
+ hint: "pre-commit hook + CI check"
117
+ });
118
+ }
119
+ options.push(
120
+ {
121
+ value: "claude",
122
+ label: "Claude Code hook",
123
+ hint: "checks files when Claude edits them"
124
+ },
125
+ {
126
+ value: "claudeMd",
127
+ label: "CLAUDE.md reference",
128
+ hint: "appends @.viberails/context.md so Claude loads rules automatically"
129
+ },
130
+ {
131
+ value: "githubAction",
132
+ label: "GitHub Actions workflow",
133
+ hint: "blocks PRs that fail viberails check"
134
+ }
135
+ );
136
+ const initialValues = isBareHook ? options.filter((o) => o.value !== "preCommit").map((o) => o.value) : options.map((o) => o.value);
137
+ const result = await clack.multiselect({
138
+ message: "Optional integrations",
139
+ options,
140
+ initialValues,
141
+ required: false
142
+ });
143
+ assertNotCancelled(result);
144
+ return {
145
+ preCommitHook: result.includes("preCommit"),
146
+ claudeCodeHook: result.includes("claude"),
147
+ claudeMdRef: result.includes("claudeMd"),
148
+ githubAction: result.includes("githubAction"),
149
+ typecheckHook: result.includes("typecheck"),
150
+ lintHook: result.includes("lint")
151
+ };
152
+ }
153
+
154
+ // src/utils/prompt-rules.ts
155
+ import * as clack4 from "@clack/prompts";
156
+
157
+ // src/utils/prompt-menu-handlers.ts
158
+ import * as clack3 from "@clack/prompts";
159
+
160
+ // src/utils/prompt-package-overrides.ts
161
+ import * as clack2 from "@clack/prompts";
162
+ function normalizePackageOverrides(packages) {
163
+ for (const pkg of packages) {
164
+ if (pkg.rules && Object.keys(pkg.rules).length === 0) {
165
+ delete pkg.rules;
166
+ }
167
+ if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
168
+ delete pkg.coverage;
169
+ }
170
+ if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
171
+ delete pkg.conventions;
172
+ }
173
+ }
174
+ return packages;
175
+ }
176
+ function packageOverrideHint(pkg, defaults) {
177
+ const tags = [];
178
+ if (pkg.conventions?.fileNaming && pkg.conventions.fileNaming !== defaults.fileNamingValue) {
179
+ tags.push(pkg.conventions.fileNaming);
180
+ }
181
+ if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== defaults.maxFileLines && pkg.rules.maxFileLines > 0) {
182
+ tags.push(`${pkg.rules.maxFileLines} lines`);
183
+ }
184
+ const coverage = pkg.rules?.testCoverage ?? defaults.testCoverage;
185
+ const isExempt = coverage === 0;
186
+ const nameSegments = pkg.name.replace(/^@[^/]+\//, "").split(/[-/]/);
187
+ const isTypesOnly = isExempt && nameSegments.some((s) => s === "types");
188
+ if (isExempt) {
189
+ tags.push(isTypesOnly ? "exempt (types-only)" : "exempt");
190
+ } else if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== defaults.testCoverage) {
191
+ tags.push(`${coverage}%`);
192
+ }
193
+ const hasSummaryOverride = pkg.coverage?.summaryPath !== void 0 && pkg.coverage.summaryPath !== defaults.coverageSummaryPath;
194
+ const defaultCommand = defaults.coverageCommand ?? "";
195
+ const hasCommandOverride = pkg.coverage?.command !== void 0 && pkg.coverage.command !== defaultCommand;
196
+ if (hasSummaryOverride) tags.push("summary override");
197
+ if (hasCommandOverride) tags.push("command override");
198
+ return tags.length > 0 ? tags.join(", ") : "(no overrides)";
199
+ }
200
+ async function promptPackageOverrides(packages, defaults) {
201
+ const editablePackages = packages.filter((pkg) => pkg.path !== ".");
202
+ if (editablePackages.length === 0) return packages;
203
+ while (true) {
204
+ const selectedPath = await clack2.select({
205
+ message: "Select package to edit overrides",
206
+ options: [
207
+ ...editablePackages.map((pkg) => ({
208
+ value: pkg.path,
209
+ label: `${pkg.path} (${pkg.name})`,
210
+ hint: packageOverrideHint(pkg, defaults)
211
+ })),
212
+ { value: SENTINEL_DONE, label: "Done" }
213
+ ]
214
+ });
215
+ assertNotCancelled(selectedPath);
216
+ if (selectedPath === SENTINEL_DONE) break;
217
+ const target = editablePackages.find((pkg) => pkg.path === selectedPath);
218
+ if (!target) continue;
219
+ await promptSinglePackageOverrides(target, defaults);
220
+ normalizePackageOverrides(editablePackages);
221
+ }
222
+ return normalizePackageOverrides(packages);
223
+ }
224
+ async function promptSinglePackageOverrides(target, defaults) {
225
+ while (true) {
226
+ const effectiveNaming = target.conventions?.fileNaming ?? defaults.fileNamingValue;
227
+ const effectiveMaxLines = target.rules?.maxFileLines ?? defaults.maxFileLines;
228
+ const effectiveCoverage = target.rules?.testCoverage ?? defaults.testCoverage;
229
+ const effectiveSummary = target.coverage?.summaryPath ?? defaults.coverageSummaryPath;
230
+ const effectiveCommand = target.coverage?.command ?? defaults.coverageCommand ?? "(auto-detect)";
231
+ const hasNamingOverride = target.conventions?.fileNaming !== void 0 && target.conventions.fileNaming !== defaults.fileNamingValue;
232
+ const hasMaxLinesOverride = target.rules?.maxFileLines !== void 0 && target.rules.maxFileLines !== defaults.maxFileLines;
233
+ const namingHint = hasNamingOverride ? String(effectiveNaming) : `(inherits: ${effectiveNaming ?? "not set"})`;
234
+ const maxLinesHint = hasMaxLinesOverride ? String(effectiveMaxLines) : `(inherits: ${effectiveMaxLines})`;
235
+ const choice = await clack2.select({
236
+ message: `Edit overrides for ${target.path}`,
237
+ options: [
238
+ { value: "fileNaming", label: "File naming", hint: namingHint },
239
+ { value: "maxFileLines", label: "Max file lines", hint: maxLinesHint },
240
+ { value: "testCoverage", label: "Test coverage", hint: String(effectiveCoverage) },
241
+ { value: "summaryPath", label: "Coverage summary path", hint: effectiveSummary },
242
+ { value: "command", label: "Coverage command", hint: effectiveCommand },
243
+ { value: "reset", label: "Reset all overrides for this package" },
244
+ { value: "back", label: "Back to package list" }
245
+ ]
246
+ });
247
+ assertNotCancelled(choice);
248
+ if (choice === "back") break;
249
+ if (choice === "fileNaming") {
250
+ const selected = await clack2.select({
251
+ message: `File naming for ${target.path}`,
252
+ options: [
253
+ ...FILE_NAMING_OPTIONS,
254
+ { value: SENTINEL_NONE, label: "(none \u2014 exempt from checks)" },
255
+ {
256
+ value: SENTINEL_INHERIT,
257
+ label: `Inherit default${defaults.fileNamingValue ? ` (${defaults.fileNamingValue})` : ""}`
258
+ }
259
+ ],
260
+ initialValue: target.conventions?.fileNaming ?? SENTINEL_INHERIT
261
+ });
262
+ assertNotCancelled(selected);
263
+ if (selected === SENTINEL_INHERIT) {
264
+ if (target.conventions) delete target.conventions.fileNaming;
265
+ } else if (selected === SENTINEL_NONE) {
266
+ target.conventions = { ...target.conventions ?? {}, fileNaming: "" };
267
+ } else {
268
+ target.conventions = { ...target.conventions ?? {}, fileNaming: selected };
269
+ }
270
+ }
271
+ if (choice === "maxFileLines") {
272
+ const result = await clack2.text({
273
+ message: `Max file lines for ${target.path} (blank to inherit default)?`,
274
+ initialValue: target.rules?.maxFileLines !== void 0 ? String(target.rules.maxFileLines) : "",
275
+ placeholder: String(defaults.maxFileLines)
276
+ });
277
+ assertNotCancelled(result);
278
+ const value = result.trim();
279
+ if (value.length === 0 || Number.parseInt(value, 10) === defaults.maxFileLines) {
280
+ if (target.rules) delete target.rules.maxFileLines;
281
+ } else {
282
+ target.rules = { ...target.rules ?? {}, maxFileLines: Number.parseInt(value, 10) };
283
+ }
284
+ }
285
+ if (choice === "testCoverage") {
286
+ const result = await clack2.text({
287
+ message: "Package testCoverage (0 to exempt package)?",
288
+ initialValue: String(effectiveCoverage),
289
+ validate: (v) => {
290
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
291
+ const n = Number.parseInt(v, 10);
292
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
293
+ }
294
+ });
295
+ assertNotCancelled(result);
296
+ const nextCoverage = Number.parseInt(result, 10);
297
+ if (nextCoverage === defaults.testCoverage) {
298
+ if (target.rules) delete target.rules.testCoverage;
299
+ } else {
300
+ target.rules = { ...target.rules ?? {}, testCoverage: nextCoverage };
301
+ }
302
+ }
303
+ if (choice === "summaryPath") {
304
+ const result = await clack2.text({
305
+ message: "Path to coverage summary file (blank to inherit default)?",
306
+ initialValue: target.coverage?.summaryPath !== void 0 ? target.coverage.summaryPath : "",
307
+ placeholder: defaults.coverageSummaryPath
308
+ });
309
+ assertNotCancelled(result);
310
+ const value = result.trim();
311
+ if (value.length === 0 || value === defaults.coverageSummaryPath) {
312
+ if (target.coverage) delete target.coverage.summaryPath;
313
+ } else {
314
+ target.coverage = { ...target.coverage ?? {}, summaryPath: value };
315
+ }
316
+ }
317
+ if (choice === "command") {
318
+ const result = await clack2.text({
319
+ message: "Coverage command (blank to auto-detect)?",
320
+ initialValue: target.coverage?.command !== void 0 ? target.coverage.command : "",
321
+ placeholder: defaults.coverageCommand ?? "(auto-detect from package.json test runner)"
322
+ });
323
+ assertNotCancelled(result);
324
+ const value = result.trim();
325
+ const defaultCommand = defaults.coverageCommand ?? "";
326
+ if (value.length === 0 || value === defaultCommand) {
327
+ if (target.coverage) delete target.coverage.command;
328
+ } else {
329
+ target.coverage = { ...target.coverage ?? {}, command: value };
330
+ }
331
+ }
332
+ if (choice === "reset") {
333
+ if (target.rules) {
334
+ delete target.rules.testCoverage;
335
+ delete target.rules.maxFileLines;
336
+ }
337
+ delete target.coverage;
338
+ delete target.conventions;
339
+ }
340
+ }
341
+ }
342
+
343
+ // src/utils/prompt-menu-handlers.ts
344
+ function getPackageDiffs(pkg, root) {
345
+ const diffs = [];
346
+ const convKeys = ["fileNaming", "componentNaming", "hookNaming", "importAlias"];
347
+ for (const key of convKeys) {
348
+ if (pkg.conventions?.[key] && pkg.conventions[key] !== root.conventions?.[key]) {
349
+ diffs.push(`${key}: ${pkg.conventions[key]}`);
350
+ }
351
+ }
352
+ const stackKeys = [
353
+ "framework",
354
+ "language",
355
+ "styling",
356
+ "backend",
357
+ "orm",
358
+ "linter",
359
+ "formatter",
360
+ "testRunner",
361
+ "packageManager"
362
+ ];
363
+ for (const key of stackKeys) {
364
+ if (pkg.stack?.[key] && pkg.stack[key] !== root.stack?.[key]) {
365
+ diffs.push(`${key}: ${pkg.stack[key]}`);
366
+ }
367
+ }
368
+ if (pkg.rules?.maxFileLines !== void 0 && pkg.rules.maxFileLines !== root.rules?.maxFileLines && pkg.rules.maxFileLines > 0) {
369
+ diffs.push(`maxFileLines: ${pkg.rules.maxFileLines}`);
370
+ }
371
+ if (pkg.rules?.testCoverage !== void 0 && pkg.rules.testCoverage !== root.rules?.testCoverage && pkg.rules.testCoverage >= 0) {
372
+ diffs.push(`testCoverage: ${pkg.rules.testCoverage}`);
373
+ }
374
+ if (pkg.coverage?.summaryPath && pkg.coverage.summaryPath !== root.coverage?.summaryPath) {
375
+ diffs.push(`coverage.summaryPath: ${pkg.coverage.summaryPath}`);
376
+ }
377
+ if (pkg.coverage?.command && pkg.coverage.command !== root.coverage?.command) {
378
+ diffs.push("coverage.command: (override)");
379
+ }
380
+ return diffs;
381
+ }
382
+ function buildMenuOptions(state, packageCount) {
383
+ const fileLimitsHint = state.maxTestFileLines > 0 ? `max ${state.maxFileLines} lines, tests ${state.maxTestFileLines}` : `max ${state.maxFileLines} lines, test files unlimited`;
384
+ const namingHint = state.enforceNaming ? `${state.fileNamingValue ?? "not set"} (enforced)` : "not enforced";
385
+ const testingHint = state.testCoverage > 0 ? `${state.testCoverage}% coverage, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}` : `coverage disabled, missing tests ${state.enforceMissingTests ? "enforced" : "not enforced"}`;
386
+ const options = [
387
+ { value: "fileLimits", label: "File limits", hint: fileLimitsHint },
388
+ { value: "naming", label: "Naming & conventions", hint: namingHint },
389
+ { value: "testing", label: "Testing & coverage", hint: testingHint }
390
+ ];
391
+ if (packageCount > 0) {
392
+ options.push({
393
+ value: "packageOverrides",
394
+ label: "Per-package overrides",
395
+ hint: `${packageCount} package${packageCount > 1 ? "s" : ""} configurable`
396
+ });
397
+ }
398
+ options.push(
399
+ { value: "reset", label: "Reset all to detected defaults" },
400
+ { value: "done", label: "Done" }
401
+ );
402
+ return options;
403
+ }
404
+ function clonePackages(packages) {
405
+ return packages ? structuredClone(packages) : void 0;
406
+ }
407
+ async function handleMenuChoice(choice, state, defaults, root) {
408
+ if (choice === "reset") {
409
+ state.maxFileLines = defaults.maxFileLines;
410
+ state.maxTestFileLines = defaults.maxTestFileLines;
411
+ state.testCoverage = defaults.testCoverage;
412
+ state.enforceMissingTests = defaults.enforceMissingTests;
413
+ state.enforceNaming = defaults.enforceNaming;
414
+ state.fileNamingValue = defaults.fileNamingValue;
415
+ state.componentNaming = defaults.componentNaming;
416
+ state.hookNaming = defaults.hookNaming;
417
+ state.importAlias = defaults.importAlias;
418
+ state.coverageSummaryPath = defaults.coverageSummaryPath;
419
+ state.coverageCommand = defaults.coverageCommand;
420
+ state.packageOverrides = clonePackages(defaults.packageOverrides);
421
+ clack3.log.info("Reset all rules to detected defaults.");
422
+ return;
423
+ }
424
+ if (choice === "fileLimits") {
425
+ await promptFileLimitsMenu(state);
426
+ return;
427
+ }
428
+ if (choice === "naming") {
429
+ await promptNamingMenu(state);
430
+ return;
431
+ }
432
+ if (choice === "testing") {
433
+ await promptTestingMenu(state);
434
+ return;
435
+ }
436
+ if (choice === "packageOverrides") {
437
+ if (state.packageOverrides) {
438
+ const packageDiffs = root ? state.packageOverrides.filter((pkg) => pkg.path !== root.path).map((pkg) => ({ pkg, diffs: getPackageDiffs(pkg, root) })).filter((entry) => entry.diffs.length > 0) : [];
439
+ state.packageOverrides = await promptPackageOverrides(state.packageOverrides, {
440
+ fileNamingValue: state.fileNamingValue,
441
+ maxFileLines: state.maxFileLines,
442
+ testCoverage: state.testCoverage,
443
+ coverageSummaryPath: state.coverageSummaryPath,
444
+ coverageCommand: state.coverageCommand
445
+ });
446
+ const lines = packageDiffs.map((entry) => `${entry.pkg.path}
447
+ ${entry.diffs.join(", ")}`);
448
+ if (lines.length > 0) {
449
+ clack3.note(lines.join("\n\n"), "Existing package differences");
450
+ }
451
+ }
452
+ return;
453
+ }
454
+ }
455
+
456
+ // src/utils/prompt-rules.ts
457
+ async function promptRuleMenu(defaults) {
458
+ const state = {
459
+ ...defaults,
460
+ packageOverrides: clonePackages(defaults.packageOverrides)
461
+ };
462
+ const root = state.packageOverrides && state.packageOverrides.length > 0 ? getRootPackage(state.packageOverrides) : void 0;
463
+ const packageCount = state.packageOverrides?.filter((pkg) => pkg.path !== ".").length ?? 0;
464
+ while (true) {
465
+ const options = buildMenuOptions(state, packageCount);
466
+ const choice = await clack4.select({ message: "Customize rules", options });
467
+ assertNotCancelled(choice);
468
+ if (choice === "done") break;
469
+ await handleMenuChoice(choice, state, defaults, root);
470
+ }
471
+ return {
472
+ maxFileLines: state.maxFileLines,
473
+ maxTestFileLines: state.maxTestFileLines,
474
+ testCoverage: state.testCoverage,
475
+ enforceMissingTests: state.enforceMissingTests,
476
+ enforceNaming: state.enforceNaming,
477
+ fileNamingValue: state.fileNamingValue,
478
+ componentNaming: state.componentNaming,
479
+ hookNaming: state.hookNaming,
480
+ importAlias: state.importAlias,
481
+ coverageSummaryPath: state.coverageSummaryPath,
482
+ coverageCommand: state.coverageCommand,
483
+ packageOverrides: state.packageOverrides
484
+ };
485
+ }
486
+
487
+ // src/utils/prompt.ts
488
+ function assertNotCancelled(value) {
489
+ if (clack5.isCancel(value)) {
490
+ clack5.cancel("Setup cancelled.");
491
+ process.exit(0);
492
+ }
493
+ }
494
+ async function confirm2(message) {
495
+ const result = await clack5.confirm({ message, initialValue: true });
496
+ assertNotCancelled(result);
497
+ return result;
498
+ }
499
+ async function confirmDangerous(message) {
500
+ const result = await clack5.confirm({ message, initialValue: false });
501
+ assertNotCancelled(result);
502
+ return result;
503
+ }
504
+ async function promptExistingConfigAction(configFile) {
505
+ const result = await clack5.select({
506
+ message: `${configFile} already exists. What do you want to do?`,
507
+ options: [
508
+ {
509
+ value: "edit",
510
+ label: "Edit existing config",
511
+ hint: "open the current rules and save updates in place"
512
+ },
513
+ {
514
+ value: "replace",
515
+ label: "Replace with a fresh scan",
516
+ hint: "re-scan the project and overwrite the current config"
517
+ },
518
+ {
519
+ value: "cancel",
520
+ label: "Cancel",
521
+ hint: "leave the current setup unchanged"
522
+ }
523
+ ]
524
+ });
525
+ assertNotCancelled(result);
526
+ return result;
527
+ }
528
+ async function promptInitDecision() {
529
+ const result = await clack5.select({
530
+ message: "How do you want to proceed?",
531
+ options: [
532
+ {
533
+ value: "accept",
534
+ label: "Accept defaults",
535
+ hint: "writes the config with these defaults; use --enforce in CI to block"
536
+ },
537
+ {
538
+ value: "customize",
539
+ label: "Customize rules",
540
+ hint: "edit limits, naming, test coverage, and package overrides"
541
+ },
542
+ {
543
+ value: "review",
544
+ label: "Review detected details",
545
+ hint: "show the full scan report with package and structure details"
546
+ }
547
+ ]
548
+ });
549
+ assertNotCancelled(result);
550
+ return result;
551
+ }
552
+
553
+ // src/utils/prompt-submenus.ts
554
+ var FILE_NAMING_OPTIONS = [
555
+ { value: "kebab-case", label: "kebab-case" },
556
+ { value: "camelCase", label: "camelCase" },
557
+ { value: "PascalCase", label: "PascalCase" },
558
+ { value: "snake_case", label: "snake_case" }
559
+ ];
560
+ var COMPONENT_NAMING_OPTIONS = [
561
+ { value: "PascalCase", label: "PascalCase", hint: "MyComponent.tsx" },
562
+ { value: "camelCase", label: "camelCase", hint: "myComponent.tsx" }
563
+ ];
564
+ var HOOK_NAMING_OPTIONS = [
565
+ { value: "useXxx", label: "useXxx", hint: "useAuth, useFormData" },
566
+ { value: "use-*", label: "use-*", hint: "use-auth, use-form-data" }
567
+ ];
568
+ async function promptFileLimitsMenu(state) {
569
+ while (true) {
570
+ const choice = await clack6.select({
571
+ message: "File limits",
572
+ options: [
573
+ { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
574
+ {
575
+ value: "maxTestFileLines",
576
+ label: "Max test file lines",
577
+ hint: state.maxTestFileLines > 0 ? String(state.maxTestFileLines) : "0 (unlimited)"
578
+ },
579
+ { value: "back", label: "Back" }
580
+ ]
581
+ });
582
+ assertNotCancelled(choice);
583
+ if (choice === "back") return;
584
+ if (choice === "maxFileLines") {
585
+ const result = await clack6.text({
586
+ message: "Maximum lines per source file?",
587
+ initialValue: String(state.maxFileLines),
588
+ validate: (v) => {
589
+ if (typeof v !== "string") return "Enter a positive number";
590
+ const n = Number.parseInt(v, 10);
591
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
592
+ }
593
+ });
594
+ assertNotCancelled(result);
595
+ state.maxFileLines = Number.parseInt(result, 10);
596
+ }
597
+ if (choice === "maxTestFileLines") {
598
+ const result = await clack6.text({
599
+ message: "Maximum lines per test file (0 to disable)?",
600
+ initialValue: String(state.maxTestFileLines),
601
+ validate: (v) => {
602
+ if (typeof v !== "string") return "Enter a number (0 or positive)";
603
+ const n = Number.parseInt(v, 10);
604
+ if (Number.isNaN(n) || n < 0) return "Enter a number (0 or positive)";
605
+ }
606
+ });
607
+ assertNotCancelled(result);
608
+ state.maxTestFileLines = Number.parseInt(result, 10);
609
+ }
610
+ }
611
+ }
612
+ async function promptNamingMenu(state) {
613
+ while (true) {
614
+ const options = [
615
+ {
616
+ value: "enforceNaming",
617
+ label: "Enforce file naming",
618
+ hint: state.enforceNaming ? "yes" : "no"
619
+ }
620
+ ];
621
+ if (state.enforceNaming) {
622
+ options.push({
623
+ value: "fileNaming",
624
+ label: "File naming convention",
625
+ hint: state.fileNamingValue ?? "(not set)"
626
+ });
627
+ }
628
+ options.push(
629
+ {
630
+ value: "componentNaming",
631
+ label: "Component naming",
632
+ hint: state.componentNaming ?? "(not set)"
633
+ },
634
+ {
635
+ value: "hookNaming",
636
+ label: "Hook naming",
637
+ hint: state.hookNaming ?? "(not set)"
638
+ },
639
+ {
640
+ value: "importAlias",
641
+ label: "Import alias",
642
+ hint: state.importAlias ?? "(not set)"
643
+ },
644
+ { value: "back", label: "Back" }
645
+ );
646
+ const choice = await clack6.select({ message: "Naming & conventions", options });
647
+ assertNotCancelled(choice);
648
+ if (choice === "back") return;
649
+ if (choice === "enforceNaming") {
650
+ const result = await clack6.confirm({
651
+ message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
652
+ initialValue: state.enforceNaming
653
+ });
654
+ assertNotCancelled(result);
655
+ if (result && !state.fileNamingValue) {
656
+ const selected = await clack6.select({
657
+ message: "Which file naming convention should be enforced?",
658
+ options: [...FILE_NAMING_OPTIONS]
659
+ });
660
+ assertNotCancelled(selected);
661
+ state.fileNamingValue = selected;
662
+ }
663
+ state.enforceNaming = result;
664
+ }
665
+ if (choice === "fileNaming") {
666
+ const selected = await clack6.select({
667
+ message: "Which file naming convention should be enforced?",
668
+ options: [...FILE_NAMING_OPTIONS],
669
+ initialValue: state.fileNamingValue
670
+ });
671
+ assertNotCancelled(selected);
672
+ state.fileNamingValue = selected;
673
+ }
674
+ if (choice === "componentNaming") {
675
+ const selected = await clack6.select({
676
+ message: "Component naming convention",
677
+ options: [
678
+ ...COMPONENT_NAMING_OPTIONS,
679
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
680
+ ],
681
+ initialValue: state.componentNaming ?? SENTINEL_CLEAR
682
+ });
683
+ assertNotCancelled(selected);
684
+ state.componentNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
685
+ }
686
+ if (choice === "hookNaming") {
687
+ const selected = await clack6.select({
688
+ message: "Hook naming convention",
689
+ options: [
690
+ ...HOOK_NAMING_OPTIONS,
691
+ { value: SENTINEL_CLEAR, label: "Clear (no convention)" }
692
+ ],
693
+ initialValue: state.hookNaming ?? SENTINEL_CLEAR
694
+ });
695
+ assertNotCancelled(selected);
696
+ state.hookNaming = selected === SENTINEL_CLEAR ? void 0 : selected;
697
+ }
698
+ if (choice === "importAlias") {
699
+ const selected = await clack6.select({
700
+ message: "Import alias pattern",
701
+ options: [
702
+ { value: "@/*", label: "@/*", hint: "import { x } from '@/utils'" },
703
+ { value: "~/*", label: "~/*", hint: "import { x } from '~/utils'" },
704
+ { value: SENTINEL_CUSTOM, label: "Custom..." },
705
+ { value: SENTINEL_CLEAR, label: "Clear (no alias)" }
706
+ ],
707
+ initialValue: state.importAlias ?? SENTINEL_CLEAR
708
+ });
709
+ assertNotCancelled(selected);
710
+ if (selected === SENTINEL_CLEAR) {
711
+ state.importAlias = void 0;
712
+ } else if (selected === SENTINEL_CUSTOM) {
713
+ const result = await clack6.text({
714
+ message: "Custom import alias (e.g. #/*)?",
715
+ initialValue: state.importAlias ?? "",
716
+ placeholder: "e.g. #/*",
717
+ validate: (v) => {
718
+ if (typeof v !== "string" || !v.trim()) return "Alias cannot be empty";
719
+ if (!/^[a-zA-Z@~#$][a-zA-Z0-9@~#$_-]*\/\*$/.test(v.trim()))
720
+ return "Must match pattern like @/*, ~/*, or #src/*";
721
+ }
722
+ });
723
+ assertNotCancelled(result);
724
+ state.importAlias = result.trim();
725
+ } else {
726
+ state.importAlias = selected;
727
+ }
728
+ }
729
+ }
730
+ }
731
+ async function promptTestingMenu(state) {
732
+ while (true) {
733
+ const options = [
734
+ {
735
+ value: "enforceMissingTests",
736
+ label: "Enforce missing tests",
737
+ hint: state.enforceMissingTests ? "yes" : "no"
738
+ },
739
+ {
740
+ value: "testCoverage",
741
+ label: "Test coverage target",
742
+ hint: state.testCoverage === 0 ? "0 (disabled)" : `${state.testCoverage}%`
743
+ }
744
+ ];
745
+ if (state.testCoverage > 0) {
746
+ options.push(
747
+ {
748
+ value: "coverageSummaryPath",
749
+ label: "Coverage summary path",
750
+ hint: state.coverageSummaryPath
751
+ },
752
+ {
753
+ value: "coverageCommand",
754
+ label: "Coverage command",
755
+ hint: state.coverageCommand ?? "auto-detect from package.json test runner"
756
+ }
757
+ );
758
+ }
759
+ options.push({ value: "back", label: "Back" });
760
+ const choice = await clack6.select({ message: "Testing & coverage", options });
761
+ assertNotCancelled(choice);
762
+ if (choice === "back") return;
763
+ if (choice === "enforceMissingTests") {
764
+ const result = await clack6.confirm({
765
+ message: "Require every source file to have a corresponding test file?",
766
+ initialValue: state.enforceMissingTests
767
+ });
768
+ assertNotCancelled(result);
769
+ state.enforceMissingTests = result;
770
+ }
771
+ if (choice === "testCoverage") {
772
+ const result = await clack6.text({
773
+ message: "Test coverage target (0 disables coverage checks)?",
774
+ initialValue: String(state.testCoverage),
775
+ validate: (v) => {
776
+ if (typeof v !== "string") return "Enter a number between 0 and 100";
777
+ const n = Number.parseInt(v, 10);
778
+ if (Number.isNaN(n) || n < 0 || n > 100) return "Enter a number between 0 and 100";
779
+ }
780
+ });
781
+ assertNotCancelled(result);
782
+ state.testCoverage = Number.parseInt(result, 10);
783
+ }
784
+ if (choice === "coverageSummaryPath") {
785
+ const result = await clack6.text({
786
+ message: "Coverage summary path (relative to package root)?",
787
+ initialValue: state.coverageSummaryPath,
788
+ validate: (v) => {
789
+ if (typeof v !== "string" || v.trim().length === 0) return "Path cannot be empty";
790
+ }
791
+ });
792
+ assertNotCancelled(result);
793
+ state.coverageSummaryPath = result.trim();
794
+ }
795
+ if (choice === "coverageCommand") {
796
+ const result = await clack6.text({
797
+ message: "Coverage command (blank to auto-detect from package.json)?",
798
+ initialValue: state.coverageCommand ?? "",
799
+ placeholder: "(auto-detect from package.json test runner)"
800
+ });
801
+ assertNotCancelled(result);
802
+ const trimmed = result.trim();
803
+ state.coverageCommand = trimmed.length > 0 ? trimmed : void 0;
804
+ }
805
+ }
806
+ }
807
+
808
+ export {
809
+ spawnAsync,
810
+ promptIntegrations,
811
+ getRootPackage,
812
+ SENTINEL_SKIP,
813
+ FILE_NAMING_OPTIONS,
814
+ promptRuleMenu,
815
+ assertNotCancelled,
816
+ confirm2 as confirm,
817
+ confirmDangerous,
818
+ promptExistingConfigAction,
819
+ promptInitDecision
820
+ };
821
+ //# sourceMappingURL=chunk-XQKOK3FU.js.map