@vibecheckai/cli 3.2.2 → 3.2.4

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.
Files changed (170) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  6. package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
  7. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
  8. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
  9. package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
  10. package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
  11. package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
  12. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
  14. package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
  15. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  16. package/bin/runners/lib/analyzers.js +606 -325
  17. package/bin/runners/lib/auth-truth.js +193 -193
  18. package/bin/runners/lib/backup.js +62 -62
  19. package/bin/runners/lib/billing.js +107 -107
  20. package/bin/runners/lib/claims.js +118 -118
  21. package/bin/runners/lib/cli-ui.js +540 -540
  22. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  23. package/bin/runners/lib/contracts/env-contract.js +181 -181
  24. package/bin/runners/lib/contracts/external-contract.js +206 -206
  25. package/bin/runners/lib/contracts/guard.js +168 -168
  26. package/bin/runners/lib/contracts/index.js +89 -89
  27. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  28. package/bin/runners/lib/contracts/route-contract.js +199 -199
  29. package/bin/runners/lib/contracts.js +804 -804
  30. package/bin/runners/lib/detect.js +89 -89
  31. package/bin/runners/lib/doctor/autofix.js +254 -254
  32. package/bin/runners/lib/doctor/index.js +37 -37
  33. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  34. package/bin/runners/lib/doctor/modules/index.js +46 -46
  35. package/bin/runners/lib/doctor/modules/network.js +250 -250
  36. package/bin/runners/lib/doctor/modules/project.js +312 -312
  37. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  38. package/bin/runners/lib/doctor/modules/security.js +348 -348
  39. package/bin/runners/lib/doctor/modules/system.js +213 -213
  40. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  41. package/bin/runners/lib/doctor/reporter.js +262 -262
  42. package/bin/runners/lib/doctor/service.js +262 -262
  43. package/bin/runners/lib/doctor/types.js +113 -113
  44. package/bin/runners/lib/doctor/ui.js +263 -263
  45. package/bin/runners/lib/doctor-v2.js +608 -608
  46. package/bin/runners/lib/drift.js +425 -425
  47. package/bin/runners/lib/enforcement.js +72 -72
  48. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  49. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  50. package/bin/runners/lib/engines/ast-cache.js +99 -0
  51. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  52. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  53. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  54. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  55. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  56. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  57. package/bin/runners/lib/engines/file-filter.js +131 -0
  58. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  59. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  60. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  61. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  62. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  63. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  64. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  65. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  66. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  67. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  68. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  69. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  70. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  71. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  72. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  73. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  74. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  75. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  76. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  77. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  78. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  79. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  80. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  81. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  82. package/bin/runners/lib/enterprise-detect.js +603 -603
  83. package/bin/runners/lib/enterprise-init.js +942 -942
  84. package/bin/runners/lib/env-resolver.js +417 -417
  85. package/bin/runners/lib/env-template.js +66 -66
  86. package/bin/runners/lib/env.js +189 -189
  87. package/bin/runners/lib/extractors/client-calls.js +990 -990
  88. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  89. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  90. package/bin/runners/lib/extractors/index.js +363 -363
  91. package/bin/runners/lib/extractors/next-routes.js +524 -524
  92. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  93. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  94. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  95. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  96. package/bin/runners/lib/findings-schema.js +281 -281
  97. package/bin/runners/lib/firewall-prompt.js +50 -50
  98. package/bin/runners/lib/global-flags.js +213 -213
  99. package/bin/runners/lib/graph/graph-builder.js +265 -265
  100. package/bin/runners/lib/graph/html-renderer.js +413 -413
  101. package/bin/runners/lib/graph/index.js +32 -32
  102. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  103. package/bin/runners/lib/graph/static-extractor.js +518 -518
  104. package/bin/runners/lib/html-report.js +650 -650
  105. package/bin/runners/lib/interactive-menu.js +1496 -1496
  106. package/bin/runners/lib/llm.js +75 -75
  107. package/bin/runners/lib/meter.js +61 -61
  108. package/bin/runners/lib/missions/evidence.js +126 -126
  109. package/bin/runners/lib/patch.js +40 -40
  110. package/bin/runners/lib/permissions/auth-model.js +213 -213
  111. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  112. package/bin/runners/lib/permissions/index.js +45 -45
  113. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  114. package/bin/runners/lib/pkgjson.js +28 -28
  115. package/bin/runners/lib/policy.js +295 -295
  116. package/bin/runners/lib/preflight.js +142 -142
  117. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  118. package/bin/runners/lib/reality/index.js +318 -318
  119. package/bin/runners/lib/reality/request-hashing.js +416 -416
  120. package/bin/runners/lib/reality/request-mapper.js +453 -453
  121. package/bin/runners/lib/reality/safety-rails.js +463 -463
  122. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  123. package/bin/runners/lib/reality/toast-detector.js +393 -393
  124. package/bin/runners/lib/reality-findings.js +84 -84
  125. package/bin/runners/lib/receipts.js +179 -179
  126. package/bin/runners/lib/redact.js +29 -29
  127. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  128. package/bin/runners/lib/replay/index.js +263 -263
  129. package/bin/runners/lib/replay/player.js +348 -348
  130. package/bin/runners/lib/replay/recorder.js +331 -331
  131. package/bin/runners/lib/report-output.js +187 -187
  132. package/bin/runners/lib/report.js +135 -135
  133. package/bin/runners/lib/route-detection.js +1140 -1140
  134. package/bin/runners/lib/sandbox/index.js +59 -59
  135. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  136. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  137. package/bin/runners/lib/sandbox/worktree.js +174 -174
  138. package/bin/runners/lib/scan-output.js +525 -190
  139. package/bin/runners/lib/schema-validator.js +350 -350
  140. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  141. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  142. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  143. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  144. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  145. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  146. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  147. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  148. package/bin/runners/lib/schemas/validator.js +438 -438
  149. package/bin/runners/lib/score-history.js +282 -282
  150. package/bin/runners/lib/share-pack.js +239 -239
  151. package/bin/runners/lib/snippets.js +67 -67
  152. package/bin/runners/lib/status-output.js +253 -253
  153. package/bin/runners/lib/terminal-ui.js +351 -271
  154. package/bin/runners/lib/upsell.js +510 -510
  155. package/bin/runners/lib/usage.js +153 -153
  156. package/bin/runners/lib/validate-patch.js +156 -156
  157. package/bin/runners/lib/verdict-engine.js +628 -628
  158. package/bin/runners/reality/engine.js +917 -917
  159. package/bin/runners/reality/flows.js +122 -122
  160. package/bin/runners/reality/report.js +378 -378
  161. package/bin/runners/reality/session.js +193 -193
  162. package/bin/runners/runGuard.js +168 -168
  163. package/bin/runners/runProof.zip +0 -0
  164. package/bin/runners/runProve.js +8 -0
  165. package/bin/runners/runReality.js +14 -0
  166. package/bin/runners/runScan.js +17 -1
  167. package/bin/runners/runTruth.js +15 -3
  168. package/mcp-server/tier-auth.js +4 -4
  169. package/mcp-server/tools/index.js +72 -72
  170. package/package.json +1 -1
@@ -1,1496 +1,1496 @@
1
- /**
2
- * Interactive Menu System - Premium Terminal UI
3
- * ═══════════════════════════════════════════════════════════════════════════════
4
- * Beautiful arrow-key navigation menus for CLI applications
5
- * No external dependencies - pure Node.js
6
- * ═══════════════════════════════════════════════════════════════════════════════
7
- */
8
-
9
- "use strict";
10
-
11
- const readline = require("readline");
12
-
13
- // ═══════════════════════════════════════════════════════════════════════════════
14
- // TERMINAL DETECTION
15
- // ═══════════════════════════════════════════════════════════════════════════════
16
- const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
17
- const SUPPORTS_TRUECOLOR = SUPPORTS_COLOR && (
18
- process.env.COLORTERM === "truecolor" ||
19
- process.env.TERM_PROGRAM === "iTerm.app" ||
20
- process.env.TERM_PROGRAM === "Apple_Terminal" ||
21
- process.env.WT_SESSION
22
- );
23
- const SUPPORTS_UNICODE = process.platform !== "win32" || process.env.WT_SESSION || process.env.TERM_PROGRAM;
24
-
25
- // ═══════════════════════════════════════════════════════════════════════════════
26
- // ANSI ESCAPE CODES
27
- // ═══════════════════════════════════════════════════════════════════════════════
28
- const ESC = "\x1b";
29
- const CSI = `${ESC}[`;
30
-
31
- const ansi = {
32
- // Cursor
33
- hideCursor: `${CSI}?25l`,
34
- showCursor: `${CSI}?25h`,
35
- saveCursor: `${CSI}s`,
36
- restoreCursor: `${CSI}u`,
37
- moveTo: (row, col) => `${CSI}${row};${col}H`,
38
- moveUp: (n = 1) => `${CSI}${n}A`,
39
- moveDown: (n = 1) => `${CSI}${n}B`,
40
- moveRight: (n = 1) => `${CSI}${n}C`,
41
- moveLeft: (n = 1) => `${CSI}${n}D`,
42
- moveToColumn: (col) => `${CSI}${col}G`,
43
-
44
- // Clear
45
- clearLine: `${CSI}2K`,
46
- clearToEnd: `${CSI}0K`,
47
- clearToStart: `${CSI}1K`,
48
- clearScreen: `${CSI}2J`,
49
- clearDown: `${CSI}0J`,
50
-
51
- // Styles
52
- reset: `${CSI}0m`,
53
- bold: `${CSI}1m`,
54
- dim: `${CSI}2m`,
55
- italic: `${CSI}3m`,
56
- underline: `${CSI}4m`,
57
- blink: `${CSI}5m`,
58
- inverse: `${CSI}7m`,
59
- hidden: `${CSI}8m`,
60
- strikethrough: `${CSI}9m`,
61
-
62
- // Colors
63
- black: `${CSI}30m`,
64
- red: `${CSI}31m`,
65
- green: `${CSI}32m`,
66
- yellow: `${CSI}33m`,
67
- blue: `${CSI}34m`,
68
- magenta: `${CSI}35m`,
69
- cyan: `${CSI}36m`,
70
- white: `${CSI}37m`,
71
- gray: `${CSI}90m`,
72
-
73
- // Bright colors
74
- brightRed: `${CSI}91m`,
75
- brightGreen: `${CSI}92m`,
76
- brightYellow: `${CSI}93m`,
77
- brightBlue: `${CSI}94m`,
78
- brightMagenta: `${CSI}95m`,
79
- brightCyan: `${CSI}96m`,
80
- brightWhite: `${CSI}97m`,
81
-
82
- // Background
83
- bgBlack: `${CSI}40m`,
84
- bgRed: `${CSI}41m`,
85
- bgGreen: `${CSI}42m`,
86
- bgYellow: `${CSI}43m`,
87
- bgBlue: `${CSI}44m`,
88
- bgMagenta: `${CSI}45m`,
89
- bgCyan: `${CSI}46m`,
90
- bgWhite: `${CSI}47m`,
91
- bgGray: `${CSI}100m`,
92
-
93
- // True color
94
- rgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${CSI}38;2;${r};${g};${b}m` : "",
95
- bgRgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${CSI}48;2;${r};${g};${b}m` : "",
96
- };
97
-
98
- // Disable colors if not supported
99
- const c = SUPPORTS_COLOR ? ansi : Object.fromEntries(
100
- Object.keys(ansi).map(k => [k, typeof ansi[k] === "function" ? () => "" : ""])
101
- );
102
-
103
- // ═══════════════════════════════════════════════════════════════════════════════
104
- // SYMBOLS
105
- // ═══════════════════════════════════════════════════════════════════════════════
106
- const sym = SUPPORTS_UNICODE ? {
107
- // Selection
108
- pointer: "❯",
109
- pointerSmall: "›",
110
- radio: {
111
- on: "◉",
112
- off: "○",
113
- },
114
- checkbox: {
115
- on: "☑",
116
- off: "☐",
117
- disabled: "▫",
118
- },
119
-
120
- // Status
121
- success: "✓",
122
- error: "✗",
123
- warning: "⚠",
124
- info: "ℹ",
125
- active: "●",
126
- pending: "○",
127
-
128
- // Decorative
129
- star: "★",
130
- heart: "♥",
131
- lightning: "⚡",
132
- fire: "🔥",
133
- rocket: "🚀",
134
- sparkles: "✨",
135
-
136
- // Arrows
137
- arrowUp: "↑",
138
- arrowDown: "↓",
139
- arrowLeft: "←",
140
- arrowRight: "→",
141
- triangleUp: "▲",
142
- triangleDown: "▼",
143
- triangleLeft: "◀",
144
- triangleRight: "▶",
145
-
146
- // Box drawing
147
- boxTopLeft: "╭",
148
- boxTopRight: "╮",
149
- boxBottomLeft: "╰",
150
- boxBottomRight: "╯",
151
- boxHorizontal: "─",
152
- boxVertical: "│",
153
- boxHorizontalBold: "━",
154
- boxVerticalBold: "┃",
155
-
156
- // Lines
157
- line: "─",
158
- lineBold: "━",
159
- lineDashed: "┄",
160
- lineDouble: "═",
161
-
162
- // Misc
163
- bullet: "•",
164
- dot: "·",
165
- ellipsis: "…",
166
- separator: "│",
167
- } : {
168
- pointer: ">",
169
- pointerSmall: ">",
170
- radio: { on: "(*)", off: "( )" },
171
- checkbox: { on: "[x]", off: "[ ]", disabled: "[-]" },
172
- success: "√",
173
- error: "×",
174
- warning: "!",
175
- info: "i",
176
- active: "*",
177
- pending: "o",
178
- star: "*",
179
- heart: "<3",
180
- lightning: "!",
181
- fire: "(!)",
182
- rocket: ">>",
183
- sparkles: "*",
184
- arrowUp: "^",
185
- arrowDown: "v",
186
- arrowLeft: "<",
187
- arrowRight: ">",
188
- triangleUp: "^",
189
- triangleDown: "v",
190
- triangleLeft: "<",
191
- triangleRight: ">",
192
- boxTopLeft: "+",
193
- boxTopRight: "+",
194
- boxBottomLeft: "+",
195
- boxBottomRight: "+",
196
- boxHorizontal: "-",
197
- boxVertical: "|",
198
- boxHorizontalBold: "=",
199
- boxVerticalBold: "|",
200
- line: "-",
201
- lineBold: "=",
202
- lineDashed: "-",
203
- lineDouble: "=",
204
- bullet: "*",
205
- dot: ".",
206
- ellipsis: "...",
207
- separator: "|",
208
- };
209
-
210
- // ═══════════════════════════════════════════════════════════════════════════════
211
- // COLOR THEMES
212
- // ═══════════════════════════════════════════════════════════════════════════════
213
- const themes = {
214
- default: {
215
- primary: c.cyan,
216
- secondary: c.magenta,
217
- accent: c.yellow,
218
- success: c.green,
219
- warning: c.yellow,
220
- error: c.red,
221
- muted: c.gray,
222
- highlight: c.brightWhite,
223
- pointer: c.cyan,
224
- selected: c.rgb(0, 255, 200),
225
- unselected: c.gray,
226
- title: c.bold + c.brightWhite,
227
- subtitle: c.dim,
228
- border: c.dim,
229
- search: c.yellow,
230
- },
231
-
232
- vibecheck: {
233
- primary: c.rgb(0, 255, 255), // Cyan
234
- secondary: c.rgb(138, 43, 226), // Purple
235
- accent: c.rgb(255, 200, 100), // Gold
236
- success: c.rgb(0, 255, 150), // Green
237
- warning: c.rgb(255, 200, 0), // Yellow
238
- error: c.rgb(255, 80, 80), // Red
239
- muted: c.rgb(120, 120, 120), // Gray
240
- highlight: c.rgb(255, 255, 255), // White
241
- pointer: c.rgb(0, 255, 255), // Cyan
242
- selected: c.rgb(0, 255, 200), // Bright cyan-green
243
- unselected: c.rgb(100, 100, 100), // Dark gray
244
- title: c.bold + c.rgb(255, 255, 255),
245
- subtitle: c.rgb(150, 150, 150),
246
- border: c.rgb(60, 60, 80),
247
- search: c.rgb(255, 200, 100),
248
- },
249
-
250
- minimal: {
251
- primary: c.white,
252
- secondary: c.gray,
253
- accent: c.white,
254
- success: c.green,
255
- warning: c.yellow,
256
- error: c.red,
257
- muted: c.dim,
258
- highlight: c.bold,
259
- pointer: c.white,
260
- selected: c.white,
261
- unselected: c.dim,
262
- title: c.bold,
263
- subtitle: c.dim,
264
- border: c.dim,
265
- search: c.white,
266
- },
267
- };
268
-
269
- // ═══════════════════════════════════════════════════════════════════════════════
270
- // UTILITY FUNCTIONS
271
- // ═══════════════════════════════════════════════════════════════════════════════
272
- function stripAnsi(str) {
273
- return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
274
- }
275
-
276
- function truncate(str, maxLen, suffix = "…") {
277
- const stripped = stripAnsi(str);
278
- if (stripped.length <= maxLen) return str;
279
-
280
- // Simple truncation (doesn't preserve ANSI, but good enough for most cases)
281
- return stripped.slice(0, maxLen - suffix.length) + suffix;
282
- }
283
-
284
- function padEnd(str, len, char = " ") {
285
- const stripped = stripAnsi(str);
286
- const padding = Math.max(0, len - stripped.length);
287
- return str + char.repeat(padding);
288
- }
289
-
290
- function padStart(str, len, char = " ") {
291
- const stripped = stripAnsi(str);
292
- const padding = Math.max(0, len - stripped.length);
293
- return char.repeat(padding) + str;
294
- }
295
-
296
- function centerText(str, width, char = " ") {
297
- const stripped = stripAnsi(str);
298
- const padding = Math.max(0, width - stripped.length);
299
- const left = Math.floor(padding / 2);
300
- const right = padding - left;
301
- return char.repeat(left) + str + char.repeat(right);
302
- }
303
-
304
- function wrapText(str, width) {
305
- const words = str.split(" ");
306
- const lines = [];
307
- let currentLine = "";
308
-
309
- for (const word of words) {
310
- if (currentLine.length + word.length + 1 <= width) {
311
- currentLine += (currentLine ? " " : "") + word;
312
- } else {
313
- if (currentLine) lines.push(currentLine);
314
- currentLine = word;
315
- }
316
- }
317
- if (currentLine) lines.push(currentLine);
318
-
319
- return lines;
320
- }
321
-
322
- function getTerminalSize() {
323
- return {
324
- columns: process.stdout.columns || 80,
325
- rows: process.stdout.rows || 24,
326
- };
327
- }
328
-
329
- // ═══════════════════════════════════════════════════════════════════════════════
330
- // GRADIENT TEXT
331
- // ═══════════════════════════════════════════════════════════════════════════════
332
- function gradient(text, colors = [[0, 255, 255], [138, 43, 226], [255, 20, 147]]) {
333
- if (!SUPPORTS_TRUECOLOR) return `${c.cyan}${text}${c.reset}`;
334
-
335
- const chars = [...text];
336
- const len = chars.length;
337
- if (len === 0) return text;
338
-
339
- return chars.map((char, i) => {
340
- const t = i / Math.max(len - 1, 1);
341
- const segmentLen = colors.length - 1;
342
- const segment = Math.min(Math.floor(t * segmentLen), segmentLen - 1);
343
- const localT = (t * segmentLen) - segment;
344
- const c1 = colors[segment];
345
- const c2 = colors[segment + 1] || c1;
346
- const r = Math.round(c1[0] + (c2[0] - c1[0]) * localT);
347
- const g = Math.round(c1[1] + (c2[1] - c1[1]) * localT);
348
- const b = Math.round(c1[2] + (c2[2] - c1[2]) * localT);
349
- return `${c.rgb(r, g, b)}${char}`;
350
- }).join("") + c.reset;
351
- }
352
-
353
- // ═══════════════════════════════════════════════════════════════════════════════
354
- // BOX DRAWING
355
- // ═══════════════════════════════════════════════════════════════════════════════
356
- function drawBox(content, options = {}) {
357
- const {
358
- title = "",
359
- width = null,
360
- padding = 1,
361
- borderColor = c.dim,
362
- titleColor = c.bold,
363
- style = "rounded", // rounded, square, double, bold
364
- } = options;
365
-
366
- const borders = {
367
- rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
368
- square: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
369
- double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" },
370
- bold: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
371
- };
372
-
373
- const b = borders[style] || borders.rounded;
374
- const lines = Array.isArray(content) ? content : content.split("\n");
375
- const maxContentWidth = Math.max(...lines.map(l => stripAnsi(l).length));
376
- const boxWidth = width || maxContentWidth + (padding * 2) + 2;
377
- const innerWidth = boxWidth - 2;
378
-
379
- const output = [];
380
-
381
- // Top border with optional title
382
- if (title) {
383
- const titleStr = ` ${title} `;
384
- const titleLen = stripAnsi(titleStr).length;
385
- const leftBorder = Math.floor((innerWidth - titleLen) / 2);
386
- const rightBorder = innerWidth - titleLen - leftBorder;
387
- output.push(
388
- `${borderColor}${b.tl}${b.h.repeat(leftBorder)}${c.reset}${titleColor}${titleStr}${c.reset}${borderColor}${b.h.repeat(rightBorder)}${b.tr}${c.reset}`
389
- );
390
- } else {
391
- output.push(`${borderColor}${b.tl}${b.h.repeat(innerWidth)}${b.tr}${c.reset}`);
392
- }
393
-
394
- // Padding top
395
- for (let i = 0; i < padding; i++) {
396
- output.push(`${borderColor}${b.v}${c.reset}${" ".repeat(innerWidth)}${borderColor}${b.v}${c.reset}`);
397
- }
398
-
399
- // Content
400
- for (const line of lines) {
401
- const stripped = stripAnsi(line);
402
- const contentPadding = " ".repeat(padding);
403
- const rightPad = " ".repeat(Math.max(0, innerWidth - stripped.length - (padding * 2)));
404
- output.push(`${borderColor}${b.v}${c.reset}${contentPadding}${line}${rightPad}${contentPadding}${borderColor}${b.v}${c.reset}`);
405
- }
406
-
407
- // Padding bottom
408
- for (let i = 0; i < padding; i++) {
409
- output.push(`${borderColor}${b.v}${c.reset}${" ".repeat(innerWidth)}${borderColor}${b.v}${c.reset}`);
410
- }
411
-
412
- // Bottom border
413
- output.push(`${borderColor}${b.bl}${b.h.repeat(innerWidth)}${b.br}${c.reset}`);
414
-
415
- return output.join("\n");
416
- }
417
-
418
- // ═══════════════════════════════════════════════════════════════════════════════
419
- // BASE PROMPT CLASS
420
- // ═══════════════════════════════════════════════════════════════════════════════
421
- class BasePrompt {
422
- constructor(options = {}) {
423
- this.theme = themes[options.theme] || themes.vibecheck;
424
- this.stdin = options.stdin || process.stdin;
425
- this.stdout = options.stdout || process.stdout;
426
- this.rl = null;
427
- this.closed = false;
428
- }
429
-
430
- write(str) {
431
- this.stdout.write(str);
432
- }
433
-
434
- clearLines(count) {
435
- for (let i = 0; i < count; i++) {
436
- this.write(ansi.moveUp(1) + ansi.clearLine);
437
- }
438
- }
439
-
440
- hideCursor() {
441
- this.write(ansi.hideCursor);
442
- }
443
-
444
- showCursor() {
445
- this.write(ansi.showCursor);
446
- }
447
-
448
- setupReadline() {
449
- if (this.rl) return;
450
-
451
- this.rl = readline.createInterface({
452
- input: this.stdin,
453
- output: this.stdout,
454
- terminal: true,
455
- });
456
-
457
- // Enable keypress events
458
- readline.emitKeypressEvents(this.stdin, this.rl);
459
-
460
- if (this.stdin.isTTY) {
461
- this.stdin.setRawMode(true);
462
- }
463
- }
464
-
465
- cleanup() {
466
- if (this.closed) return;
467
- this.closed = true;
468
-
469
- this.showCursor();
470
-
471
- if (this.stdin.isTTY) {
472
- this.stdin.setRawMode(false);
473
- }
474
-
475
- if (this.rl) {
476
- this.rl.close();
477
- this.rl = null;
478
- }
479
- }
480
- }
481
-
482
- // ═══════════════════════════════════════════════════════════════════════════════
483
- // SELECT PROMPT - Single selection from list
484
- // ═══════════════════════════════════════════════════════════════════════════════
485
- class SelectPrompt extends BasePrompt {
486
- constructor(options = {}) {
487
- super(options);
488
-
489
- this.message = options.message || "Select an option";
490
- this.choices = this.normalizeChoices(options.choices || []);
491
- this.pageSize = options.pageSize || 10;
492
- this.loop = options.loop !== false;
493
- this.showSearch = options.search !== false;
494
- this.hint = options.hint || "";
495
- this.initialIndex = options.initial || 0;
496
-
497
- // State
498
- this.selectedIndex = this.initialIndex;
499
- this.searchQuery = "";
500
- this.filteredChoices = [...this.choices];
501
- this.scrollOffset = 0;
502
- this.searching = false;
503
- this.renderedLines = 0;
504
- }
505
-
506
- normalizeChoices(choices) {
507
- return choices.map((choice, index) => {
508
- if (typeof choice === "string") {
509
- return { value: choice, label: choice, index };
510
- }
511
- return {
512
- value: choice.value ?? choice.label ?? choice.name,
513
- label: choice.label ?? choice.name ?? choice.value,
514
- description: choice.description || choice.desc || "",
515
- disabled: choice.disabled || false,
516
- group: choice.group || null,
517
- icon: choice.icon || null,
518
- hint: choice.hint || "",
519
- index,
520
- };
521
- });
522
- }
523
-
524
- filterChoices() {
525
- if (!this.searchQuery) {
526
- this.filteredChoices = [...this.choices];
527
- return;
528
- }
529
-
530
- const query = this.searchQuery.toLowerCase();
531
- this.filteredChoices = this.choices.filter(choice => {
532
- const label = (choice.label || "").toLowerCase();
533
- const description = (choice.description || "").toLowerCase();
534
- return label.includes(query) || description.includes(query);
535
- });
536
-
537
- // Reset selection if current is out of bounds
538
- if (this.selectedIndex >= this.filteredChoices.length) {
539
- this.selectedIndex = Math.max(0, this.filteredChoices.length - 1);
540
- }
541
- }
542
-
543
- getVisibleChoices() {
544
- const start = this.scrollOffset;
545
- const end = Math.min(start + this.pageSize, this.filteredChoices.length);
546
- return this.filteredChoices.slice(start, end);
547
- }
548
-
549
- updateScroll() {
550
- // Ensure selected item is visible
551
- if (this.selectedIndex < this.scrollOffset) {
552
- this.scrollOffset = this.selectedIndex;
553
- } else if (this.selectedIndex >= this.scrollOffset + this.pageSize) {
554
- this.scrollOffset = this.selectedIndex - this.pageSize + 1;
555
- }
556
- }
557
-
558
- renderChoice(choice, isSelected, index) {
559
- const t = this.theme;
560
- const pointer = isSelected ? `${t.pointer}${sym.pointer}${c.reset}` : " ";
561
- const icon = choice.icon ? `${choice.icon} ` : "";
562
-
563
- let label;
564
- if (choice.disabled) {
565
- label = `${t.muted}${icon}${choice.label} (disabled)${c.reset}`;
566
- } else if (isSelected) {
567
- label = `${t.selected}${c.bold}${icon}${choice.label}${c.reset}`;
568
- } else {
569
- label = `${t.unselected}${icon}${choice.label}${c.reset}`;
570
- }
571
-
572
- let description = "";
573
- if (choice.description && isSelected) {
574
- description = ` ${t.muted}${sym.separator} ${choice.description}${c.reset}`;
575
- }
576
-
577
- let hint = "";
578
- if (choice.hint && isSelected) {
579
- hint = ` ${t.accent}${choice.hint}${c.reset}`;
580
- }
581
-
582
- return `${pointer} ${label}${description}${hint}`;
583
- }
584
-
585
- renderHeader() {
586
- const t = this.theme;
587
- const lines = [];
588
-
589
- // Title
590
- lines.push(`${t.title}${sym.pointer} ${this.message}${c.reset}`);
591
-
592
- // Search box
593
- if (this.showSearch) {
594
- const searchIcon = this.searching ? `${t.search}${sym.pointer}${c.reset}` : `${t.muted}/${c.reset}`;
595
- const query = this.searchQuery || (this.searching ? "" : "type to search");
596
- const queryStyle = this.searchQuery ? t.search : t.muted;
597
- lines.push(` ${searchIcon} ${queryStyle}${query}${c.reset}`);
598
- }
599
-
600
- // Hint
601
- if (this.hint && !this.searching) {
602
- lines.push(` ${t.muted}${this.hint}${c.reset}`);
603
- }
604
-
605
- lines.push("");
606
-
607
- return lines;
608
- }
609
-
610
- renderFooter() {
611
- const t = this.theme;
612
- const lines = [];
613
-
614
- // Scroll indicators
615
- const total = this.filteredChoices.length;
616
- const showing = Math.min(this.pageSize, total);
617
- const hasMore = total > this.pageSize;
618
-
619
- if (hasMore) {
620
- const scrollInfo = `${this.scrollOffset + 1}-${this.scrollOffset + showing} of ${total}`;
621
- lines.push("");
622
- lines.push(` ${t.muted}${scrollInfo}${c.reset}`);
623
- }
624
-
625
- // Navigation hints
626
- lines.push("");
627
- const navHints = [
628
- `${t.muted}↑/↓${c.reset} navigate`,
629
- `${t.muted}enter${c.reset} select`,
630
- ];
631
- if (this.showSearch) {
632
- navHints.unshift(`${t.muted}/${c.reset} search`);
633
- }
634
- navHints.push(`${t.muted}esc${c.reset} cancel`);
635
- lines.push(` ${navHints.join(" ")}`);
636
-
637
- return lines;
638
- }
639
-
640
- render() {
641
- // Clear previous render
642
- if (this.renderedLines > 0) {
643
- this.clearLines(this.renderedLines);
644
- }
645
-
646
- const lines = [];
647
-
648
- // Header
649
- lines.push(...this.renderHeader());
650
-
651
- // Groups handling
652
- let currentGroup = null;
653
- const visibleChoices = this.getVisibleChoices();
654
-
655
- for (let i = 0; i < visibleChoices.length; i++) {
656
- const choice = visibleChoices[i];
657
- const globalIndex = this.scrollOffset + i;
658
- const isSelected = globalIndex === this.selectedIndex;
659
-
660
- // Group header
661
- if (choice.group && choice.group !== currentGroup) {
662
- currentGroup = choice.group;
663
- lines.push(` ${this.theme.subtitle}${sym.bullet} ${currentGroup}${c.reset}`);
664
- }
665
-
666
- lines.push(this.renderChoice(choice, isSelected, globalIndex));
667
- }
668
-
669
- // Empty state
670
- if (visibleChoices.length === 0) {
671
- lines.push(` ${this.theme.muted}No matches found${c.reset}`);
672
- }
673
-
674
- // Footer
675
- lines.push(...this.renderFooter());
676
-
677
- // Write output
678
- this.write(lines.join("\n") + "\n");
679
- this.renderedLines = lines.length;
680
- }
681
-
682
- moveUp() {
683
- if (this.filteredChoices.length === 0) return;
684
-
685
- let newIndex = this.selectedIndex - 1;
686
-
687
- // Skip disabled items
688
- while (newIndex >= 0 && this.filteredChoices[newIndex]?.disabled) {
689
- newIndex--;
690
- }
691
-
692
- if (newIndex < 0) {
693
- if (this.loop) {
694
- newIndex = this.filteredChoices.length - 1;
695
- while (newIndex > 0 && this.filteredChoices[newIndex]?.disabled) {
696
- newIndex--;
697
- }
698
- } else {
699
- return;
700
- }
701
- }
702
-
703
- this.selectedIndex = newIndex;
704
- this.updateScroll();
705
- }
706
-
707
- moveDown() {
708
- if (this.filteredChoices.length === 0) return;
709
-
710
- let newIndex = this.selectedIndex + 1;
711
-
712
- // Skip disabled items
713
- while (newIndex < this.filteredChoices.length && this.filteredChoices[newIndex]?.disabled) {
714
- newIndex++;
715
- }
716
-
717
- if (newIndex >= this.filteredChoices.length) {
718
- if (this.loop) {
719
- newIndex = 0;
720
- while (newIndex < this.filteredChoices.length && this.filteredChoices[newIndex]?.disabled) {
721
- newIndex++;
722
- }
723
- } else {
724
- return;
725
- }
726
- }
727
-
728
- this.selectedIndex = newIndex;
729
- this.updateScroll();
730
- }
731
-
732
- handleKeypress(key, str) {
733
- if (!key) return;
734
-
735
- const name = key.name || "";
736
- const ctrl = key.ctrl || false;
737
-
738
- // Cancel
739
- if (name === "escape" || (ctrl && name === "c")) {
740
- this.cleanup();
741
- return { action: "cancel" };
742
- }
743
-
744
- // Submit
745
- if (name === "return" || name === "enter") {
746
- const selected = this.filteredChoices[this.selectedIndex];
747
- if (selected && !selected.disabled) {
748
- this.cleanup();
749
- return { action: "submit", value: selected.value, choice: selected };
750
- }
751
- }
752
-
753
- // Navigation
754
- if (name === "up" || (ctrl && name === "p")) {
755
- this.moveUp();
756
- this.render();
757
- return { action: "navigate" };
758
- }
759
-
760
- if (name === "down" || (ctrl && name === "n")) {
761
- this.moveDown();
762
- this.render();
763
- return { action: "navigate" };
764
- }
765
-
766
- // Page navigation
767
- if (name === "pageup") {
768
- this.selectedIndex = Math.max(0, this.selectedIndex - this.pageSize);
769
- this.updateScroll();
770
- this.render();
771
- return { action: "navigate" };
772
- }
773
-
774
- if (name === "pagedown") {
775
- this.selectedIndex = Math.min(this.filteredChoices.length - 1, this.selectedIndex + this.pageSize);
776
- this.updateScroll();
777
- this.render();
778
- return { action: "navigate" };
779
- }
780
-
781
- // Home/End
782
- if (name === "home") {
783
- this.selectedIndex = 0;
784
- this.scrollOffset = 0;
785
- this.render();
786
- return { action: "navigate" };
787
- }
788
-
789
- if (name === "end") {
790
- this.selectedIndex = this.filteredChoices.length - 1;
791
- this.updateScroll();
792
- this.render();
793
- return { action: "navigate" };
794
- }
795
-
796
- // Search toggle
797
- if (this.showSearch && str === "/" && !this.searching) {
798
- this.searching = true;
799
- this.render();
800
- return { action: "search" };
801
- }
802
-
803
- // Search input
804
- if (this.searching || this.showSearch) {
805
- if (name === "backspace") {
806
- this.searchQuery = this.searchQuery.slice(0, -1);
807
- this.filterChoices();
808
- this.updateScroll();
809
- this.render();
810
- return { action: "search" };
811
- }
812
-
813
- // Printable character
814
- if (str && str.length === 1 && !ctrl && str !== "/") {
815
- this.searchQuery += str;
816
- this.searching = true;
817
- this.filterChoices();
818
- this.selectedIndex = 0;
819
- this.scrollOffset = 0;
820
- this.render();
821
- return { action: "search" };
822
- }
823
- }
824
-
825
- return { action: "none" };
826
- }
827
-
828
- async run() {
829
- if (!this.stdin.isTTY) {
830
- throw new Error("Interactive prompts require a TTY");
831
- }
832
-
833
- this.setupReadline();
834
- this.hideCursor();
835
- this.render();
836
-
837
- return new Promise((resolve, reject) => {
838
- const onKeypress = (str, key) => {
839
- try {
840
- const result = this.handleKeypress(key, str);
841
-
842
- if (result.action === "submit") {
843
- resolve(result.value);
844
- } else if (result.action === "cancel") {
845
- resolve(null);
846
- }
847
- } catch (err) {
848
- this.cleanup();
849
- reject(err);
850
- }
851
- };
852
-
853
- this.stdin.on("keypress", onKeypress);
854
-
855
- // Cleanup handler
856
- const cleanup = () => {
857
- this.stdin.off("keypress", onKeypress);
858
- };
859
-
860
- this.rl.once("close", () => {
861
- cleanup();
862
- resolve(null);
863
- });
864
- });
865
- }
866
- }
867
-
868
- // ═══════════════════════════════════════════════════════════════════════════════
869
- // MULTI-SELECT PROMPT
870
- // ═══════════════════════════════════════════════════════════════════════════════
871
- class MultiSelectPrompt extends SelectPrompt {
872
- constructor(options = {}) {
873
- super(options);
874
-
875
- this.selected = new Set(options.initial || []);
876
- this.min = options.min || 0;
877
- this.max = options.max || Infinity;
878
- this.required = options.required || false;
879
- }
880
-
881
- renderChoice(choice, isFocused, index) {
882
- const t = this.theme;
883
- const isSelected = this.selected.has(choice.value);
884
-
885
- const pointer = isFocused ? `${t.pointer}${sym.pointer}${c.reset}` : " ";
886
- const checkbox = choice.disabled
887
- ? `${t.muted}${sym.checkbox.disabled}${c.reset}`
888
- : isSelected
889
- ? `${t.success}${sym.checkbox.on}${c.reset}`
890
- : `${t.unselected}${sym.checkbox.off}${c.reset}`;
891
-
892
- const icon = choice.icon ? `${choice.icon} ` : "";
893
-
894
- let label;
895
- if (choice.disabled) {
896
- label = `${t.muted}${icon}${choice.label} (disabled)${c.reset}`;
897
- } else if (isFocused) {
898
- label = `${t.highlight}${icon}${choice.label}${c.reset}`;
899
- } else if (isSelected) {
900
- label = `${t.selected}${icon}${choice.label}${c.reset}`;
901
- } else {
902
- label = `${t.unselected}${icon}${choice.label}${c.reset}`;
903
- }
904
-
905
- let description = "";
906
- if (choice.description && isFocused) {
907
- description = ` ${t.muted}${sym.separator} ${choice.description}${c.reset}`;
908
- }
909
-
910
- return `${pointer} ${checkbox} ${label}${description}`;
911
- }
912
-
913
- renderFooter() {
914
- const t = this.theme;
915
- const lines = [];
916
-
917
- // Selection count
918
- const count = this.selected.size;
919
- const countStr = count === 0
920
- ? "none selected"
921
- : `${count} selected`;
922
-
923
- lines.push("");
924
- lines.push(` ${t.muted}${countStr}${c.reset}`);
925
-
926
- // Navigation hints
927
- lines.push("");
928
- const navHints = [
929
- `${t.muted}space${c.reset} toggle`,
930
- `${t.muted}↑/↓${c.reset} navigate`,
931
- `${t.muted}enter${c.reset} confirm`,
932
- `${t.muted}a${c.reset} all`,
933
- `${t.muted}esc${c.reset} cancel`,
934
- ];
935
- lines.push(` ${navHints.join(" ")}`);
936
-
937
- return lines;
938
- }
939
-
940
- toggleCurrent() {
941
- const choice = this.filteredChoices[this.selectedIndex];
942
- if (!choice || choice.disabled) return;
943
-
944
- if (this.selected.has(choice.value)) {
945
- this.selected.delete(choice.value);
946
- } else {
947
- if (this.selected.size < this.max) {
948
- this.selected.add(choice.value);
949
- }
950
- }
951
- }
952
-
953
- selectAll() {
954
- for (const choice of this.filteredChoices) {
955
- if (!choice.disabled && this.selected.size < this.max) {
956
- this.selected.add(choice.value);
957
- }
958
- }
959
- }
960
-
961
- selectNone() {
962
- this.selected.clear();
963
- }
964
-
965
- handleKeypress(key, str) {
966
- if (!key) return { action: "none" };
967
-
968
- const name = key.name || "";
969
- const ctrl = key.ctrl || false;
970
-
971
- // Cancel
972
- if (name === "escape" || (ctrl && name === "c")) {
973
- this.cleanup();
974
- return { action: "cancel" };
975
- }
976
-
977
- // Submit
978
- if (name === "return" || name === "enter") {
979
- if (this.required && this.selected.size === 0) {
980
- // Don't submit if required and nothing selected
981
- return { action: "none" };
982
- }
983
- if (this.selected.size < this.min) {
984
- return { action: "none" };
985
- }
986
-
987
- this.cleanup();
988
- const selectedChoices = this.choices.filter(c => this.selected.has(c.value));
989
- return {
990
- action: "submit",
991
- value: Array.from(this.selected),
992
- choices: selectedChoices,
993
- };
994
- }
995
-
996
- // Toggle selection
997
- if (name === "space") {
998
- this.toggleCurrent();
999
- this.render();
1000
- return { action: "toggle" };
1001
- }
1002
-
1003
- // Select all
1004
- if (str === "a" && !this.searching) {
1005
- if (this.selected.size === this.filteredChoices.filter(c => !c.disabled).length) {
1006
- this.selectNone();
1007
- } else {
1008
- this.selectAll();
1009
- }
1010
- this.render();
1011
- return { action: "toggle" };
1012
- }
1013
-
1014
- // Inherit navigation from parent
1015
- return super.handleKeypress(key, str);
1016
- }
1017
-
1018
- async run() {
1019
- if (!this.stdin.isTTY) {
1020
- throw new Error("Interactive prompts require a TTY");
1021
- }
1022
-
1023
- this.setupReadline();
1024
- this.hideCursor();
1025
- this.render();
1026
-
1027
- return new Promise((resolve, reject) => {
1028
- const onKeypress = (str, key) => {
1029
- try {
1030
- const result = this.handleKeypress(key, str);
1031
-
1032
- if (result.action === "submit") {
1033
- resolve(result.value);
1034
- } else if (result.action === "cancel") {
1035
- resolve(null);
1036
- }
1037
- } catch (err) {
1038
- this.cleanup();
1039
- reject(err);
1040
- }
1041
- };
1042
-
1043
- this.stdin.on("keypress", onKeypress);
1044
-
1045
- this.rl.once("close", () => {
1046
- this.stdin.off("keypress", onKeypress);
1047
- resolve(null);
1048
- });
1049
- });
1050
- }
1051
- }
1052
-
1053
- // ═══════════════════════════════════════════════════════════════════════════════
1054
- // CONFIRM PROMPT
1055
- // ═══════════════════════════════════════════════════════════════════════════════
1056
- class ConfirmPrompt extends BasePrompt {
1057
- constructor(options = {}) {
1058
- super(options);
1059
-
1060
- this.message = options.message || "Are you sure?";
1061
- this.default = options.default !== false;
1062
- this.active = options.active || "Yes";
1063
- this.inactive = options.inactive || "No";
1064
- this.value = this.default;
1065
- this.renderedLines = 0;
1066
- }
1067
-
1068
- render() {
1069
- if (this.renderedLines > 0) {
1070
- this.clearLines(this.renderedLines);
1071
- }
1072
-
1073
- const t = this.theme;
1074
- const yes = this.value
1075
- ? `${t.success}${c.bold}${c.underline}${this.active}${c.reset}`
1076
- : `${t.muted}${this.active}${c.reset}`;
1077
- const no = !this.value
1078
- ? `${t.error}${c.bold}${c.underline}${this.inactive}${c.reset}`
1079
- : `${t.muted}${this.inactive}${c.reset}`;
1080
-
1081
- const line = `${t.title}${sym.pointer} ${this.message}${c.reset} ${yes} / ${no}`;
1082
-
1083
- this.write(line + "\n");
1084
- this.renderedLines = 1;
1085
- }
1086
-
1087
- handleKeypress(key, str) {
1088
- if (!key) return { action: "none" };
1089
-
1090
- const name = key.name || "";
1091
- const ctrl = key.ctrl || false;
1092
-
1093
- // Cancel
1094
- if (name === "escape" || (ctrl && name === "c")) {
1095
- this.cleanup();
1096
- return { action: "cancel" };
1097
- }
1098
-
1099
- // Submit
1100
- if (name === "return" || name === "enter") {
1101
- this.cleanup();
1102
- return { action: "submit", value: this.value };
1103
- }
1104
-
1105
- // Toggle or direct selection
1106
- if (name === "left" || name === "right" || name === "tab") {
1107
- this.value = !this.value;
1108
- this.render();
1109
- return { action: "toggle" };
1110
- }
1111
-
1112
- // Direct yes/no
1113
- if (str === "y" || str === "Y") {
1114
- this.value = true;
1115
- this.render();
1116
- return { action: "toggle" };
1117
- }
1118
-
1119
- if (str === "n" || str === "N") {
1120
- this.value = false;
1121
- this.render();
1122
- return { action: "toggle" };
1123
- }
1124
-
1125
- return { action: "none" };
1126
- }
1127
-
1128
- async run() {
1129
- if (!this.stdin.isTTY) {
1130
- throw new Error("Interactive prompts require a TTY");
1131
- }
1132
-
1133
- this.setupReadline();
1134
- this.hideCursor();
1135
- this.render();
1136
-
1137
- return new Promise((resolve, reject) => {
1138
- const onKeypress = (str, key) => {
1139
- try {
1140
- const result = this.handleKeypress(key, str);
1141
-
1142
- if (result.action === "submit") {
1143
- resolve(result.value);
1144
- } else if (result.action === "cancel") {
1145
- resolve(null);
1146
- }
1147
- } catch (err) {
1148
- this.cleanup();
1149
- reject(err);
1150
- }
1151
- };
1152
-
1153
- this.stdin.on("keypress", onKeypress);
1154
-
1155
- this.rl.once("close", () => {
1156
- this.stdin.off("keypress", onKeypress);
1157
- resolve(null);
1158
- });
1159
- });
1160
- }
1161
- }
1162
-
1163
- // ═══════════════════════════════════════════════════════════════════════════════
1164
- // INPUT PROMPT
1165
- // ═══════════════════════════════════════════════════════════════════════════════
1166
- class InputPrompt extends BasePrompt {
1167
- constructor(options = {}) {
1168
- super(options);
1169
-
1170
- this.message = options.message || "Enter value";
1171
- this.default = options.default || "";
1172
- this.placeholder = options.placeholder || "";
1173
- this.validate = options.validate || (() => true);
1174
- this.transform = options.transform || ((v) => v);
1175
- this.mask = options.mask || null; // For password input
1176
-
1177
- this.value = "";
1178
- this.cursorPosition = 0;
1179
- this.error = null;
1180
- this.renderedLines = 0;
1181
- }
1182
-
1183
- render() {
1184
- if (this.renderedLines > 0) {
1185
- this.clearLines(this.renderedLines);
1186
- }
1187
-
1188
- const t = this.theme;
1189
- const lines = [];
1190
-
1191
- // Message
1192
- lines.push(`${t.title}${sym.pointer} ${this.message}${c.reset}`);
1193
-
1194
- // Input line
1195
- let displayValue = this.value;
1196
- if (this.mask && this.value) {
1197
- displayValue = this.mask.repeat(this.value.length);
1198
- }
1199
-
1200
- if (!displayValue && this.placeholder) {
1201
- displayValue = `${t.muted}${this.placeholder}${c.reset}`;
1202
- } else if (!displayValue && this.default) {
1203
- displayValue = `${t.muted}(${this.default})${c.reset}`;
1204
- }
1205
-
1206
- const cursor = `${t.primary}▌${c.reset}`;
1207
- lines.push(` ${displayValue}${cursor}`);
1208
-
1209
- // Error
1210
- if (this.error) {
1211
- lines.push(` ${t.error}${sym.error} ${this.error}${c.reset}`);
1212
- }
1213
-
1214
- this.write(lines.join("\n") + "\n");
1215
- this.renderedLines = lines.length;
1216
- }
1217
-
1218
- handleKeypress(key, str) {
1219
- if (!key) return { action: "none" };
1220
-
1221
- const name = key.name || "";
1222
- const ctrl = key.ctrl || false;
1223
-
1224
- // Cancel
1225
- if (name === "escape" || (ctrl && name === "c")) {
1226
- this.cleanup();
1227
- return { action: "cancel" };
1228
- }
1229
-
1230
- // Submit
1231
- if (name === "return" || name === "enter") {
1232
- const finalValue = this.value || this.default;
1233
- const validation = this.validate(finalValue);
1234
-
1235
- if (validation !== true) {
1236
- this.error = typeof validation === "string" ? validation : "Invalid input";
1237
- this.render();
1238
- return { action: "error" };
1239
- }
1240
-
1241
- this.cleanup();
1242
- return { action: "submit", value: this.transform(finalValue) };
1243
- }
1244
-
1245
- // Backspace
1246
- if (name === "backspace") {
1247
- this.value = this.value.slice(0, -1);
1248
- this.error = null;
1249
- this.render();
1250
- return { action: "input" };
1251
- }
1252
-
1253
- // Clear line
1254
- if (ctrl && name === "u") {
1255
- this.value = "";
1256
- this.error = null;
1257
- this.render();
1258
- return { action: "input" };
1259
- }
1260
-
1261
- // Printable character
1262
- if (str && str.length === 1 && !ctrl) {
1263
- this.value += str;
1264
- this.error = null;
1265
- this.render();
1266
- return { action: "input" };
1267
- }
1268
-
1269
- return { action: "none" };
1270
- }
1271
-
1272
- async run() {
1273
- if (!this.stdin.isTTY) {
1274
- throw new Error("Interactive prompts require a TTY");
1275
- }
1276
-
1277
- this.setupReadline();
1278
- this.hideCursor();
1279
- this.render();
1280
-
1281
- return new Promise((resolve, reject) => {
1282
- const onKeypress = (str, key) => {
1283
- try {
1284
- const result = this.handleKeypress(key, str);
1285
-
1286
- if (result.action === "submit") {
1287
- resolve(result.value);
1288
- } else if (result.action === "cancel") {
1289
- resolve(null);
1290
- }
1291
- } catch (err) {
1292
- this.cleanup();
1293
- reject(err);
1294
- }
1295
- };
1296
-
1297
- this.stdin.on("keypress", onKeypress);
1298
-
1299
- this.rl.once("close", () => {
1300
- this.stdin.off("keypress", onKeypress);
1301
- resolve(null);
1302
- });
1303
- });
1304
- }
1305
- }
1306
-
1307
- // ═══════════════════════════════════════════════════════════════════════════════
1308
- // WIZARD (Multi-step form)
1309
- // ═══════════════════════════════════════════════════════════════════════════════
1310
- class Wizard extends BasePrompt {
1311
- constructor(options = {}) {
1312
- super(options);
1313
-
1314
- this.title = options.title || "Setup Wizard";
1315
- this.steps = options.steps || [];
1316
- this.onCancel = options.onCancel || (() => {});
1317
-
1318
- this.currentStep = 0;
1319
- this.answers = {};
1320
- }
1321
-
1322
- renderProgress() {
1323
- const t = this.theme;
1324
- const total = this.steps.length;
1325
- const current = this.currentStep + 1;
1326
-
1327
- // Progress bar
1328
- const barWidth = 30;
1329
- const filled = Math.round((current / total) * barWidth);
1330
- const empty = barWidth - filled;
1331
- const bar = `${t.success}${"█".repeat(filled)}${c.reset}${t.muted}${"░".repeat(empty)}${c.reset}`;
1332
-
1333
- // Step indicators
1334
- const stepIndicators = this.steps.map((step, i) => {
1335
- if (i < this.currentStep) {
1336
- return `${t.success}${sym.success}${c.reset}`;
1337
- } else if (i === this.currentStep) {
1338
- return `${t.primary}${sym.active}${c.reset}`;
1339
- } else {
1340
- return `${t.muted}${sym.pending}${c.reset}`;
1341
- }
1342
- }).join(" ");
1343
-
1344
- console.log();
1345
- console.log(` ${t.title}${this.title}${c.reset}`);
1346
- console.log(` ${bar} ${t.muted}${current}/${total}${c.reset}`);
1347
- console.log(` ${stepIndicators}`);
1348
- console.log();
1349
- }
1350
-
1351
- async runStep(step) {
1352
- const stepConfig = {
1353
- ...step,
1354
- theme: this.theme,
1355
- };
1356
-
1357
- let prompt;
1358
- switch (step.type) {
1359
- case "select":
1360
- prompt = new SelectPrompt(stepConfig);
1361
- break;
1362
- case "multiselect":
1363
- prompt = new MultiSelectPrompt(stepConfig);
1364
- break;
1365
- case "confirm":
1366
- prompt = new ConfirmPrompt(stepConfig);
1367
- break;
1368
- case "input":
1369
- case "text":
1370
- prompt = new InputPrompt(stepConfig);
1371
- break;
1372
- case "password":
1373
- prompt = new InputPrompt({ ...stepConfig, mask: "*" });
1374
- break;
1375
- default:
1376
- throw new Error(`Unknown step type: ${step.type}`);
1377
- }
1378
-
1379
- return await prompt.run();
1380
- }
1381
-
1382
- async run() {
1383
- console.clear();
1384
-
1385
- for (let i = 0; i < this.steps.length; i++) {
1386
- this.currentStep = i;
1387
- const step = this.steps[i];
1388
-
1389
- // Clear and show progress
1390
- console.clear();
1391
- this.renderProgress();
1392
-
1393
- // Run step
1394
- const answer = await this.runStep(step);
1395
-
1396
- if (answer === null) {
1397
- // Cancelled
1398
- this.onCancel();
1399
- return null;
1400
- }
1401
-
1402
- this.answers[step.name || `step_${i}`] = answer;
1403
-
1404
- // Run step callback if provided
1405
- if (step.onAnswer) {
1406
- await step.onAnswer(answer, this.answers);
1407
- }
1408
- }
1409
-
1410
- return this.answers;
1411
- }
1412
- }
1413
-
1414
- // ═══════════════════════════════════════════════════════════════════════════════
1415
- // CONVENIENCE FUNCTIONS
1416
- // ═══════════════════════════════════════════════════════════════════════════════
1417
- async function select(options) {
1418
- const prompt = new SelectPrompt(options);
1419
- return await prompt.run();
1420
- }
1421
-
1422
- async function multiselect(options) {
1423
- const prompt = new MultiSelectPrompt(options);
1424
- return await prompt.run();
1425
- }
1426
-
1427
- async function confirm(options) {
1428
- if (typeof options === "string") {
1429
- options = { message: options };
1430
- }
1431
- const prompt = new ConfirmPrompt(options);
1432
- return await prompt.run();
1433
- }
1434
-
1435
- async function input(options) {
1436
- if (typeof options === "string") {
1437
- options = { message: options };
1438
- }
1439
- const prompt = new InputPrompt(options);
1440
- return await prompt.run();
1441
- }
1442
-
1443
- async function password(options) {
1444
- if (typeof options === "string") {
1445
- options = { message: options };
1446
- }
1447
- const prompt = new InputPrompt({ ...options, mask: "*" });
1448
- return await prompt.run();
1449
- }
1450
-
1451
- async function wizard(options) {
1452
- const w = new Wizard(options);
1453
- return await w.run();
1454
- }
1455
-
1456
- // ═══════════════════════════════════════════════════════════════════════════════
1457
- // EXPORTS
1458
- // ═══════════════════════════════════════════════════════════════════════════════
1459
- module.exports = {
1460
- // Prompts
1461
- SelectPrompt,
1462
- MultiSelectPrompt,
1463
- ConfirmPrompt,
1464
- InputPrompt,
1465
- Wizard,
1466
-
1467
- // Convenience functions
1468
- select,
1469
- multiselect,
1470
- confirm,
1471
- input,
1472
- password,
1473
- wizard,
1474
-
1475
- // Utilities
1476
- gradient,
1477
- drawBox,
1478
- truncate,
1479
- padEnd,
1480
- padStart,
1481
- centerText,
1482
- wrapText,
1483
- stripAnsi,
1484
- getTerminalSize,
1485
-
1486
- // Styling
1487
- ansi,
1488
- c,
1489
- sym,
1490
- themes,
1491
-
1492
- // Constants
1493
- SUPPORTS_COLOR,
1494
- SUPPORTS_TRUECOLOR,
1495
- SUPPORTS_UNICODE,
1496
- };
1
+ /**
2
+ * Interactive Menu System - Premium Terminal UI
3
+ * ═══════════════════════════════════════════════════════════════════════════════
4
+ * Beautiful arrow-key navigation menus for CLI applications
5
+ * No external dependencies - pure Node.js
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const readline = require("readline");
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════════
14
+ // TERMINAL DETECTION
15
+ // ═══════════════════════════════════════════════════════════════════════════════
16
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
17
+ const SUPPORTS_TRUECOLOR = SUPPORTS_COLOR && (
18
+ process.env.COLORTERM === "truecolor" ||
19
+ process.env.TERM_PROGRAM === "iTerm.app" ||
20
+ process.env.TERM_PROGRAM === "Apple_Terminal" ||
21
+ process.env.WT_SESSION
22
+ );
23
+ const SUPPORTS_UNICODE = process.platform !== "win32" || process.env.WT_SESSION || process.env.TERM_PROGRAM;
24
+
25
+ // ═══════════════════════════════════════════════════════════════════════════════
26
+ // ANSI ESCAPE CODES
27
+ // ═══════════════════════════════════════════════════════════════════════════════
28
+ const ESC = "\x1b";
29
+ const CSI = `${ESC}[`;
30
+
31
+ const ansi = {
32
+ // Cursor
33
+ hideCursor: `${CSI}?25l`,
34
+ showCursor: `${CSI}?25h`,
35
+ saveCursor: `${CSI}s`,
36
+ restoreCursor: `${CSI}u`,
37
+ moveTo: (row, col) => `${CSI}${row};${col}H`,
38
+ moveUp: (n = 1) => `${CSI}${n}A`,
39
+ moveDown: (n = 1) => `${CSI}${n}B`,
40
+ moveRight: (n = 1) => `${CSI}${n}C`,
41
+ moveLeft: (n = 1) => `${CSI}${n}D`,
42
+ moveToColumn: (col) => `${CSI}${col}G`,
43
+
44
+ // Clear
45
+ clearLine: `${CSI}2K`,
46
+ clearToEnd: `${CSI}0K`,
47
+ clearToStart: `${CSI}1K`,
48
+ clearScreen: `${CSI}2J`,
49
+ clearDown: `${CSI}0J`,
50
+
51
+ // Styles
52
+ reset: `${CSI}0m`,
53
+ bold: `${CSI}1m`,
54
+ dim: `${CSI}2m`,
55
+ italic: `${CSI}3m`,
56
+ underline: `${CSI}4m`,
57
+ blink: `${CSI}5m`,
58
+ inverse: `${CSI}7m`,
59
+ hidden: `${CSI}8m`,
60
+ strikethrough: `${CSI}9m`,
61
+
62
+ // Colors
63
+ black: `${CSI}30m`,
64
+ red: `${CSI}31m`,
65
+ green: `${CSI}32m`,
66
+ yellow: `${CSI}33m`,
67
+ blue: `${CSI}34m`,
68
+ magenta: `${CSI}35m`,
69
+ cyan: `${CSI}36m`,
70
+ white: `${CSI}37m`,
71
+ gray: `${CSI}90m`,
72
+
73
+ // Bright colors
74
+ brightRed: `${CSI}91m`,
75
+ brightGreen: `${CSI}92m`,
76
+ brightYellow: `${CSI}93m`,
77
+ brightBlue: `${CSI}94m`,
78
+ brightMagenta: `${CSI}95m`,
79
+ brightCyan: `${CSI}96m`,
80
+ brightWhite: `${CSI}97m`,
81
+
82
+ // Background
83
+ bgBlack: `${CSI}40m`,
84
+ bgRed: `${CSI}41m`,
85
+ bgGreen: `${CSI}42m`,
86
+ bgYellow: `${CSI}43m`,
87
+ bgBlue: `${CSI}44m`,
88
+ bgMagenta: `${CSI}45m`,
89
+ bgCyan: `${CSI}46m`,
90
+ bgWhite: `${CSI}47m`,
91
+ bgGray: `${CSI}100m`,
92
+
93
+ // True color
94
+ rgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${CSI}38;2;${r};${g};${b}m` : "",
95
+ bgRgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `${CSI}48;2;${r};${g};${b}m` : "",
96
+ };
97
+
98
+ // Disable colors if not supported
99
+ const c = SUPPORTS_COLOR ? ansi : Object.fromEntries(
100
+ Object.keys(ansi).map(k => [k, typeof ansi[k] === "function" ? () => "" : ""])
101
+ );
102
+
103
+ // ═══════════════════════════════════════════════════════════════════════════════
104
+ // SYMBOLS
105
+ // ═══════════════════════════════════════════════════════════════════════════════
106
+ const sym = SUPPORTS_UNICODE ? {
107
+ // Selection
108
+ pointer: "❯",
109
+ pointerSmall: "›",
110
+ radio: {
111
+ on: "◉",
112
+ off: "○",
113
+ },
114
+ checkbox: {
115
+ on: "☑",
116
+ off: "☐",
117
+ disabled: "▫",
118
+ },
119
+
120
+ // Status
121
+ success: "✓",
122
+ error: "✗",
123
+ warning: "⚠",
124
+ info: "ℹ",
125
+ active: "●",
126
+ pending: "○",
127
+
128
+ // Decorative
129
+ star: "★",
130
+ heart: "♥",
131
+ lightning: "⚡",
132
+ fire: "🔥",
133
+ rocket: "🚀",
134
+ sparkles: "✨",
135
+
136
+ // Arrows
137
+ arrowUp: "↑",
138
+ arrowDown: "↓",
139
+ arrowLeft: "←",
140
+ arrowRight: "→",
141
+ triangleUp: "▲",
142
+ triangleDown: "▼",
143
+ triangleLeft: "◀",
144
+ triangleRight: "▶",
145
+
146
+ // Box drawing
147
+ boxTopLeft: "╭",
148
+ boxTopRight: "╮",
149
+ boxBottomLeft: "╰",
150
+ boxBottomRight: "╯",
151
+ boxHorizontal: "─",
152
+ boxVertical: "│",
153
+ boxHorizontalBold: "━",
154
+ boxVerticalBold: "┃",
155
+
156
+ // Lines
157
+ line: "─",
158
+ lineBold: "━",
159
+ lineDashed: "┄",
160
+ lineDouble: "═",
161
+
162
+ // Misc
163
+ bullet: "•",
164
+ dot: "·",
165
+ ellipsis: "…",
166
+ separator: "│",
167
+ } : {
168
+ pointer: ">",
169
+ pointerSmall: ">",
170
+ radio: { on: "(*)", off: "( )" },
171
+ checkbox: { on: "[x]", off: "[ ]", disabled: "[-]" },
172
+ success: "√",
173
+ error: "×",
174
+ warning: "!",
175
+ info: "i",
176
+ active: "*",
177
+ pending: "o",
178
+ star: "*",
179
+ heart: "<3",
180
+ lightning: "!",
181
+ fire: "(!)",
182
+ rocket: ">>",
183
+ sparkles: "*",
184
+ arrowUp: "^",
185
+ arrowDown: "v",
186
+ arrowLeft: "<",
187
+ arrowRight: ">",
188
+ triangleUp: "^",
189
+ triangleDown: "v",
190
+ triangleLeft: "<",
191
+ triangleRight: ">",
192
+ boxTopLeft: "+",
193
+ boxTopRight: "+",
194
+ boxBottomLeft: "+",
195
+ boxBottomRight: "+",
196
+ boxHorizontal: "-",
197
+ boxVertical: "|",
198
+ boxHorizontalBold: "=",
199
+ boxVerticalBold: "|",
200
+ line: "-",
201
+ lineBold: "=",
202
+ lineDashed: "-",
203
+ lineDouble: "=",
204
+ bullet: "*",
205
+ dot: ".",
206
+ ellipsis: "...",
207
+ separator: "|",
208
+ };
209
+
210
+ // ═══════════════════════════════════════════════════════════════════════════════
211
+ // COLOR THEMES
212
+ // ═══════════════════════════════════════════════════════════════════════════════
213
+ const themes = {
214
+ default: {
215
+ primary: c.cyan,
216
+ secondary: c.magenta,
217
+ accent: c.yellow,
218
+ success: c.green,
219
+ warning: c.yellow,
220
+ error: c.red,
221
+ muted: c.gray,
222
+ highlight: c.brightWhite,
223
+ pointer: c.cyan,
224
+ selected: c.rgb(0, 255, 200),
225
+ unselected: c.gray,
226
+ title: c.bold + c.brightWhite,
227
+ subtitle: c.dim,
228
+ border: c.dim,
229
+ search: c.yellow,
230
+ },
231
+
232
+ vibecheck: {
233
+ primary: c.rgb(0, 255, 255), // Cyan
234
+ secondary: c.rgb(138, 43, 226), // Purple
235
+ accent: c.rgb(255, 200, 100), // Gold
236
+ success: c.rgb(0, 255, 150), // Green
237
+ warning: c.rgb(255, 200, 0), // Yellow
238
+ error: c.rgb(255, 80, 80), // Red
239
+ muted: c.rgb(120, 120, 120), // Gray
240
+ highlight: c.rgb(255, 255, 255), // White
241
+ pointer: c.rgb(0, 255, 255), // Cyan
242
+ selected: c.rgb(0, 255, 200), // Bright cyan-green
243
+ unselected: c.rgb(100, 100, 100), // Dark gray
244
+ title: c.bold + c.rgb(255, 255, 255),
245
+ subtitle: c.rgb(150, 150, 150),
246
+ border: c.rgb(60, 60, 80),
247
+ search: c.rgb(255, 200, 100),
248
+ },
249
+
250
+ minimal: {
251
+ primary: c.white,
252
+ secondary: c.gray,
253
+ accent: c.white,
254
+ success: c.green,
255
+ warning: c.yellow,
256
+ error: c.red,
257
+ muted: c.dim,
258
+ highlight: c.bold,
259
+ pointer: c.white,
260
+ selected: c.white,
261
+ unselected: c.dim,
262
+ title: c.bold,
263
+ subtitle: c.dim,
264
+ border: c.dim,
265
+ search: c.white,
266
+ },
267
+ };
268
+
269
+ // ═══════════════════════════════════════════════════════════════════════════════
270
+ // UTILITY FUNCTIONS
271
+ // ═══════════════════════════════════════════════════════════════════════════════
272
+ function stripAnsi(str) {
273
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
274
+ }
275
+
276
+ function truncate(str, maxLen, suffix = "…") {
277
+ const stripped = stripAnsi(str);
278
+ if (stripped.length <= maxLen) return str;
279
+
280
+ // Simple truncation (doesn't preserve ANSI, but good enough for most cases)
281
+ return stripped.slice(0, maxLen - suffix.length) + suffix;
282
+ }
283
+
284
+ function padEnd(str, len, char = " ") {
285
+ const stripped = stripAnsi(str);
286
+ const padding = Math.max(0, len - stripped.length);
287
+ return str + char.repeat(padding);
288
+ }
289
+
290
+ function padStart(str, len, char = " ") {
291
+ const stripped = stripAnsi(str);
292
+ const padding = Math.max(0, len - stripped.length);
293
+ return char.repeat(padding) + str;
294
+ }
295
+
296
+ function centerText(str, width, char = " ") {
297
+ const stripped = stripAnsi(str);
298
+ const padding = Math.max(0, width - stripped.length);
299
+ const left = Math.floor(padding / 2);
300
+ const right = padding - left;
301
+ return char.repeat(left) + str + char.repeat(right);
302
+ }
303
+
304
+ function wrapText(str, width) {
305
+ const words = str.split(" ");
306
+ const lines = [];
307
+ let currentLine = "";
308
+
309
+ for (const word of words) {
310
+ if (currentLine.length + word.length + 1 <= width) {
311
+ currentLine += (currentLine ? " " : "") + word;
312
+ } else {
313
+ if (currentLine) lines.push(currentLine);
314
+ currentLine = word;
315
+ }
316
+ }
317
+ if (currentLine) lines.push(currentLine);
318
+
319
+ return lines;
320
+ }
321
+
322
+ function getTerminalSize() {
323
+ return {
324
+ columns: process.stdout.columns || 80,
325
+ rows: process.stdout.rows || 24,
326
+ };
327
+ }
328
+
329
+ // ═══════════════════════════════════════════════════════════════════════════════
330
+ // GRADIENT TEXT
331
+ // ═══════════════════════════════════════════════════════════════════════════════
332
+ function gradient(text, colors = [[0, 255, 255], [138, 43, 226], [255, 20, 147]]) {
333
+ if (!SUPPORTS_TRUECOLOR) return `${c.cyan}${text}${c.reset}`;
334
+
335
+ const chars = [...text];
336
+ const len = chars.length;
337
+ if (len === 0) return text;
338
+
339
+ return chars.map((char, i) => {
340
+ const t = i / Math.max(len - 1, 1);
341
+ const segmentLen = colors.length - 1;
342
+ const segment = Math.min(Math.floor(t * segmentLen), segmentLen - 1);
343
+ const localT = (t * segmentLen) - segment;
344
+ const c1 = colors[segment];
345
+ const c2 = colors[segment + 1] || c1;
346
+ const r = Math.round(c1[0] + (c2[0] - c1[0]) * localT);
347
+ const g = Math.round(c1[1] + (c2[1] - c1[1]) * localT);
348
+ const b = Math.round(c1[2] + (c2[2] - c1[2]) * localT);
349
+ return `${c.rgb(r, g, b)}${char}`;
350
+ }).join("") + c.reset;
351
+ }
352
+
353
+ // ═══════════════════════════════════════════════════════════════════════════════
354
+ // BOX DRAWING
355
+ // ═══════════════════════════════════════════════════════════════════════════════
356
+ function drawBox(content, options = {}) {
357
+ const {
358
+ title = "",
359
+ width = null,
360
+ padding = 1,
361
+ borderColor = c.dim,
362
+ titleColor = c.bold,
363
+ style = "rounded", // rounded, square, double, bold
364
+ } = options;
365
+
366
+ const borders = {
367
+ rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
368
+ square: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
369
+ double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║" },
370
+ bold: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
371
+ };
372
+
373
+ const b = borders[style] || borders.rounded;
374
+ const lines = Array.isArray(content) ? content : content.split("\n");
375
+ const maxContentWidth = Math.max(...lines.map(l => stripAnsi(l).length));
376
+ const boxWidth = width || maxContentWidth + (padding * 2) + 2;
377
+ const innerWidth = boxWidth - 2;
378
+
379
+ const output = [];
380
+
381
+ // Top border with optional title
382
+ if (title) {
383
+ const titleStr = ` ${title} `;
384
+ const titleLen = stripAnsi(titleStr).length;
385
+ const leftBorder = Math.floor((innerWidth - titleLen) / 2);
386
+ const rightBorder = innerWidth - titleLen - leftBorder;
387
+ output.push(
388
+ `${borderColor}${b.tl}${b.h.repeat(leftBorder)}${c.reset}${titleColor}${titleStr}${c.reset}${borderColor}${b.h.repeat(rightBorder)}${b.tr}${c.reset}`
389
+ );
390
+ } else {
391
+ output.push(`${borderColor}${b.tl}${b.h.repeat(innerWidth)}${b.tr}${c.reset}`);
392
+ }
393
+
394
+ // Padding top
395
+ for (let i = 0; i < padding; i++) {
396
+ output.push(`${borderColor}${b.v}${c.reset}${" ".repeat(innerWidth)}${borderColor}${b.v}${c.reset}`);
397
+ }
398
+
399
+ // Content
400
+ for (const line of lines) {
401
+ const stripped = stripAnsi(line);
402
+ const contentPadding = " ".repeat(padding);
403
+ const rightPad = " ".repeat(Math.max(0, innerWidth - stripped.length - (padding * 2)));
404
+ output.push(`${borderColor}${b.v}${c.reset}${contentPadding}${line}${rightPad}${contentPadding}${borderColor}${b.v}${c.reset}`);
405
+ }
406
+
407
+ // Padding bottom
408
+ for (let i = 0; i < padding; i++) {
409
+ output.push(`${borderColor}${b.v}${c.reset}${" ".repeat(innerWidth)}${borderColor}${b.v}${c.reset}`);
410
+ }
411
+
412
+ // Bottom border
413
+ output.push(`${borderColor}${b.bl}${b.h.repeat(innerWidth)}${b.br}${c.reset}`);
414
+
415
+ return output.join("\n");
416
+ }
417
+
418
+ // ═══════════════════════════════════════════════════════════════════════════════
419
+ // BASE PROMPT CLASS
420
+ // ═══════════════════════════════════════════════════════════════════════════════
421
+ class BasePrompt {
422
+ constructor(options = {}) {
423
+ this.theme = themes[options.theme] || themes.vibecheck;
424
+ this.stdin = options.stdin || process.stdin;
425
+ this.stdout = options.stdout || process.stdout;
426
+ this.rl = null;
427
+ this.closed = false;
428
+ }
429
+
430
+ write(str) {
431
+ this.stdout.write(str);
432
+ }
433
+
434
+ clearLines(count) {
435
+ for (let i = 0; i < count; i++) {
436
+ this.write(ansi.moveUp(1) + ansi.clearLine);
437
+ }
438
+ }
439
+
440
+ hideCursor() {
441
+ this.write(ansi.hideCursor);
442
+ }
443
+
444
+ showCursor() {
445
+ this.write(ansi.showCursor);
446
+ }
447
+
448
+ setupReadline() {
449
+ if (this.rl) return;
450
+
451
+ this.rl = readline.createInterface({
452
+ input: this.stdin,
453
+ output: this.stdout,
454
+ terminal: true,
455
+ });
456
+
457
+ // Enable keypress events
458
+ readline.emitKeypressEvents(this.stdin, this.rl);
459
+
460
+ if (this.stdin.isTTY) {
461
+ this.stdin.setRawMode(true);
462
+ }
463
+ }
464
+
465
+ cleanup() {
466
+ if (this.closed) return;
467
+ this.closed = true;
468
+
469
+ this.showCursor();
470
+
471
+ if (this.stdin.isTTY) {
472
+ this.stdin.setRawMode(false);
473
+ }
474
+
475
+ if (this.rl) {
476
+ this.rl.close();
477
+ this.rl = null;
478
+ }
479
+ }
480
+ }
481
+
482
+ // ═══════════════════════════════════════════════════════════════════════════════
483
+ // SELECT PROMPT - Single selection from list
484
+ // ═══════════════════════════════════════════════════════════════════════════════
485
+ class SelectPrompt extends BasePrompt {
486
+ constructor(options = {}) {
487
+ super(options);
488
+
489
+ this.message = options.message || "Select an option";
490
+ this.choices = this.normalizeChoices(options.choices || []);
491
+ this.pageSize = options.pageSize || 10;
492
+ this.loop = options.loop !== false;
493
+ this.showSearch = options.search !== false;
494
+ this.hint = options.hint || "";
495
+ this.initialIndex = options.initial || 0;
496
+
497
+ // State
498
+ this.selectedIndex = this.initialIndex;
499
+ this.searchQuery = "";
500
+ this.filteredChoices = [...this.choices];
501
+ this.scrollOffset = 0;
502
+ this.searching = false;
503
+ this.renderedLines = 0;
504
+ }
505
+
506
+ normalizeChoices(choices) {
507
+ return choices.map((choice, index) => {
508
+ if (typeof choice === "string") {
509
+ return { value: choice, label: choice, index };
510
+ }
511
+ return {
512
+ value: choice.value ?? choice.label ?? choice.name,
513
+ label: choice.label ?? choice.name ?? choice.value,
514
+ description: choice.description || choice.desc || "",
515
+ disabled: choice.disabled || false,
516
+ group: choice.group || null,
517
+ icon: choice.icon || null,
518
+ hint: choice.hint || "",
519
+ index,
520
+ };
521
+ });
522
+ }
523
+
524
+ filterChoices() {
525
+ if (!this.searchQuery) {
526
+ this.filteredChoices = [...this.choices];
527
+ return;
528
+ }
529
+
530
+ const query = this.searchQuery.toLowerCase();
531
+ this.filteredChoices = this.choices.filter(choice => {
532
+ const label = (choice.label || "").toLowerCase();
533
+ const description = (choice.description || "").toLowerCase();
534
+ return label.includes(query) || description.includes(query);
535
+ });
536
+
537
+ // Reset selection if current is out of bounds
538
+ if (this.selectedIndex >= this.filteredChoices.length) {
539
+ this.selectedIndex = Math.max(0, this.filteredChoices.length - 1);
540
+ }
541
+ }
542
+
543
+ getVisibleChoices() {
544
+ const start = this.scrollOffset;
545
+ const end = Math.min(start + this.pageSize, this.filteredChoices.length);
546
+ return this.filteredChoices.slice(start, end);
547
+ }
548
+
549
+ updateScroll() {
550
+ // Ensure selected item is visible
551
+ if (this.selectedIndex < this.scrollOffset) {
552
+ this.scrollOffset = this.selectedIndex;
553
+ } else if (this.selectedIndex >= this.scrollOffset + this.pageSize) {
554
+ this.scrollOffset = this.selectedIndex - this.pageSize + 1;
555
+ }
556
+ }
557
+
558
+ renderChoice(choice, isSelected, index) {
559
+ const t = this.theme;
560
+ const pointer = isSelected ? `${t.pointer}${sym.pointer}${c.reset}` : " ";
561
+ const icon = choice.icon ? `${choice.icon} ` : "";
562
+
563
+ let label;
564
+ if (choice.disabled) {
565
+ label = `${t.muted}${icon}${choice.label} (disabled)${c.reset}`;
566
+ } else if (isSelected) {
567
+ label = `${t.selected}${c.bold}${icon}${choice.label}${c.reset}`;
568
+ } else {
569
+ label = `${t.unselected}${icon}${choice.label}${c.reset}`;
570
+ }
571
+
572
+ let description = "";
573
+ if (choice.description && isSelected) {
574
+ description = ` ${t.muted}${sym.separator} ${choice.description}${c.reset}`;
575
+ }
576
+
577
+ let hint = "";
578
+ if (choice.hint && isSelected) {
579
+ hint = ` ${t.accent}${choice.hint}${c.reset}`;
580
+ }
581
+
582
+ return `${pointer} ${label}${description}${hint}`;
583
+ }
584
+
585
+ renderHeader() {
586
+ const t = this.theme;
587
+ const lines = [];
588
+
589
+ // Title
590
+ lines.push(`${t.title}${sym.pointer} ${this.message}${c.reset}`);
591
+
592
+ // Search box
593
+ if (this.showSearch) {
594
+ const searchIcon = this.searching ? `${t.search}${sym.pointer}${c.reset}` : `${t.muted}/${c.reset}`;
595
+ const query = this.searchQuery || (this.searching ? "" : "type to search");
596
+ const queryStyle = this.searchQuery ? t.search : t.muted;
597
+ lines.push(` ${searchIcon} ${queryStyle}${query}${c.reset}`);
598
+ }
599
+
600
+ // Hint
601
+ if (this.hint && !this.searching) {
602
+ lines.push(` ${t.muted}${this.hint}${c.reset}`);
603
+ }
604
+
605
+ lines.push("");
606
+
607
+ return lines;
608
+ }
609
+
610
+ renderFooter() {
611
+ const t = this.theme;
612
+ const lines = [];
613
+
614
+ // Scroll indicators
615
+ const total = this.filteredChoices.length;
616
+ const showing = Math.min(this.pageSize, total);
617
+ const hasMore = total > this.pageSize;
618
+
619
+ if (hasMore) {
620
+ const scrollInfo = `${this.scrollOffset + 1}-${this.scrollOffset + showing} of ${total}`;
621
+ lines.push("");
622
+ lines.push(` ${t.muted}${scrollInfo}${c.reset}`);
623
+ }
624
+
625
+ // Navigation hints
626
+ lines.push("");
627
+ const navHints = [
628
+ `${t.muted}↑/↓${c.reset} navigate`,
629
+ `${t.muted}enter${c.reset} select`,
630
+ ];
631
+ if (this.showSearch) {
632
+ navHints.unshift(`${t.muted}/${c.reset} search`);
633
+ }
634
+ navHints.push(`${t.muted}esc${c.reset} cancel`);
635
+ lines.push(` ${navHints.join(" ")}`);
636
+
637
+ return lines;
638
+ }
639
+
640
+ render() {
641
+ // Clear previous render
642
+ if (this.renderedLines > 0) {
643
+ this.clearLines(this.renderedLines);
644
+ }
645
+
646
+ const lines = [];
647
+
648
+ // Header
649
+ lines.push(...this.renderHeader());
650
+
651
+ // Groups handling
652
+ let currentGroup = null;
653
+ const visibleChoices = this.getVisibleChoices();
654
+
655
+ for (let i = 0; i < visibleChoices.length; i++) {
656
+ const choice = visibleChoices[i];
657
+ const globalIndex = this.scrollOffset + i;
658
+ const isSelected = globalIndex === this.selectedIndex;
659
+
660
+ // Group header
661
+ if (choice.group && choice.group !== currentGroup) {
662
+ currentGroup = choice.group;
663
+ lines.push(` ${this.theme.subtitle}${sym.bullet} ${currentGroup}${c.reset}`);
664
+ }
665
+
666
+ lines.push(this.renderChoice(choice, isSelected, globalIndex));
667
+ }
668
+
669
+ // Empty state
670
+ if (visibleChoices.length === 0) {
671
+ lines.push(` ${this.theme.muted}No matches found${c.reset}`);
672
+ }
673
+
674
+ // Footer
675
+ lines.push(...this.renderFooter());
676
+
677
+ // Write output
678
+ this.write(lines.join("\n") + "\n");
679
+ this.renderedLines = lines.length;
680
+ }
681
+
682
+ moveUp() {
683
+ if (this.filteredChoices.length === 0) return;
684
+
685
+ let newIndex = this.selectedIndex - 1;
686
+
687
+ // Skip disabled items
688
+ while (newIndex >= 0 && this.filteredChoices[newIndex]?.disabled) {
689
+ newIndex--;
690
+ }
691
+
692
+ if (newIndex < 0) {
693
+ if (this.loop) {
694
+ newIndex = this.filteredChoices.length - 1;
695
+ while (newIndex > 0 && this.filteredChoices[newIndex]?.disabled) {
696
+ newIndex--;
697
+ }
698
+ } else {
699
+ return;
700
+ }
701
+ }
702
+
703
+ this.selectedIndex = newIndex;
704
+ this.updateScroll();
705
+ }
706
+
707
+ moveDown() {
708
+ if (this.filteredChoices.length === 0) return;
709
+
710
+ let newIndex = this.selectedIndex + 1;
711
+
712
+ // Skip disabled items
713
+ while (newIndex < this.filteredChoices.length && this.filteredChoices[newIndex]?.disabled) {
714
+ newIndex++;
715
+ }
716
+
717
+ if (newIndex >= this.filteredChoices.length) {
718
+ if (this.loop) {
719
+ newIndex = 0;
720
+ while (newIndex < this.filteredChoices.length && this.filteredChoices[newIndex]?.disabled) {
721
+ newIndex++;
722
+ }
723
+ } else {
724
+ return;
725
+ }
726
+ }
727
+
728
+ this.selectedIndex = newIndex;
729
+ this.updateScroll();
730
+ }
731
+
732
+ handleKeypress(key, str) {
733
+ if (!key) return;
734
+
735
+ const name = key.name || "";
736
+ const ctrl = key.ctrl || false;
737
+
738
+ // Cancel
739
+ if (name === "escape" || (ctrl && name === "c")) {
740
+ this.cleanup();
741
+ return { action: "cancel" };
742
+ }
743
+
744
+ // Submit
745
+ if (name === "return" || name === "enter") {
746
+ const selected = this.filteredChoices[this.selectedIndex];
747
+ if (selected && !selected.disabled) {
748
+ this.cleanup();
749
+ return { action: "submit", value: selected.value, choice: selected };
750
+ }
751
+ }
752
+
753
+ // Navigation
754
+ if (name === "up" || (ctrl && name === "p")) {
755
+ this.moveUp();
756
+ this.render();
757
+ return { action: "navigate" };
758
+ }
759
+
760
+ if (name === "down" || (ctrl && name === "n")) {
761
+ this.moveDown();
762
+ this.render();
763
+ return { action: "navigate" };
764
+ }
765
+
766
+ // Page navigation
767
+ if (name === "pageup") {
768
+ this.selectedIndex = Math.max(0, this.selectedIndex - this.pageSize);
769
+ this.updateScroll();
770
+ this.render();
771
+ return { action: "navigate" };
772
+ }
773
+
774
+ if (name === "pagedown") {
775
+ this.selectedIndex = Math.min(this.filteredChoices.length - 1, this.selectedIndex + this.pageSize);
776
+ this.updateScroll();
777
+ this.render();
778
+ return { action: "navigate" };
779
+ }
780
+
781
+ // Home/End
782
+ if (name === "home") {
783
+ this.selectedIndex = 0;
784
+ this.scrollOffset = 0;
785
+ this.render();
786
+ return { action: "navigate" };
787
+ }
788
+
789
+ if (name === "end") {
790
+ this.selectedIndex = this.filteredChoices.length - 1;
791
+ this.updateScroll();
792
+ this.render();
793
+ return { action: "navigate" };
794
+ }
795
+
796
+ // Search toggle
797
+ if (this.showSearch && str === "/" && !this.searching) {
798
+ this.searching = true;
799
+ this.render();
800
+ return { action: "search" };
801
+ }
802
+
803
+ // Search input
804
+ if (this.searching || this.showSearch) {
805
+ if (name === "backspace") {
806
+ this.searchQuery = this.searchQuery.slice(0, -1);
807
+ this.filterChoices();
808
+ this.updateScroll();
809
+ this.render();
810
+ return { action: "search" };
811
+ }
812
+
813
+ // Printable character
814
+ if (str && str.length === 1 && !ctrl && str !== "/") {
815
+ this.searchQuery += str;
816
+ this.searching = true;
817
+ this.filterChoices();
818
+ this.selectedIndex = 0;
819
+ this.scrollOffset = 0;
820
+ this.render();
821
+ return { action: "search" };
822
+ }
823
+ }
824
+
825
+ return { action: "none" };
826
+ }
827
+
828
+ async run() {
829
+ if (!this.stdin.isTTY) {
830
+ throw new Error("Interactive prompts require a TTY");
831
+ }
832
+
833
+ this.setupReadline();
834
+ this.hideCursor();
835
+ this.render();
836
+
837
+ return new Promise((resolve, reject) => {
838
+ const onKeypress = (str, key) => {
839
+ try {
840
+ const result = this.handleKeypress(key, str);
841
+
842
+ if (result.action === "submit") {
843
+ resolve(result.value);
844
+ } else if (result.action === "cancel") {
845
+ resolve(null);
846
+ }
847
+ } catch (err) {
848
+ this.cleanup();
849
+ reject(err);
850
+ }
851
+ };
852
+
853
+ this.stdin.on("keypress", onKeypress);
854
+
855
+ // Cleanup handler
856
+ const cleanup = () => {
857
+ this.stdin.off("keypress", onKeypress);
858
+ };
859
+
860
+ this.rl.once("close", () => {
861
+ cleanup();
862
+ resolve(null);
863
+ });
864
+ });
865
+ }
866
+ }
867
+
868
+ // ═══════════════════════════════════════════════════════════════════════════════
869
+ // MULTI-SELECT PROMPT
870
+ // ═══════════════════════════════════════════════════════════════════════════════
871
+ class MultiSelectPrompt extends SelectPrompt {
872
+ constructor(options = {}) {
873
+ super(options);
874
+
875
+ this.selected = new Set(options.initial || []);
876
+ this.min = options.min || 0;
877
+ this.max = options.max || Infinity;
878
+ this.required = options.required || false;
879
+ }
880
+
881
+ renderChoice(choice, isFocused, index) {
882
+ const t = this.theme;
883
+ const isSelected = this.selected.has(choice.value);
884
+
885
+ const pointer = isFocused ? `${t.pointer}${sym.pointer}${c.reset}` : " ";
886
+ const checkbox = choice.disabled
887
+ ? `${t.muted}${sym.checkbox.disabled}${c.reset}`
888
+ : isSelected
889
+ ? `${t.success}${sym.checkbox.on}${c.reset}`
890
+ : `${t.unselected}${sym.checkbox.off}${c.reset}`;
891
+
892
+ const icon = choice.icon ? `${choice.icon} ` : "";
893
+
894
+ let label;
895
+ if (choice.disabled) {
896
+ label = `${t.muted}${icon}${choice.label} (disabled)${c.reset}`;
897
+ } else if (isFocused) {
898
+ label = `${t.highlight}${icon}${choice.label}${c.reset}`;
899
+ } else if (isSelected) {
900
+ label = `${t.selected}${icon}${choice.label}${c.reset}`;
901
+ } else {
902
+ label = `${t.unselected}${icon}${choice.label}${c.reset}`;
903
+ }
904
+
905
+ let description = "";
906
+ if (choice.description && isFocused) {
907
+ description = ` ${t.muted}${sym.separator} ${choice.description}${c.reset}`;
908
+ }
909
+
910
+ return `${pointer} ${checkbox} ${label}${description}`;
911
+ }
912
+
913
+ renderFooter() {
914
+ const t = this.theme;
915
+ const lines = [];
916
+
917
+ // Selection count
918
+ const count = this.selected.size;
919
+ const countStr = count === 0
920
+ ? "none selected"
921
+ : `${count} selected`;
922
+
923
+ lines.push("");
924
+ lines.push(` ${t.muted}${countStr}${c.reset}`);
925
+
926
+ // Navigation hints
927
+ lines.push("");
928
+ const navHints = [
929
+ `${t.muted}space${c.reset} toggle`,
930
+ `${t.muted}↑/↓${c.reset} navigate`,
931
+ `${t.muted}enter${c.reset} confirm`,
932
+ `${t.muted}a${c.reset} all`,
933
+ `${t.muted}esc${c.reset} cancel`,
934
+ ];
935
+ lines.push(` ${navHints.join(" ")}`);
936
+
937
+ return lines;
938
+ }
939
+
940
+ toggleCurrent() {
941
+ const choice = this.filteredChoices[this.selectedIndex];
942
+ if (!choice || choice.disabled) return;
943
+
944
+ if (this.selected.has(choice.value)) {
945
+ this.selected.delete(choice.value);
946
+ } else {
947
+ if (this.selected.size < this.max) {
948
+ this.selected.add(choice.value);
949
+ }
950
+ }
951
+ }
952
+
953
+ selectAll() {
954
+ for (const choice of this.filteredChoices) {
955
+ if (!choice.disabled && this.selected.size < this.max) {
956
+ this.selected.add(choice.value);
957
+ }
958
+ }
959
+ }
960
+
961
+ selectNone() {
962
+ this.selected.clear();
963
+ }
964
+
965
+ handleKeypress(key, str) {
966
+ if (!key) return { action: "none" };
967
+
968
+ const name = key.name || "";
969
+ const ctrl = key.ctrl || false;
970
+
971
+ // Cancel
972
+ if (name === "escape" || (ctrl && name === "c")) {
973
+ this.cleanup();
974
+ return { action: "cancel" };
975
+ }
976
+
977
+ // Submit
978
+ if (name === "return" || name === "enter") {
979
+ if (this.required && this.selected.size === 0) {
980
+ // Don't submit if required and nothing selected
981
+ return { action: "none" };
982
+ }
983
+ if (this.selected.size < this.min) {
984
+ return { action: "none" };
985
+ }
986
+
987
+ this.cleanup();
988
+ const selectedChoices = this.choices.filter(c => this.selected.has(c.value));
989
+ return {
990
+ action: "submit",
991
+ value: Array.from(this.selected),
992
+ choices: selectedChoices,
993
+ };
994
+ }
995
+
996
+ // Toggle selection
997
+ if (name === "space") {
998
+ this.toggleCurrent();
999
+ this.render();
1000
+ return { action: "toggle" };
1001
+ }
1002
+
1003
+ // Select all
1004
+ if (str === "a" && !this.searching) {
1005
+ if (this.selected.size === this.filteredChoices.filter(c => !c.disabled).length) {
1006
+ this.selectNone();
1007
+ } else {
1008
+ this.selectAll();
1009
+ }
1010
+ this.render();
1011
+ return { action: "toggle" };
1012
+ }
1013
+
1014
+ // Inherit navigation from parent
1015
+ return super.handleKeypress(key, str);
1016
+ }
1017
+
1018
+ async run() {
1019
+ if (!this.stdin.isTTY) {
1020
+ throw new Error("Interactive prompts require a TTY");
1021
+ }
1022
+
1023
+ this.setupReadline();
1024
+ this.hideCursor();
1025
+ this.render();
1026
+
1027
+ return new Promise((resolve, reject) => {
1028
+ const onKeypress = (str, key) => {
1029
+ try {
1030
+ const result = this.handleKeypress(key, str);
1031
+
1032
+ if (result.action === "submit") {
1033
+ resolve(result.value);
1034
+ } else if (result.action === "cancel") {
1035
+ resolve(null);
1036
+ }
1037
+ } catch (err) {
1038
+ this.cleanup();
1039
+ reject(err);
1040
+ }
1041
+ };
1042
+
1043
+ this.stdin.on("keypress", onKeypress);
1044
+
1045
+ this.rl.once("close", () => {
1046
+ this.stdin.off("keypress", onKeypress);
1047
+ resolve(null);
1048
+ });
1049
+ });
1050
+ }
1051
+ }
1052
+
1053
+ // ═══════════════════════════════════════════════════════════════════════════════
1054
+ // CONFIRM PROMPT
1055
+ // ═══════════════════════════════════════════════════════════════════════════════
1056
+ class ConfirmPrompt extends BasePrompt {
1057
+ constructor(options = {}) {
1058
+ super(options);
1059
+
1060
+ this.message = options.message || "Are you sure?";
1061
+ this.default = options.default !== false;
1062
+ this.active = options.active || "Yes";
1063
+ this.inactive = options.inactive || "No";
1064
+ this.value = this.default;
1065
+ this.renderedLines = 0;
1066
+ }
1067
+
1068
+ render() {
1069
+ if (this.renderedLines > 0) {
1070
+ this.clearLines(this.renderedLines);
1071
+ }
1072
+
1073
+ const t = this.theme;
1074
+ const yes = this.value
1075
+ ? `${t.success}${c.bold}${c.underline}${this.active}${c.reset}`
1076
+ : `${t.muted}${this.active}${c.reset}`;
1077
+ const no = !this.value
1078
+ ? `${t.error}${c.bold}${c.underline}${this.inactive}${c.reset}`
1079
+ : `${t.muted}${this.inactive}${c.reset}`;
1080
+
1081
+ const line = `${t.title}${sym.pointer} ${this.message}${c.reset} ${yes} / ${no}`;
1082
+
1083
+ this.write(line + "\n");
1084
+ this.renderedLines = 1;
1085
+ }
1086
+
1087
+ handleKeypress(key, str) {
1088
+ if (!key) return { action: "none" };
1089
+
1090
+ const name = key.name || "";
1091
+ const ctrl = key.ctrl || false;
1092
+
1093
+ // Cancel
1094
+ if (name === "escape" || (ctrl && name === "c")) {
1095
+ this.cleanup();
1096
+ return { action: "cancel" };
1097
+ }
1098
+
1099
+ // Submit
1100
+ if (name === "return" || name === "enter") {
1101
+ this.cleanup();
1102
+ return { action: "submit", value: this.value };
1103
+ }
1104
+
1105
+ // Toggle or direct selection
1106
+ if (name === "left" || name === "right" || name === "tab") {
1107
+ this.value = !this.value;
1108
+ this.render();
1109
+ return { action: "toggle" };
1110
+ }
1111
+
1112
+ // Direct yes/no
1113
+ if (str === "y" || str === "Y") {
1114
+ this.value = true;
1115
+ this.render();
1116
+ return { action: "toggle" };
1117
+ }
1118
+
1119
+ if (str === "n" || str === "N") {
1120
+ this.value = false;
1121
+ this.render();
1122
+ return { action: "toggle" };
1123
+ }
1124
+
1125
+ return { action: "none" };
1126
+ }
1127
+
1128
+ async run() {
1129
+ if (!this.stdin.isTTY) {
1130
+ throw new Error("Interactive prompts require a TTY");
1131
+ }
1132
+
1133
+ this.setupReadline();
1134
+ this.hideCursor();
1135
+ this.render();
1136
+
1137
+ return new Promise((resolve, reject) => {
1138
+ const onKeypress = (str, key) => {
1139
+ try {
1140
+ const result = this.handleKeypress(key, str);
1141
+
1142
+ if (result.action === "submit") {
1143
+ resolve(result.value);
1144
+ } else if (result.action === "cancel") {
1145
+ resolve(null);
1146
+ }
1147
+ } catch (err) {
1148
+ this.cleanup();
1149
+ reject(err);
1150
+ }
1151
+ };
1152
+
1153
+ this.stdin.on("keypress", onKeypress);
1154
+
1155
+ this.rl.once("close", () => {
1156
+ this.stdin.off("keypress", onKeypress);
1157
+ resolve(null);
1158
+ });
1159
+ });
1160
+ }
1161
+ }
1162
+
1163
+ // ═══════════════════════════════════════════════════════════════════════════════
1164
+ // INPUT PROMPT
1165
+ // ═══════════════════════════════════════════════════════════════════════════════
1166
+ class InputPrompt extends BasePrompt {
1167
+ constructor(options = {}) {
1168
+ super(options);
1169
+
1170
+ this.message = options.message || "Enter value";
1171
+ this.default = options.default || "";
1172
+ this.placeholder = options.placeholder || "";
1173
+ this.validate = options.validate || (() => true);
1174
+ this.transform = options.transform || ((v) => v);
1175
+ this.mask = options.mask || null; // For password input
1176
+
1177
+ this.value = "";
1178
+ this.cursorPosition = 0;
1179
+ this.error = null;
1180
+ this.renderedLines = 0;
1181
+ }
1182
+
1183
+ render() {
1184
+ if (this.renderedLines > 0) {
1185
+ this.clearLines(this.renderedLines);
1186
+ }
1187
+
1188
+ const t = this.theme;
1189
+ const lines = [];
1190
+
1191
+ // Message
1192
+ lines.push(`${t.title}${sym.pointer} ${this.message}${c.reset}`);
1193
+
1194
+ // Input line
1195
+ let displayValue = this.value;
1196
+ if (this.mask && this.value) {
1197
+ displayValue = this.mask.repeat(this.value.length);
1198
+ }
1199
+
1200
+ if (!displayValue && this.placeholder) {
1201
+ displayValue = `${t.muted}${this.placeholder}${c.reset}`;
1202
+ } else if (!displayValue && this.default) {
1203
+ displayValue = `${t.muted}(${this.default})${c.reset}`;
1204
+ }
1205
+
1206
+ const cursor = `${t.primary}▌${c.reset}`;
1207
+ lines.push(` ${displayValue}${cursor}`);
1208
+
1209
+ // Error
1210
+ if (this.error) {
1211
+ lines.push(` ${t.error}${sym.error} ${this.error}${c.reset}`);
1212
+ }
1213
+
1214
+ this.write(lines.join("\n") + "\n");
1215
+ this.renderedLines = lines.length;
1216
+ }
1217
+
1218
+ handleKeypress(key, str) {
1219
+ if (!key) return { action: "none" };
1220
+
1221
+ const name = key.name || "";
1222
+ const ctrl = key.ctrl || false;
1223
+
1224
+ // Cancel
1225
+ if (name === "escape" || (ctrl && name === "c")) {
1226
+ this.cleanup();
1227
+ return { action: "cancel" };
1228
+ }
1229
+
1230
+ // Submit
1231
+ if (name === "return" || name === "enter") {
1232
+ const finalValue = this.value || this.default;
1233
+ const validation = this.validate(finalValue);
1234
+
1235
+ if (validation !== true) {
1236
+ this.error = typeof validation === "string" ? validation : "Invalid input";
1237
+ this.render();
1238
+ return { action: "error" };
1239
+ }
1240
+
1241
+ this.cleanup();
1242
+ return { action: "submit", value: this.transform(finalValue) };
1243
+ }
1244
+
1245
+ // Backspace
1246
+ if (name === "backspace") {
1247
+ this.value = this.value.slice(0, -1);
1248
+ this.error = null;
1249
+ this.render();
1250
+ return { action: "input" };
1251
+ }
1252
+
1253
+ // Clear line
1254
+ if (ctrl && name === "u") {
1255
+ this.value = "";
1256
+ this.error = null;
1257
+ this.render();
1258
+ return { action: "input" };
1259
+ }
1260
+
1261
+ // Printable character
1262
+ if (str && str.length === 1 && !ctrl) {
1263
+ this.value += str;
1264
+ this.error = null;
1265
+ this.render();
1266
+ return { action: "input" };
1267
+ }
1268
+
1269
+ return { action: "none" };
1270
+ }
1271
+
1272
+ async run() {
1273
+ if (!this.stdin.isTTY) {
1274
+ throw new Error("Interactive prompts require a TTY");
1275
+ }
1276
+
1277
+ this.setupReadline();
1278
+ this.hideCursor();
1279
+ this.render();
1280
+
1281
+ return new Promise((resolve, reject) => {
1282
+ const onKeypress = (str, key) => {
1283
+ try {
1284
+ const result = this.handleKeypress(key, str);
1285
+
1286
+ if (result.action === "submit") {
1287
+ resolve(result.value);
1288
+ } else if (result.action === "cancel") {
1289
+ resolve(null);
1290
+ }
1291
+ } catch (err) {
1292
+ this.cleanup();
1293
+ reject(err);
1294
+ }
1295
+ };
1296
+
1297
+ this.stdin.on("keypress", onKeypress);
1298
+
1299
+ this.rl.once("close", () => {
1300
+ this.stdin.off("keypress", onKeypress);
1301
+ resolve(null);
1302
+ });
1303
+ });
1304
+ }
1305
+ }
1306
+
1307
+ // ═══════════════════════════════════════════════════════════════════════════════
1308
+ // WIZARD (Multi-step form)
1309
+ // ═══════════════════════════════════════════════════════════════════════════════
1310
+ class Wizard extends BasePrompt {
1311
+ constructor(options = {}) {
1312
+ super(options);
1313
+
1314
+ this.title = options.title || "Setup Wizard";
1315
+ this.steps = options.steps || [];
1316
+ this.onCancel = options.onCancel || (() => {});
1317
+
1318
+ this.currentStep = 0;
1319
+ this.answers = {};
1320
+ }
1321
+
1322
+ renderProgress() {
1323
+ const t = this.theme;
1324
+ const total = this.steps.length;
1325
+ const current = this.currentStep + 1;
1326
+
1327
+ // Progress bar
1328
+ const barWidth = 30;
1329
+ const filled = Math.round((current / total) * barWidth);
1330
+ const empty = barWidth - filled;
1331
+ const bar = `${t.success}${"█".repeat(filled)}${c.reset}${t.muted}${"░".repeat(empty)}${c.reset}`;
1332
+
1333
+ // Step indicators
1334
+ const stepIndicators = this.steps.map((step, i) => {
1335
+ if (i < this.currentStep) {
1336
+ return `${t.success}${sym.success}${c.reset}`;
1337
+ } else if (i === this.currentStep) {
1338
+ return `${t.primary}${sym.active}${c.reset}`;
1339
+ } else {
1340
+ return `${t.muted}${sym.pending}${c.reset}`;
1341
+ }
1342
+ }).join(" ");
1343
+
1344
+ console.log();
1345
+ console.log(` ${t.title}${this.title}${c.reset}`);
1346
+ console.log(` ${bar} ${t.muted}${current}/${total}${c.reset}`);
1347
+ console.log(` ${stepIndicators}`);
1348
+ console.log();
1349
+ }
1350
+
1351
+ async runStep(step) {
1352
+ const stepConfig = {
1353
+ ...step,
1354
+ theme: this.theme,
1355
+ };
1356
+
1357
+ let prompt;
1358
+ switch (step.type) {
1359
+ case "select":
1360
+ prompt = new SelectPrompt(stepConfig);
1361
+ break;
1362
+ case "multiselect":
1363
+ prompt = new MultiSelectPrompt(stepConfig);
1364
+ break;
1365
+ case "confirm":
1366
+ prompt = new ConfirmPrompt(stepConfig);
1367
+ break;
1368
+ case "input":
1369
+ case "text":
1370
+ prompt = new InputPrompt(stepConfig);
1371
+ break;
1372
+ case "password":
1373
+ prompt = new InputPrompt({ ...stepConfig, mask: "*" });
1374
+ break;
1375
+ default:
1376
+ throw new Error(`Unknown step type: ${step.type}`);
1377
+ }
1378
+
1379
+ return await prompt.run();
1380
+ }
1381
+
1382
+ async run() {
1383
+ console.clear();
1384
+
1385
+ for (let i = 0; i < this.steps.length; i++) {
1386
+ this.currentStep = i;
1387
+ const step = this.steps[i];
1388
+
1389
+ // Clear and show progress
1390
+ console.clear();
1391
+ this.renderProgress();
1392
+
1393
+ // Run step
1394
+ const answer = await this.runStep(step);
1395
+
1396
+ if (answer === null) {
1397
+ // Cancelled
1398
+ this.onCancel();
1399
+ return null;
1400
+ }
1401
+
1402
+ this.answers[step.name || `step_${i}`] = answer;
1403
+
1404
+ // Run step callback if provided
1405
+ if (step.onAnswer) {
1406
+ await step.onAnswer(answer, this.answers);
1407
+ }
1408
+ }
1409
+
1410
+ return this.answers;
1411
+ }
1412
+ }
1413
+
1414
+ // ═══════════════════════════════════════════════════════════════════════════════
1415
+ // CONVENIENCE FUNCTIONS
1416
+ // ═══════════════════════════════════════════════════════════════════════════════
1417
+ async function select(options) {
1418
+ const prompt = new SelectPrompt(options);
1419
+ return await prompt.run();
1420
+ }
1421
+
1422
+ async function multiselect(options) {
1423
+ const prompt = new MultiSelectPrompt(options);
1424
+ return await prompt.run();
1425
+ }
1426
+
1427
+ async function confirm(options) {
1428
+ if (typeof options === "string") {
1429
+ options = { message: options };
1430
+ }
1431
+ const prompt = new ConfirmPrompt(options);
1432
+ return await prompt.run();
1433
+ }
1434
+
1435
+ async function input(options) {
1436
+ if (typeof options === "string") {
1437
+ options = { message: options };
1438
+ }
1439
+ const prompt = new InputPrompt(options);
1440
+ return await prompt.run();
1441
+ }
1442
+
1443
+ async function password(options) {
1444
+ if (typeof options === "string") {
1445
+ options = { message: options };
1446
+ }
1447
+ const prompt = new InputPrompt({ ...options, mask: "*" });
1448
+ return await prompt.run();
1449
+ }
1450
+
1451
+ async function wizard(options) {
1452
+ const w = new Wizard(options);
1453
+ return await w.run();
1454
+ }
1455
+
1456
+ // ═══════════════════════════════════════════════════════════════════════════════
1457
+ // EXPORTS
1458
+ // ═══════════════════════════════════════════════════════════════════════════════
1459
+ module.exports = {
1460
+ // Prompts
1461
+ SelectPrompt,
1462
+ MultiSelectPrompt,
1463
+ ConfirmPrompt,
1464
+ InputPrompt,
1465
+ Wizard,
1466
+
1467
+ // Convenience functions
1468
+ select,
1469
+ multiselect,
1470
+ confirm,
1471
+ input,
1472
+ password,
1473
+ wizard,
1474
+
1475
+ // Utilities
1476
+ gradient,
1477
+ drawBox,
1478
+ truncate,
1479
+ padEnd,
1480
+ padStart,
1481
+ centerText,
1482
+ wrapText,
1483
+ stripAnsi,
1484
+ getTerminalSize,
1485
+
1486
+ // Styling
1487
+ ansi,
1488
+ c,
1489
+ sym,
1490
+ themes,
1491
+
1492
+ // Constants
1493
+ SUPPORTS_COLOR,
1494
+ SUPPORTS_TRUECOLOR,
1495
+ SUPPORTS_UNICODE,
1496
+ };