@compilr-dev/cli 0.4.0 → 0.5.0

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 (315) hide show
  1. package/README.md +30 -12
  2. package/dist/agent.d.ts +74 -1
  3. package/dist/agent.js +259 -76
  4. package/dist/anchors/index.d.ts +9 -0
  5. package/dist/anchors/index.js +9 -0
  6. package/dist/anchors/project-anchors.d.ts +79 -0
  7. package/dist/anchors/project-anchors.js +202 -0
  8. package/dist/commands/handler-types.d.ts +68 -0
  9. package/dist/commands/handler-types.js +8 -0
  10. package/dist/commands/handlers/agent-commands.d.ts +13 -0
  11. package/dist/commands/handlers/agent-commands.js +305 -0
  12. package/dist/commands/handlers/design-commands.d.ts +15 -0
  13. package/dist/commands/handlers/design-commands.js +334 -0
  14. package/dist/commands/handlers/index.d.ts +20 -0
  15. package/dist/commands/handlers/index.js +43 -0
  16. package/dist/commands/handlers/overlay-commands.d.ts +21 -0
  17. package/dist/commands/handlers/overlay-commands.js +287 -0
  18. package/dist/commands/handlers/project-commands.d.ts +11 -0
  19. package/dist/commands/handlers/project-commands.js +167 -0
  20. package/dist/commands/handlers/simple-commands.d.ts +19 -0
  21. package/dist/commands/handlers/simple-commands.js +144 -0
  22. package/dist/commands/index.d.ts +2 -1
  23. package/dist/commands/registry.d.ts +50 -0
  24. package/dist/commands/registry.js +75 -0
  25. package/dist/commands-v2/handlers/context.d.ts +13 -0
  26. package/dist/commands-v2/handlers/context.js +348 -0
  27. package/dist/commands-v2/handlers/core.d.ts +13 -0
  28. package/dist/commands-v2/handlers/core.js +165 -0
  29. package/dist/commands-v2/handlers/debug.d.ts +11 -0
  30. package/dist/commands-v2/handlers/debug.js +159 -0
  31. package/dist/commands-v2/handlers/index.d.ts +12 -0
  32. package/dist/commands-v2/handlers/index.js +24 -0
  33. package/dist/commands-v2/handlers/project.d.ts +22 -0
  34. package/dist/commands-v2/handlers/project.js +814 -0
  35. package/dist/commands-v2/handlers/settings.d.ts +15 -0
  36. package/dist/commands-v2/handlers/settings.js +235 -0
  37. package/dist/commands-v2/index.d.ts +13 -0
  38. package/dist/commands-v2/index.js +15 -0
  39. package/dist/commands-v2/registry.d.ts +37 -0
  40. package/dist/commands-v2/registry.js +80 -0
  41. package/dist/commands-v2/types.d.ts +75 -0
  42. package/dist/commands-v2/types.js +7 -0
  43. package/dist/commands.js +110 -7
  44. package/dist/index.js +288 -29
  45. package/dist/input-handlers/index.d.ts +7 -0
  46. package/dist/input-handlers/index.js +7 -0
  47. package/dist/input-handlers/memory-handler.d.ts +26 -0
  48. package/dist/input-handlers/memory-handler.js +68 -0
  49. package/dist/repl-helpers.d.ts +63 -0
  50. package/dist/repl-helpers.js +318 -0
  51. package/dist/repl-v2.d.ts +155 -0
  52. package/dist/repl-v2.js +774 -0
  53. package/dist/repl.d.ts +32 -4
  54. package/dist/repl.js +250 -977
  55. package/dist/settings/index.d.ts +23 -0
  56. package/dist/settings/index.js +48 -0
  57. package/dist/settings/paths.d.ts +110 -0
  58. package/dist/settings/paths.js +264 -0
  59. package/dist/templates/compilr-md.js +7 -4
  60. package/dist/templates/index.js +3 -4
  61. package/dist/themes/colors.js +3 -1
  62. package/dist/themes/registry.d.ts +5 -36
  63. package/dist/themes/registry.js +11 -95
  64. package/dist/themes/types.d.ts +3 -38
  65. package/dist/themes/types.js +2 -2
  66. package/dist/tools/anchor-tools.d.ts +31 -0
  67. package/dist/tools/anchor-tools.js +255 -0
  68. package/dist/tools/backlog-wrappers.d.ts +54 -0
  69. package/dist/tools/backlog-wrappers.js +338 -0
  70. package/dist/tools/backlog.js +1 -1
  71. package/dist/tools/db-tools.d.ts +65 -0
  72. package/dist/tools/db-tools.js +19 -0
  73. package/dist/tools/document-db.d.ts +43 -0
  74. package/dist/tools/document-db.js +220 -0
  75. package/dist/tools/project-db.d.ts +102 -0
  76. package/dist/tools/project-db.js +370 -0
  77. package/dist/tools/workitem-db.d.ts +103 -0
  78. package/dist/tools/workitem-db.js +549 -0
  79. package/dist/tools.js +13 -3
  80. package/dist/ui/agents-overlay-v2.d.ts +43 -0
  81. package/dist/ui/agents-overlay-v2.js +809 -0
  82. package/dist/ui/agents-overlay.d.ts +5 -5
  83. package/dist/ui/agents-overlay.js +782 -420
  84. package/dist/ui/anchors-overlay.d.ts +12 -0
  85. package/dist/ui/anchors-overlay.js +775 -0
  86. package/dist/ui/arch-type-overlay.d.ts +1 -6
  87. package/dist/ui/arch-type-overlay.js +175 -203
  88. package/dist/ui/ask-user-overlay-v2.d.ts +26 -0
  89. package/dist/ui/ask-user-overlay-v2.js +555 -0
  90. package/dist/ui/ask-user-overlay.d.ts +2 -2
  91. package/dist/ui/ask-user-overlay.js +443 -535
  92. package/dist/ui/ask-user-simple-overlay-v2.d.ts +25 -0
  93. package/dist/ui/ask-user-simple-overlay-v2.js +215 -0
  94. package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
  95. package/dist/ui/ask-user-simple-overlay.js +182 -209
  96. package/dist/ui/backlog-overlay.d.ts +16 -1
  97. package/dist/ui/backlog-overlay.js +525 -659
  98. package/dist/ui/base/index.d.ts +26 -0
  99. package/dist/ui/base/index.js +33 -0
  100. package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
  101. package/dist/ui/base/inline-overlay-utils.js +320 -0
  102. package/dist/ui/base/inline-overlay.d.ts +159 -0
  103. package/dist/ui/base/inline-overlay.js +257 -0
  104. package/dist/ui/base/key-utils.d.ts +15 -0
  105. package/dist/ui/base/key-utils.js +30 -0
  106. package/dist/ui/base/overlay-base-v2.d.ts +193 -0
  107. package/dist/ui/base/overlay-base-v2.js +246 -0
  108. package/dist/ui/base/overlay-base.d.ts +156 -0
  109. package/dist/ui/base/overlay-base.js +238 -0
  110. package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
  111. package/dist/ui/base/overlay-lifecycle.js +159 -0
  112. package/dist/ui/base/overlay-types.d.ts +185 -0
  113. package/dist/ui/base/overlay-types.js +7 -0
  114. package/dist/ui/base/render-utils.d.ts +8 -0
  115. package/dist/ui/base/render-utils.js +11 -0
  116. package/dist/ui/base/screen-stack.d.ts +148 -0
  117. package/dist/ui/base/screen-stack.js +184 -0
  118. package/dist/ui/base/tabbed-list-overlay-v2.d.ts +103 -0
  119. package/dist/ui/base/tabbed-list-overlay-v2.js +317 -0
  120. package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
  121. package/dist/ui/base/tabbed-list-overlay.js +369 -0
  122. package/dist/ui/commands-overlay-v2.d.ts +33 -0
  123. package/dist/ui/commands-overlay-v2.js +441 -0
  124. package/dist/ui/commands-overlay.d.ts +7 -2
  125. package/dist/ui/commands-overlay.js +384 -355
  126. package/dist/ui/config-overlay.d.ts +5 -4
  127. package/dist/ui/config-overlay.js +243 -513
  128. package/dist/ui/conversation.d.ts +75 -4
  129. package/dist/ui/conversation.js +374 -161
  130. package/dist/ui/docs-overlay.d.ts +17 -0
  131. package/dist/ui/docs-overlay.js +303 -0
  132. package/dist/ui/ephemeral.d.ts +1 -1
  133. package/dist/ui/ephemeral.js +1 -1
  134. package/dist/ui/features/index.d.ts +34 -0
  135. package/dist/ui/features/index.js +34 -0
  136. package/dist/ui/features/input-feature.d.ts +85 -0
  137. package/dist/ui/features/input-feature.js +238 -0
  138. package/dist/ui/features/list-feature.d.ts +155 -0
  139. package/dist/ui/features/list-feature.js +244 -0
  140. package/dist/ui/features/pagination-feature.d.ts +154 -0
  141. package/dist/ui/features/pagination-feature.js +238 -0
  142. package/dist/ui/features/search-feature.d.ts +148 -0
  143. package/dist/ui/features/search-feature.js +185 -0
  144. package/dist/ui/features/tab-feature.d.ts +194 -0
  145. package/dist/ui/features/tab-feature.js +307 -0
  146. package/dist/ui/footer-v2.d.ts +222 -0
  147. package/dist/ui/footer-v2.js +1349 -0
  148. package/dist/ui/footer.d.ts +107 -0
  149. package/dist/ui/footer.js +359 -67
  150. package/dist/ui/guardrail-overlay.d.ts +29 -0
  151. package/dist/ui/guardrail-overlay.js +145 -0
  152. package/dist/ui/help-overlay-v2.d.ts +34 -0
  153. package/dist/ui/help-overlay-v2.js +309 -0
  154. package/dist/ui/help-overlay.d.ts +16 -0
  155. package/dist/ui/help-overlay.js +316 -0
  156. package/dist/ui/index.d.ts +1 -1
  157. package/dist/ui/index.js +1 -3
  158. package/dist/ui/init-overlay-v2.d.ts +34 -0
  159. package/dist/ui/init-overlay-v2.js +600 -0
  160. package/dist/ui/init-overlay.d.ts +12 -2
  161. package/dist/ui/init-overlay.js +349 -270
  162. package/dist/ui/input-prompt-v2.d.ts +1 -0
  163. package/dist/ui/input-prompt-v2.js +14 -6
  164. package/dist/ui/input-prompt.d.ts +116 -33
  165. package/dist/ui/input-prompt.js +536 -337
  166. package/dist/ui/iteration-limit-overlay-v2.d.ts +21 -0
  167. package/dist/ui/iteration-limit-overlay-v2.js +114 -0
  168. package/dist/ui/iteration-limit-overlay.d.ts +2 -2
  169. package/dist/ui/iteration-limit-overlay.js +92 -128
  170. package/dist/ui/keys-overlay-v2.d.ts +41 -0
  171. package/dist/ui/keys-overlay-v2.js +248 -0
  172. package/dist/ui/keys-overlay.d.ts +1 -0
  173. package/dist/ui/keys-overlay.js +203 -141
  174. package/dist/ui/line-utils.d.ts +88 -0
  175. package/dist/ui/line-utils.js +150 -0
  176. package/dist/ui/live-region.d.ts +161 -0
  177. package/dist/ui/live-region.js +387 -0
  178. package/dist/ui/mascot/expressions.d.ts +32 -0
  179. package/dist/ui/mascot/expressions.js +213 -0
  180. package/dist/ui/mascot/index.d.ts +8 -0
  181. package/dist/ui/mascot/index.js +8 -0
  182. package/dist/ui/mascot/renderer.d.ts +19 -0
  183. package/dist/ui/mascot/renderer.js +97 -0
  184. package/dist/ui/mascot-overlay-v2.d.ts +41 -0
  185. package/dist/ui/mascot-overlay-v2.js +138 -0
  186. package/dist/ui/mascot-overlay.d.ts +21 -0
  187. package/dist/ui/mascot-overlay.js +146 -0
  188. package/dist/ui/model-overlay-v2.d.ts +49 -0
  189. package/dist/ui/model-overlay-v2.js +118 -0
  190. package/dist/ui/model-overlay.d.ts +27 -0
  191. package/dist/ui/model-overlay.js +221 -0
  192. package/dist/ui/model-warning-overlay.js +3 -5
  193. package/dist/ui/new-overlay.d.ts +34 -0
  194. package/dist/ui/new-overlay.js +604 -0
  195. package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
  196. package/dist/ui/overlay/impl/agents-overlay-v2.js +825 -0
  197. package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
  198. package/dist/ui/overlay/impl/anchors-overlay-v2.js +783 -0
  199. package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
  200. package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
  201. package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
  202. package/dist/ui/overlay/impl/ask-user-overlay-v2.js +584 -0
  203. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
  204. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
  205. package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +49 -0
  206. package/dist/ui/overlay/impl/backlog-overlay-v2.js +642 -0
  207. package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
  208. package/dist/ui/overlay/impl/commands-overlay-v2.js +441 -0
  209. package/dist/ui/overlay/impl/config-overlay-v2.d.ts +100 -0
  210. package/dist/ui/overlay/impl/config-overlay-v2.js +654 -0
  211. package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +55 -0
  212. package/dist/ui/overlay/impl/dashboard-overlay-v2.js +359 -0
  213. package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
  214. package/dist/ui/overlay/impl/docs-overlay-v2.js +114 -0
  215. package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +77 -0
  216. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1071 -0
  217. package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
  218. package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
  219. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +34 -0
  220. package/dist/ui/overlay/impl/help-overlay-v2.js +309 -0
  221. package/dist/ui/overlay/impl/init-overlay-v2.d.ts +77 -0
  222. package/dist/ui/overlay/impl/init-overlay-v2.js +593 -0
  223. package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
  224. package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
  225. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
  226. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
  227. package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
  228. package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
  229. package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
  230. package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
  231. package/dist/ui/overlay/impl/model-overlay-v2.d.ts +49 -0
  232. package/dist/ui/overlay/impl/model-overlay-v2.js +118 -0
  233. package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
  234. package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
  235. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +77 -0
  236. package/dist/ui/overlay/impl/new-overlay-v2.js +593 -0
  237. package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
  238. package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
  239. package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +36 -0
  240. package/dist/ui/overlay/impl/projects-overlay-v2.js +499 -0
  241. package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
  242. package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
  243. package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
  244. package/dist/ui/overlay/impl/tools-overlay-v2.js +218 -0
  245. package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +31 -0
  246. package/dist/ui/overlay/impl/tutorial-overlay-v2.js +1035 -0
  247. package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
  248. package/dist/ui/overlay/impl/workflow-overlay-v2.js +637 -0
  249. package/dist/ui/overlay/index.d.ts +33 -0
  250. package/dist/ui/overlay/index.js +35 -0
  251. package/dist/ui/overlay/key-utils.d.ts +6 -0
  252. package/dist/ui/overlay/key-utils.js +6 -0
  253. package/dist/ui/overlay/overlay-types.d.ts +128 -0
  254. package/dist/ui/overlay/overlay-types.js +22 -0
  255. package/dist/ui/overlay/types.d.ts +135 -0
  256. package/dist/ui/overlay/types.js +22 -0
  257. package/dist/ui/overlays/help-overlay-v2.d.ts +28 -0
  258. package/dist/ui/overlays/help-overlay-v2.js +198 -0
  259. package/dist/ui/overlays/index.d.ts +11 -0
  260. package/dist/ui/overlays/index.js +11 -0
  261. package/dist/ui/overlays.d.ts +0 -4
  262. package/dist/ui/overlays.js +0 -444
  263. package/dist/ui/permission-overlay-v2.d.ts +36 -0
  264. package/dist/ui/permission-overlay-v2.js +380 -0
  265. package/dist/ui/permission-overlay.d.ts +1 -1
  266. package/dist/ui/permission-overlay.js +186 -298
  267. package/dist/ui/projects-overlay.d.ts +19 -0
  268. package/dist/ui/projects-overlay.js +484 -0
  269. package/dist/ui/providers/types.d.ts +178 -0
  270. package/dist/ui/providers/types.js +9 -0
  271. package/dist/ui/render-modes.d.ts +36 -0
  272. package/dist/ui/render-modes.js +44 -0
  273. package/dist/ui/startup-menu.d.ts +36 -0
  274. package/dist/ui/startup-menu.js +236 -0
  275. package/dist/ui/subagent-renderer.d.ts +117 -0
  276. package/dist/ui/subagent-renderer.js +334 -0
  277. package/dist/ui/terminal-codes.d.ts +94 -0
  278. package/dist/ui/terminal-codes.js +124 -0
  279. package/dist/ui/terminal-renderer.d.ts +221 -0
  280. package/dist/ui/terminal-renderer.js +751 -0
  281. package/dist/ui/terminal-ui.d.ts +463 -0
  282. package/dist/ui/terminal-ui.js +2296 -0
  283. package/dist/ui/terminal.d.ts +20 -0
  284. package/dist/ui/terminal.js +72 -0
  285. package/dist/ui/theme-overlay-v2.d.ts +42 -0
  286. package/dist/ui/theme-overlay-v2.js +135 -0
  287. package/dist/ui/theme-overlay.d.ts +24 -0
  288. package/dist/ui/theme-overlay.js +127 -0
  289. package/dist/ui/todo-zone.js +53 -25
  290. package/dist/ui/tool-formatters.d.ts +16 -0
  291. package/dist/ui/tool-formatters.js +516 -0
  292. package/dist/ui/tools-overlay-v2.d.ts +47 -0
  293. package/dist/ui/tools-overlay-v2.js +218 -0
  294. package/dist/ui/tools-overlay.d.ts +10 -2
  295. package/dist/ui/tools-overlay.js +172 -220
  296. package/dist/ui/tutorial-overlay-v2.d.ts +31 -0
  297. package/dist/ui/tutorial-overlay-v2.js +1035 -0
  298. package/dist/ui/tutorial-overlay.d.ts +1 -0
  299. package/dist/ui/tutorial-overlay.js +400 -302
  300. package/dist/ui/workflow-overlay.d.ts +22 -0
  301. package/dist/ui/workflow-overlay.js +636 -0
  302. package/dist/utils/debug-log.d.ts +28 -0
  303. package/dist/utils/debug-log.js +57 -0
  304. package/dist/utils/model-tiers.js +1 -1
  305. package/dist/utils/path-safety.d.ts +56 -0
  306. package/dist/utils/path-safety.js +239 -0
  307. package/dist/workflow/guided-mode-injector.d.ts +42 -0
  308. package/dist/workflow/guided-mode-injector.js +191 -0
  309. package/dist/workflow/index.d.ts +8 -0
  310. package/dist/workflow/index.js +8 -0
  311. package/dist/workflow/step-criteria.d.ts +62 -0
  312. package/dist/workflow/step-criteria.js +150 -0
  313. package/dist/workflow/step-tracker.d.ts +92 -0
  314. package/dist/workflow/step-tracker.js +141 -0
  315. package/package.json +12 -5
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Ask User Overlay
2
+ * Ask User Overlay (Refactored)
3
3
  *
4
4
  * Modal overlay for presenting multi-question forms to the user.
5
- * Used by the ask_user tool during /design and /refine workflows.
5
+ * Uses InlineOverlay base class for consistent lifecycle management.
6
6
  *
7
7
  * Features:
8
8
  * - Multiple questions with navigation (←/→ or Tab)
@@ -12,636 +12,544 @@
12
12
  * - Progress indicator showing answered questions
13
13
  */
14
14
  import chalk from 'chalk';
15
- import * as terminal from './terminal.js';
16
15
  import { getStyles } from '../themes/index.js';
16
+ import { InlineOverlay } from './base/inline-overlay.js';
17
17
  // =============================================================================
18
- // Rendering Helpers
18
+ // Overlay Implementation
19
19
  // =============================================================================
20
- function renderTabBar(questions, currentIndex, answers, isOnSubmitTab) {
21
- const s = getStyles();
22
- const lines = [];
23
- // Tab bar like config-overlay: " Questions: App Type Users Features Submit "
24
- // Current tab highlighted with bgBlue, answered tabs with success color
25
- let tabLine = ' ';
26
- for (let i = 0; i < questions.length; i++) {
27
- const q = questions[i];
28
- const hasAnswer = q.id in answers;
29
- const isCurrent = i === currentIndex && !isOnSubmitTab;
30
- // Use header if available, fallback to formatted id
31
- const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
32
- if (isCurrent) {
33
- // Current tab: highlighted background (theme selection color)
34
- tabLine += s.selected(` ${label} `) + ' ';
35
- }
36
- else if (hasAnswer) {
37
- // Answered tab: success color with checkmark
38
- tabLine += s.success(`✓ ${label}`) + ' ';
20
+ class AskUserOverlayImpl extends InlineOverlay {
21
+ questions;
22
+ context;
23
+ constructor(options) {
24
+ super();
25
+ this.questions = options.questions;
26
+ this.context = options.context;
27
+ }
28
+ getInitialState() {
29
+ // Check if first question should auto-start in typing mode
30
+ const firstQ = this.questions[0];
31
+ const firstQAutoType = (firstQ.options?.length ?? 0) === 0 &&
32
+ firstQ.allowCustom !== false &&
33
+ firstQ.multiSelect !== true;
34
+ return {
35
+ currentQuestion: 0,
36
+ selectedIndex: 0,
37
+ inputBuffer: '',
38
+ isTypingCustom: firstQAutoType,
39
+ answers: {},
40
+ multiSelections: new Set(),
41
+ warningMessage: '',
42
+ isOnSubmitTab: false,
43
+ };
44
+ }
45
+ render() {
46
+ const s = getStyles();
47
+ const lines = [];
48
+ const cols = this.getTerminalWidth();
49
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
50
+ // Header
51
+ lines.push(border);
52
+ lines.push(' ' + s.primaryBold('Questions'));
53
+ lines.push('');
54
+ // Tab bar
55
+ lines.push(...this.renderTabBar());
56
+ lines.push('');
57
+ // Current question or Submit tab
58
+ if (this.state.isOnSubmitTab) {
59
+ lines.push(...this.renderSubmitTab());
39
60
  }
40
61
  else {
41
- // Pending tab: muted
42
- tabLine += s.muted(` ${label} `) + ' ';
62
+ lines.push(...this.renderQuestion());
63
+ }
64
+ // Footer with instructions
65
+ lines.push(...this.renderFooter());
66
+ // Warning message if present
67
+ if (this.state.warningMessage) {
68
+ lines.push('');
69
+ lines.push(s.warning(` ⚠ ${this.state.warningMessage}`));
43
70
  }
71
+ // Bottom border
72
+ lines.push(border);
73
+ return lines;
44
74
  }
45
- // Add Submit tab at the end
46
- const allAnswered = questions.every((q) => q.id in answers);
47
- if (isOnSubmitTab) {
48
- tabLine += s.selected(' Submit ');
75
+ handleKey(data) {
76
+ // Ctrl+C always cancels
77
+ if (this.isCtrlCKey(data)) {
78
+ return { type: 'close', result: this.getResult() };
79
+ }
80
+ // Handle Submit tab first
81
+ if (this.state.isOnSubmitTab) {
82
+ return this.handleSubmitTabKey(data);
83
+ }
84
+ // Handle typing mode
85
+ if (this.state.isTypingCustom) {
86
+ return this.handleTypingKey(data);
87
+ }
88
+ // Handle navigation mode
89
+ return this.handleNavigationKey(data);
49
90
  }
50
- else if (allAnswered) {
51
- tabLine += s.success(' Submit ');
91
+ getCleanupSummary(result) {
92
+ const s = getStyles();
93
+ const answeredCount = Object.keys(result.answers).length;
94
+ const skippedCount = result.skipped.length;
95
+ if (answeredCount === 0) {
96
+ return s.muted('Questions: ') + s.warning('Cancelled');
97
+ }
98
+ if (skippedCount > 0) {
99
+ return s.muted('Questions: ') + s.success(`${String(answeredCount)} answered`) + s.muted(`, ${String(skippedCount)} skipped`);
100
+ }
101
+ return s.muted('Questions: ') + s.success(`${String(answeredCount)} answered`);
52
102
  }
53
- else {
54
- tabLine += s.muted(' Submit ');
103
+ // ===========================================================================
104
+ // Private: Rendering Helpers
105
+ // ===========================================================================
106
+ renderTabBar() {
107
+ const s = getStyles();
108
+ const lines = [];
109
+ let tabLine = ' ';
110
+ for (let i = 0; i < this.questions.length; i++) {
111
+ const q = this.questions[i];
112
+ const hasAnswer = q.id in this.state.answers;
113
+ const isCurrent = i === this.state.currentQuestion && !this.state.isOnSubmitTab;
114
+ const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
115
+ if (isCurrent) {
116
+ tabLine += s.selected(` ${label} `) + ' ';
117
+ }
118
+ else if (hasAnswer) {
119
+ tabLine += s.success(`✓ ${label}`) + ' ';
120
+ }
121
+ else {
122
+ tabLine += s.muted(` ${label} `) + ' ';
123
+ }
124
+ }
125
+ // Add Submit tab
126
+ const allAnswered = this.questions.every((q) => q.id in this.state.answers);
127
+ if (this.state.isOnSubmitTab) {
128
+ tabLine += s.selected(' Submit ');
129
+ }
130
+ else if (allAnswered) {
131
+ tabLine += s.success(' Submit ');
132
+ }
133
+ else {
134
+ tabLine += s.muted(' Submit ');
135
+ }
136
+ lines.push(tabLine);
137
+ return lines;
55
138
  }
56
- lines.push(tabLine);
57
- return lines;
58
- }
59
- function renderQuestion(question, questionIndex, totalQuestions, state, context) {
60
- const s = getStyles();
61
- const lines = [];
62
- // Context if provided (only on first render)
63
- if (context && questionIndex === 0) {
64
- lines.push(s.muted(' ' + context));
139
+ renderQuestion() {
140
+ const s = getStyles();
141
+ const lines = [];
142
+ const question = this.questions[this.state.currentQuestion];
143
+ const questionIndex = this.state.currentQuestion;
144
+ const totalQuestions = this.questions.length;
145
+ // Context if provided (only on first question)
146
+ if (this.context && questionIndex === 0) {
147
+ lines.push(s.muted(' ' + this.context));
148
+ lines.push('');
149
+ }
150
+ // Question number and text
151
+ lines.push(chalk.bold(` [${String(questionIndex + 1)}/${String(totalQuestions)}] ${question.question}`));
65
152
  lines.push('');
66
- }
67
- // Question number and text
68
- lines.push(chalk.bold(` [${String(questionIndex + 1)}/${String(totalQuestions)}] ${question.question}`));
69
- lines.push('');
70
- // Options
71
- const options = question.options ?? [];
72
- const allowCustom = question.allowCustom !== false; // Default true
73
- const isMultiSelect = question.multiSelect === true;
74
- const customIndex = options.length; // Custom is always last option
75
- // Check if there's an existing answer for this question
76
- const answeredOptions = new Set();
77
- let customAnswerValue = '';
78
- if (question.id in state.answers) {
79
- const existingAnswer = state.answers[question.id];
80
- if (isMultiSelect && Array.isArray(existingAnswer)) {
81
- for (const ans of existingAnswer) {
82
- const idx = options.indexOf(ans);
83
- if (idx >= 0) {
84
- answeredOptions.add(idx);
85
- }
86
- else {
87
- customAnswerValue = ans;
88
- }
153
+ // Options
154
+ const options = question.options ?? [];
155
+ const allowCustom = question.allowCustom !== false;
156
+ const isMultiSelect = question.multiSelect === true;
157
+ const customIndex = options.length;
158
+ // Get existing answer info
159
+ const { answeredOptions, customAnswerValue } = this.getAnswerInfo(question);
160
+ for (let i = 0; i < options.length; i++) {
161
+ const isCursor = this.state.selectedIndex === i;
162
+ const isMultiSelected = isMultiSelect && this.state.multiSelections.has(i);
163
+ const isAnswered = answeredOptions.has(i);
164
+ const prefix = isCursor ? '' : ' ';
165
+ let marker = '';
166
+ if (isMultiSelect) {
167
+ marker = isMultiSelected ? '[✓] ' : '[ ] ';
168
+ }
169
+ else if (isAnswered) {
170
+ marker = '● ';
171
+ }
172
+ const label = `${String(i + 1)}. ${options[i]}`;
173
+ if (isCursor) {
174
+ lines.push(s.primary(prefix + marker + label));
175
+ }
176
+ else if (isMultiSelected || isAnswered) {
177
+ lines.push(s.success(prefix + marker + label));
178
+ }
179
+ else {
180
+ lines.push(s.muted(prefix + marker + label));
89
181
  }
90
182
  }
91
- else if (typeof existingAnswer === 'string') {
92
- const idx = options.indexOf(existingAnswer);
93
- if (idx >= 0) {
94
- answeredOptions.add(idx);
183
+ // Custom option
184
+ if (allowCustom) {
185
+ const isCursor = this.state.selectedIndex === customIndex;
186
+ const prefix = isCursor ? ' ❯ ' : ' ';
187
+ const hasCustomAnswer = customAnswerValue !== '' || this.state.inputBuffer !== '';
188
+ const displayValue = this.state.inputBuffer || customAnswerValue;
189
+ if (this.state.isTypingCustom) {
190
+ lines.push(s.primary(prefix + 'Custom: ' + this.state.inputBuffer + '▋'));
191
+ }
192
+ else if (hasCustomAnswer) {
193
+ const marker = isMultiSelect ? '[✓] ' : '● ';
194
+ const label = `${String(options.length + 1)}. Custom: "${displayValue}"`;
195
+ lines.push(isCursor ? s.primary(prefix + marker + label) : s.success(prefix + marker + label));
95
196
  }
96
197
  else {
97
- customAnswerValue = existingAnswer;
198
+ const label = `${String(options.length + 1)}. Type something custom...`;
199
+ lines.push(isCursor ? s.primary(prefix + label) : s.muted(prefix + label));
98
200
  }
99
201
  }
202
+ return lines;
100
203
  }
101
- for (let i = 0; i < options.length; i++) {
102
- const isCursor = state.selectedIndex === i;
103
- const isMultiSelected = isMultiSelect && state.multiSelections.has(i);
104
- const isAnswered = answeredOptions.has(i);
105
- const prefix = isCursor ? ' ❯ ' : ' ';
106
- // For multi-select: show checkbox state
107
- // For single-select: show bullet for answered option
108
- let marker = '';
109
- if (isMultiSelect) {
110
- marker = isMultiSelected ? '[✓] ' : '[ ] ';
204
+ renderSubmitTab() {
205
+ const s = getStyles();
206
+ const lines = [];
207
+ lines.push('');
208
+ lines.push(chalk.bold(' Ready to submit?'));
209
+ lines.push('');
210
+ const answeredCount = Object.keys(this.state.answers).length;
211
+ const totalCount = this.questions.length;
212
+ const allAnswered = answeredCount === totalCount;
213
+ if (allAnswered) {
214
+ lines.push(s.success(` ✓ All ${String(totalCount)} questions answered`));
215
+ lines.push('');
216
+ lines.push(' ' + s.muted('Press Enter to submit your answers.'));
111
217
  }
112
- else if (isAnswered) {
113
- marker = '● '; // Filled bullet for answered single-select
218
+ else {
219
+ const unansweredCount = totalCount - answeredCount;
220
+ lines.push(s.warning(` ⚠ ${String(unansweredCount)} of ${String(totalCount)} questions not answered`));
221
+ lines.push('');
222
+ lines.push(' ' + s.muted('Unanswered:'));
223
+ for (const q of this.questions) {
224
+ if (!(q.id in this.state.answers)) {
225
+ const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
226
+ lines.push(' ' + s.muted(`• ${label}`));
227
+ }
228
+ }
229
+ lines.push('');
230
+ lines.push(' ' + s.muted('Press ← or Tab to go back and answer.'));
114
231
  }
115
- const label = `${String(i + 1)}. ${options[i]}`;
116
- if (isCursor) {
117
- lines.push(s.primary(prefix + marker + label));
232
+ return lines;
233
+ }
234
+ renderFooter() {
235
+ const s = getStyles();
236
+ const lines = [];
237
+ lines.push('');
238
+ if (this.state.isOnSubmitTab) {
239
+ lines.push(s.muted(' Enter Submit all · ←/Tab Go back · Esc Cancel'));
118
240
  }
119
- else if (isMultiSelected || isAnswered) {
120
- lines.push(s.success(prefix + marker + label));
241
+ else if (this.state.isTypingCustom) {
242
+ lines.push(s.muted(' Enter Submit · Esc Cancel custom'));
121
243
  }
122
244
  else {
123
- lines.push(s.muted(prefix + marker + label));
245
+ const currentQ = this.questions[this.state.currentQuestion];
246
+ if (currentQ.multiSelect === true) {
247
+ lines.push(s.muted(' ↑↓ Navigate · Space Toggle · Enter Confirm · Tab Next · Esc Cancel'));
248
+ }
249
+ else {
250
+ lines.push(s.muted(' ↑↓ Navigate · Enter Select · Tab Next · Esc Cancel'));
251
+ }
124
252
  }
253
+ return lines;
125
254
  }
126
- // Custom option
127
- if (allowCustom) {
128
- const isCursor = state.selectedIndex === customIndex;
129
- const prefix = isCursor ? ' ❯ ' : ' ';
130
- const hasCustomAnswer = customAnswerValue !== '' || state.inputBuffer !== '';
131
- const displayValue = state.inputBuffer || customAnswerValue;
132
- if (state.isTypingCustom) {
133
- lines.push(s.primary(prefix + 'Custom: ' + state.inputBuffer + '█'));
134
- }
135
- else if (hasCustomAnswer) {
136
- // Show the custom answer value
137
- const marker = isMultiSelect ? '[✓] ' : '● ';
138
- const label = `${String(options.length + 1)}. Custom: "${displayValue}"`;
139
- lines.push(isCursor ? s.primary(prefix + marker + label) : s.success(prefix + marker + label));
255
+ // ===========================================================================
256
+ // Private: Key Handlers
257
+ // ===========================================================================
258
+ handleSubmitTabKey(data) {
259
+ if (this.isEscapeKey(data)) {
260
+ return { type: 'close', result: this.getResult() };
140
261
  }
141
- else {
142
- const label = `${String(options.length + 1)}. Type something custom...`;
143
- lines.push(isCursor ? s.primary(prefix + label) : s.muted(prefix + label));
262
+ if (this.isLeftArrowKey(data) || this.isTabKey(data)) {
263
+ this.prevQuestion();
264
+ return { type: 'continue' };
144
265
  }
145
- }
146
- return lines;
147
- }
148
- function renderFooter(isTypingCustom, isMultiSelect, isOnSubmitTab) {
149
- const s = getStyles();
150
- const lines = [];
151
- lines.push('');
152
- if (isOnSubmitTab) {
153
- lines.push(s.muted(' Enter Submit all · ←/Tab Go back · Esc Cancel'));
154
- }
155
- else if (isTypingCustom) {
156
- lines.push(s.muted(' Enter Submit · Esc Cancel custom'));
157
- }
158
- else if (isMultiSelect) {
159
- lines.push(s.muted(' ↑↓ Navigate · Space Toggle · Enter Confirm · Tab Next · Esc Cancel'));
160
- }
161
- else {
162
- lines.push(s.muted(' ↑↓ Navigate · Enter Select · Tab Next · Esc Cancel'));
163
- }
164
- return lines;
165
- }
166
- function renderSubmitTab(questions, answers) {
167
- const s = getStyles();
168
- const lines = [];
169
- lines.push('');
170
- lines.push(chalk.bold(' Ready to submit?'));
171
- lines.push('');
172
- // Show summary of answers
173
- const answeredCount = Object.keys(answers).length;
174
- const totalCount = questions.length;
175
- const allAnswered = answeredCount === totalCount;
176
- if (allAnswered) {
177
- lines.push(s.success(` ✓ All ${String(totalCount)} questions answered`));
178
- lines.push('');
179
- lines.push(' ' + s.muted('Press Enter to submit your answers.'));
180
- }
181
- else {
182
- const unansweredCount = totalCount - answeredCount;
183
- lines.push(s.warning(` ⚠ ${String(unansweredCount)} of ${String(totalCount)} questions not answered`));
184
- lines.push('');
185
- // List unanswered questions
186
- lines.push(' ' + s.muted('Unanswered:'));
187
- for (const q of questions) {
188
- if (!(q.id in answers)) {
189
- const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
190
- lines.push(' ' + s.muted(`• ${label}`));
266
+ if (this.isEnterKey(data)) {
267
+ if (this.tryComplete()) {
268
+ return { type: 'close', result: this.getResult() };
191
269
  }
270
+ // tryComplete sets warning and navigates to first unanswered
192
271
  }
193
- lines.push('');
194
- lines.push(' ' + s.muted('Press ← or Tab to go back and answer.'));
272
+ return { type: 'continue' };
195
273
  }
196
- return lines;
197
- }
198
- // Debug flag - set to true to enable logging to stderr
199
- const ASK_USER_DEBUG = false;
200
- function debugLog(msg) {
201
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
202
- if (ASK_USER_DEBUG) {
203
- process.stderr.write(`[ask-user] ${msg}\n`);
204
- }
205
- }
206
- function render(questions, state, context, previousLineCount = 0, targetLineCount = 0) {
207
- const s = getStyles();
208
- const lines = [];
209
- const cols = terminal.getTerminalWidth();
210
- const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
211
- debugLog(`render: q=${String(state.currentQuestion)}, prevLines=${String(previousLineCount)}, target=${String(targetLineCount)}`);
212
- // Clear previous render
213
- if (previousLineCount > 0) {
214
- terminal.clearLinesAbove(previousLineCount);
215
- }
216
- // Header
217
- lines.push(border);
218
- lines.push(' ' + s.primaryBold('Questions'));
219
- lines.push('');
220
- // Tab bar (like config-overlay) - includes Submit tab
221
- lines.push(...renderTabBar(questions, state.currentQuestion, state.answers, state.isOnSubmitTab));
222
- lines.push('');
223
- // Current question or Submit tab
224
- if (state.isOnSubmitTab) {
225
- lines.push(...renderSubmitTab(questions, state.answers));
226
- }
227
- else {
228
- const currentQ = questions[state.currentQuestion];
229
- lines.push(...renderQuestion(currentQ, state.currentQuestion, questions.length, state, context));
274
+ handleTypingKey(data) {
275
+ const currentQ = this.questions[this.state.currentQuestion];
276
+ const options = currentQ.options ?? [];
277
+ if (this.isEscapeKey(data)) {
278
+ this.state.isTypingCustom = false;
279
+ this.state.inputBuffer = '';
280
+ return { type: 'continue' };
281
+ }
282
+ if (this.isUpArrowKey(data) && options.length > 0) {
283
+ this.state.isTypingCustom = false;
284
+ this.state.inputBuffer = '';
285
+ this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
286
+ return { type: 'continue' };
287
+ }
288
+ if (this.isDownArrowKey(data)) {
289
+ // Custom is last option, down arrow does nothing
290
+ return { type: 'continue' };
291
+ }
292
+ if (this.isEnterKey(data)) {
293
+ if (this.submitAnswer()) {
294
+ this.state.warningMessage = '';
295
+ this.nextQuestion();
296
+ }
297
+ return { type: 'continue' };
298
+ }
299
+ if (this.isBackspaceKey(data)) {
300
+ this.state.inputBuffer = this.state.inputBuffer.slice(0, -1);
301
+ return { type: 'continue' };
302
+ }
303
+ // Printable character
304
+ const char = this.getPrintableChar(data);
305
+ if (char) {
306
+ this.state.inputBuffer += char;
307
+ }
308
+ return { type: 'continue' };
230
309
  }
231
- // Footer with instructions
232
- const currentQ = state.isOnSubmitTab ? null : questions[state.currentQuestion];
233
- lines.push(...renderFooter(state.isTypingCustom, currentQ?.multiSelect === true, state.isOnSubmitTab));
234
- // Warning message if present
235
- if (state.warningMessage) {
236
- lines.push('');
237
- lines.push(s.warning(` ${state.warningMessage}`));
310
+ handleNavigationKey(data) {
311
+ const optionCount = this.getOptionCount();
312
+ const currentQ = this.questions[this.state.currentQuestion];
313
+ const isMultiSelect = currentQ.multiSelect === true;
314
+ const options = currentQ.options ?? [];
315
+ const allowCustom = currentQ.allowCustom !== false;
316
+ const customIndex = options.length;
317
+ if (this.isEscapeKey(data)) {
318
+ return { type: 'close', result: this.getResult() };
319
+ }
320
+ if (this.isUpArrowKey(data)) {
321
+ this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
322
+ if (!isMultiSelect && allowCustom && this.state.selectedIndex === customIndex) {
323
+ this.state.isTypingCustom = true;
324
+ }
325
+ return { type: 'continue' };
326
+ }
327
+ if (this.isDownArrowKey(data)) {
328
+ this.state.selectedIndex = Math.min(optionCount - 1, this.state.selectedIndex + 1);
329
+ if (!isMultiSelect && allowCustom && this.state.selectedIndex === customIndex) {
330
+ this.state.isTypingCustom = true;
331
+ }
332
+ return { type: 'continue' };
333
+ }
334
+ if (this.isLeftArrowKey(data)) {
335
+ this.prevQuestion();
336
+ return { type: 'continue' };
337
+ }
338
+ if (this.isRightArrowKey(data) || this.isTabKey(data)) {
339
+ this.nextQuestion();
340
+ return { type: 'continue' };
341
+ }
342
+ if (this.isSpaceKey(data) && isMultiSelect) {
343
+ if (this.state.multiSelections.has(this.state.selectedIndex)) {
344
+ this.state.multiSelections.delete(this.state.selectedIndex);
345
+ }
346
+ else {
347
+ this.state.multiSelections.add(this.state.selectedIndex);
348
+ }
349
+ return { type: 'continue' };
350
+ }
351
+ if (this.isEnterKey(data)) {
352
+ if (this.submitAnswer()) {
353
+ this.state.warningMessage = '';
354
+ this.nextQuestion();
355
+ }
356
+ return { type: 'continue' };
357
+ }
358
+ // Number keys 1-9
359
+ if (data.length === 1 && data[0] >= 0x31 && data[0] <= 0x39) {
360
+ const numIndex = data[0] - 0x31;
361
+ if (numIndex < optionCount) {
362
+ this.state.selectedIndex = numIndex;
363
+ if (!isMultiSelect) {
364
+ if (this.submitAnswer()) {
365
+ this.state.warningMessage = '';
366
+ this.nextQuestion();
367
+ }
368
+ }
369
+ }
370
+ }
371
+ return { type: 'continue' };
238
372
  }
239
- // Bottom border to close the overlay
240
- lines.push(border);
241
- // Pad with empty lines to maintain consistent height (prevents cursor drift)
242
- while (lines.length < targetLineCount) {
243
- lines.push('');
373
+ // ===========================================================================
374
+ // Private: Helper Methods
375
+ // ===========================================================================
376
+ getResult() {
377
+ const skipped = this.questions.filter((q) => !(q.id in this.state.answers)).map((q) => q.id);
378
+ return {
379
+ answers: this.state.answers,
380
+ skipped,
381
+ };
244
382
  }
245
- // Render all lines (use write + join like config-overlay to match clearLinesAbove behavior)
246
- terminal.write(lines.join('\n'));
247
- debugLog(`render done: newLines=${String(lines.length)}, optionCount=${String(questions[state.currentQuestion].options?.length ?? 0)}`);
248
- return lines.length;
249
- }
250
- // =============================================================================
251
- // Main Export
252
- // =============================================================================
253
- /**
254
- * Show the ask user overlay
255
- */
256
- export async function showAskUserOverlay(options) {
257
- const { questions, context } = options;
258
- // Check if first question should auto-start in typing mode
259
- // (no options, custom allowed, single-select)
260
- const firstQ = questions[0];
261
- const firstQAutoType = (firstQ.options?.length ?? 0) === 0 &&
262
- firstQ.allowCustom !== false &&
263
- firstQ.multiSelect !== true;
264
- const state = {
265
- currentQuestion: 0,
266
- selectedIndex: 0,
267
- inputBuffer: '',
268
- isTypingCustom: firstQAutoType,
269
- answers: {},
270
- multiSelections: new Set(),
271
- cancelled: false,
272
- warningMessage: '',
273
- isOnSubmitTab: false,
274
- };
275
- let lineCount = 0;
276
- let maxLineCount = 0; // Track max lines ever rendered to ensure full clearing
277
- // NOTE: Footer is already paused by the caller (index.ts handler calls sharedState.pauseFooter)
278
- // Do NOT call pauseForOverlay() here - it causes double-pause issues
279
- // The caller's pause already clears the footer from screen
280
- // Ensure we start from a fresh line (like config-overlay does)
281
- terminal.writeLine('');
282
- terminal.hideCursor();
283
- const wasRawMode = process.stdin.isRaw;
284
- terminal.enableRawMode();
285
- // Initial render (no re-render needed - matches config-overlay pattern)
286
- lineCount = render(questions, state, context, 0);
287
- maxLineCount = Math.max(maxLineCount, lineCount);
288
- // Helper: Get option count for current question
289
- const getOptionCount = () => {
290
- const q = questions[state.currentQuestion];
383
+ getOptionCount() {
384
+ const q = this.questions[this.state.currentQuestion];
291
385
  const optionCount = q.options?.length ?? 0;
292
386
  const allowCustom = q.allowCustom !== false;
293
387
  return allowCustom ? optionCount + 1 : optionCount;
294
- };
295
- // Helper: Restore state from existing answer for current question
296
- const restoreFromAnswer = () => {
297
- const q = questions[state.currentQuestion];
388
+ }
389
+ getAnswerInfo(question) {
390
+ const options = question.options ?? [];
391
+ const isMultiSelect = question.multiSelect === true;
392
+ const answeredOptions = new Set();
393
+ let customAnswerValue = '';
394
+ if (question.id in this.state.answers) {
395
+ const existingAnswer = this.state.answers[question.id];
396
+ if (isMultiSelect && Array.isArray(existingAnswer)) {
397
+ for (const ans of existingAnswer) {
398
+ const idx = options.indexOf(ans);
399
+ if (idx >= 0) {
400
+ answeredOptions.add(idx);
401
+ }
402
+ else {
403
+ customAnswerValue = ans;
404
+ }
405
+ }
406
+ }
407
+ else if (typeof existingAnswer === 'string') {
408
+ const idx = options.indexOf(existingAnswer);
409
+ if (idx >= 0) {
410
+ answeredOptions.add(idx);
411
+ }
412
+ else {
413
+ customAnswerValue = existingAnswer;
414
+ }
415
+ }
416
+ }
417
+ return { answeredOptions, customAnswerValue };
418
+ }
419
+ restoreFromAnswer() {
420
+ const q = this.questions[this.state.currentQuestion];
298
421
  const options = q.options ?? [];
299
422
  const isMultiSelect = q.multiSelect === true;
300
423
  const allowCustom = q.allowCustom !== false;
301
424
  // Reset state first
302
- state.selectedIndex = 0;
303
- state.inputBuffer = '';
304
- state.isTypingCustom = false;
305
- state.multiSelections.clear();
306
- if (!(q.id in state.answers)) {
425
+ this.state.selectedIndex = 0;
426
+ this.state.inputBuffer = '';
427
+ this.state.isTypingCustom = false;
428
+ this.state.multiSelections.clear();
429
+ if (!(q.id in this.state.answers)) {
307
430
  // No existing answer - check if we should auto-type
308
- // (no options, custom allowed, single-select)
309
431
  if (options.length === 0 && allowCustom && !isMultiSelect) {
310
- state.isTypingCustom = true;
432
+ this.state.isTypingCustom = true;
311
433
  }
312
434
  return;
313
435
  }
314
- const existingAnswer = state.answers[q.id];
436
+ const existingAnswer = this.state.answers[q.id];
315
437
  if (isMultiSelect && Array.isArray(existingAnswer)) {
316
- // Multi-select: restore all selected options
317
438
  for (const answer of existingAnswer) {
318
439
  const idx = options.indexOf(answer);
319
440
  if (idx >= 0) {
320
- state.multiSelections.add(idx);
441
+ this.state.multiSelections.add(idx);
321
442
  }
322
443
  else {
323
- // Custom answer - add to custom index and set buffer
324
- state.multiSelections.add(options.length);
325
- state.inputBuffer = answer;
444
+ this.state.multiSelections.add(options.length);
445
+ this.state.inputBuffer = answer;
326
446
  }
327
447
  }
328
- // Set selectedIndex to first selected item
329
- if (state.multiSelections.size > 0) {
330
- state.selectedIndex = Math.min(...state.multiSelections);
448
+ if (this.state.multiSelections.size > 0) {
449
+ this.state.selectedIndex = Math.min(...this.state.multiSelections);
331
450
  }
332
451
  }
333
452
  else if (typeof existingAnswer === 'string') {
334
- // Single select: find which option was selected
335
453
  const idx = options.indexOf(existingAnswer);
336
454
  if (idx >= 0) {
337
- state.selectedIndex = idx;
455
+ this.state.selectedIndex = idx;
338
456
  }
339
457
  else {
340
- // Custom answer
341
- state.selectedIndex = options.length; // Custom option index
342
- state.inputBuffer = existingAnswer;
343
- // Don't enter typing mode - just show the value
458
+ this.state.selectedIndex = options.length;
459
+ this.state.inputBuffer = existingAnswer;
344
460
  }
345
461
  }
346
- };
347
- // Helper: Navigate to next question or Submit tab
348
- const nextQuestion = () => {
349
- if (state.isOnSubmitTab) {
350
- // Already on Submit tab, do nothing
462
+ }
463
+ nextQuestion() {
464
+ if (this.state.isOnSubmitTab) {
351
465
  return;
352
466
  }
353
- if (state.currentQuestion < questions.length - 1) {
354
- // Move to next question
355
- debugLog(`nextQuestion: ${String(state.currentQuestion)} -> ${String(state.currentQuestion + 1)}, lineCount=${String(lineCount)}`);
356
- state.currentQuestion++;
357
- restoreFromAnswer();
467
+ if (this.state.currentQuestion < this.questions.length - 1) {
468
+ this.state.currentQuestion++;
469
+ this.restoreFromAnswer();
358
470
  }
359
471
  else {
360
472
  // On last question - move to Submit tab
361
- debugLog(`nextQuestion: moving to Submit tab, lineCount=${String(lineCount)}`);
362
- state.isOnSubmitTab = true;
363
- // IMPORTANT: Clear typing mode when entering Submit tab
364
- // Otherwise, Enter key is captured by typing mode handler instead of Submit handler
365
- state.isTypingCustom = false;
366
- state.inputBuffer = '';
367
- }
368
- };
369
- // Helper: Navigate to previous question (or from Submit tab to last question)
370
- const prevQuestion = () => {
371
- if (state.isOnSubmitTab) {
372
- // Move from Submit tab back to last question
373
- debugLog(`prevQuestion: leaving Submit tab to question ${String(questions.length - 1)}`);
374
- state.isOnSubmitTab = false;
375
- restoreFromAnswer();
473
+ this.state.isOnSubmitTab = true;
474
+ this.state.isTypingCustom = false;
475
+ this.state.inputBuffer = '';
476
+ }
477
+ }
478
+ prevQuestion() {
479
+ if (this.state.isOnSubmitTab) {
480
+ this.state.isOnSubmitTab = false;
481
+ this.restoreFromAnswer();
376
482
  return;
377
483
  }
378
- if (state.currentQuestion > 0) {
379
- debugLog(`prevQuestion: ${String(state.currentQuestion)} -> ${String(state.currentQuestion - 1)}, lineCount=${String(lineCount)}`);
380
- state.currentQuestion--;
381
- restoreFromAnswer();
382
- }
383
- };
384
- // Helper: Get list of unanswered question indices
385
- const getUnansweredIndices = () => {
386
- const unanswered = [];
387
- for (let i = 0; i < questions.length; i++) {
388
- if (!(questions[i].id in state.answers)) {
389
- unanswered.push(i);
390
- }
391
- }
392
- return unanswered;
393
- };
394
- // Helper: Check if all questions are answered and either complete or navigate to first unanswered
395
- // Returns true if all complete and we should resolve, false if navigated to unanswered
396
- const tryComplete = () => {
397
- const unanswered = getUnansweredIndices();
484
+ if (this.state.currentQuestion > 0) {
485
+ this.state.currentQuestion--;
486
+ this.restoreFromAnswer();
487
+ }
488
+ }
489
+ tryComplete() {
490
+ const unanswered = this.questions.filter((q) => !(q.id in this.state.answers));
398
491
  if (unanswered.length === 0) {
399
- // All questions answered - can complete
400
- state.warningMessage = '';
492
+ this.state.warningMessage = '';
401
493
  return true;
402
494
  }
403
495
  // Navigate to first unanswered question and show warning
404
- const firstUnanswered = unanswered[0];
405
- const unansweredCount = unanswered.length;
406
- state.warningMessage = `${String(unansweredCount)} question${unansweredCount > 1 ? 's' : ''} unanswered. Please answer all questions.`;
407
- state.currentQuestion = firstUnanswered;
408
- restoreFromAnswer();
496
+ const firstUnansweredIndex = this.questions.findIndex((q) => !(q.id in this.state.answers));
497
+ this.state.warningMessage = `${String(unanswered.length)} question${unanswered.length > 1 ? 's' : ''} unanswered. Please answer all questions.`;
498
+ this.state.currentQuestion = firstUnansweredIndex;
499
+ this.state.isOnSubmitTab = false;
500
+ this.restoreFromAnswer();
409
501
  return false;
410
- };
411
- // Helper: Submit answer for current question
412
- const submitAnswer = () => {
413
- const q = questions[state.currentQuestion];
502
+ }
503
+ submitAnswer() {
504
+ const q = this.questions[this.state.currentQuestion];
414
505
  const options = q.options ?? [];
415
506
  const allowCustom = q.allowCustom !== false;
416
507
  const isMultiSelect = q.multiSelect === true;
417
508
  const customIndex = options.length;
418
509
  if (isMultiSelect) {
419
- // Multi-select: gather selected options
420
510
  const selectedAnswers = [];
421
- for (const idx of state.multiSelections) {
511
+ for (const idx of this.state.multiSelections) {
422
512
  if (idx < options.length) {
423
513
  selectedAnswers.push(options[idx]);
424
514
  }
425
515
  }
426
- // Include custom if selected and typed
427
- if (state.multiSelections.has(customIndex) && state.inputBuffer.trim()) {
428
- selectedAnswers.push(state.inputBuffer.trim());
516
+ if (this.state.multiSelections.has(customIndex) && this.state.inputBuffer.trim()) {
517
+ selectedAnswers.push(this.state.inputBuffer.trim());
429
518
  }
430
519
  if (selectedAnswers.length > 0) {
431
- state.answers[q.id] = selectedAnswers;
520
+ this.state.answers[q.id] = selectedAnswers;
432
521
  return true;
433
522
  }
434
- return false; // Nothing selected
523
+ return false;
435
524
  }
436
525
  else {
437
- // Single select
438
- if (state.isTypingCustom) {
439
- const custom = state.inputBuffer.trim();
526
+ if (this.state.isTypingCustom) {
527
+ const custom = this.state.inputBuffer.trim();
440
528
  if (custom) {
441
- state.answers[q.id] = custom;
529
+ this.state.answers[q.id] = custom;
442
530
  return true;
443
531
  }
444
- return false; // Empty custom
532
+ return false;
445
533
  }
446
- else if (state.selectedIndex < options.length) {
447
- state.answers[q.id] = options[state.selectedIndex];
534
+ else if (this.state.selectedIndex < options.length) {
535
+ this.state.answers[q.id] = options[this.state.selectedIndex];
448
536
  return true;
449
537
  }
450
- else if (allowCustom && state.selectedIndex === customIndex) {
451
- // Enter custom input mode
452
- state.isTypingCustom = true;
453
- return false; // Don't advance yet
538
+ else if (allowCustom && this.state.selectedIndex === customIndex) {
539
+ this.state.isTypingCustom = true;
540
+ return false;
454
541
  }
455
542
  }
456
543
  return false;
457
- };
458
- return new Promise((resolve) => {
459
- const cleanup = () => {
460
- debugLog(`cleanup: clearing maxLineCount=${String(maxLineCount)}`);
461
- // Clear the overlay - use maxLineCount to ensure full clearing
462
- terminal.clearLinesAbove(maxLineCount);
463
- terminal.writeLine('');
464
- terminal.showCursor();
465
- if (!wasRawMode) {
466
- terminal.disableRawMode();
467
- }
468
- process.stdin.removeListener('data', handleData);
469
- // NOTE: Footer resume is handled by the caller (index.ts) in the finally block
470
- };
471
- const handleData = (data) => {
472
- onData(data);
473
- };
474
- const onData = (data) => {
475
- const isEscape = data.length === 1 && data[0] === 0x1b;
476
- const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
477
- const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
478
- const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
479
- const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
480
- const isCtrlC = data.length === 1 && data[0] === 0x03;
481
- const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
482
- const isBackspace = data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
483
- const isTab = data.length === 1 && data[0] === 0x09;
484
- const isSpace = data.length === 1 && data[0] === 0x20;
485
- // Helper to get skipped questions
486
- const getSkipped = () => questions.filter((q) => !(q.id in state.answers)).map((q) => q.id);
487
- // Ctrl+C always cancels
488
- if (isCtrlC) {
489
- cleanup();
490
- state.cancelled = true;
491
- resolve({
492
- answers: state.answers,
493
- skipped: getSkipped(),
494
- });
495
- return;
496
- }
497
- // Submit tab takes priority over typing mode
498
- // (This is a safeguard - nextQuestion() should already clear isTypingCustom)
499
- if (state.isOnSubmitTab) {
500
- // On Submit tab - handle special navigation
501
- if (isEscape) {
502
- // Cancel overlay
503
- cleanup();
504
- resolve({
505
- answers: state.answers,
506
- skipped: getSkipped(),
507
- });
508
- return;
509
- }
510
- else if (isLeftArrow || isTab) {
511
- // Go back to last question
512
- prevQuestion();
513
- }
514
- else if (isEnter) {
515
- // Try to submit - check completeness
516
- if (tryComplete()) {
517
- cleanup();
518
- resolve({
519
- answers: state.answers,
520
- skipped: getSkipped(),
521
- });
522
- return;
523
- }
524
- // tryComplete sets warning and navigates to first unanswered
525
- }
526
- // Re-render and return early
527
- lineCount = render(questions, state, context, maxLineCount, maxLineCount);
528
- maxLineCount = Math.max(maxLineCount, lineCount);
529
- return;
530
- }
531
- // If typing custom input
532
- if (state.isTypingCustom) {
533
- const currentQ = questions[state.currentQuestion];
534
- const options = currentQ.options ?? [];
535
- if (isEscape) {
536
- // Cancel custom input
537
- state.isTypingCustom = false;
538
- state.inputBuffer = '';
539
- }
540
- else if (isUpArrow && options.length > 0) {
541
- // Exit typing mode and navigate up (only if there are other options)
542
- state.isTypingCustom = false;
543
- state.inputBuffer = '';
544
- state.selectedIndex = Math.max(0, state.selectedIndex - 1);
545
- }
546
- else if (isDownArrow) {
547
- // Custom is last option, so down arrow does nothing
548
- // But we handle it to be consistent
549
- }
550
- else if (isEnter) {
551
- // Submit custom input
552
- if (submitAnswer()) {
553
- // Clear warning on successful submit
554
- state.warningMessage = '';
555
- // Move to next question (or Submit tab if on last question)
556
- nextQuestion();
557
- }
558
- }
559
- else if (isBackspace) {
560
- state.inputBuffer = state.inputBuffer.slice(0, -1);
561
- }
562
- else if (data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f) {
563
- // Printable character
564
- state.inputBuffer += String.fromCharCode(data[0]);
565
- }
566
- }
567
- else {
568
- // Navigation mode - on a question (Submit tab is handled earlier)
569
- const optionCount = getOptionCount();
570
- const currentQ = questions[state.currentQuestion];
571
- const isMultiSelect = currentQ.multiSelect === true;
572
- const options = currentQ.options ?? [];
573
- const allowCustom = currentQ.allowCustom !== false;
574
- const customIndex = options.length;
575
- if (isEscape) {
576
- // Cancel overlay
577
- cleanup();
578
- resolve({
579
- answers: state.answers,
580
- skipped: getSkipped(),
581
- });
582
- return;
583
- }
584
- else if (isUpArrow) {
585
- state.selectedIndex = Math.max(0, state.selectedIndex - 1);
586
- // Auto-enable typing when navigating to custom option (single-select only)
587
- if (!isMultiSelect && allowCustom && state.selectedIndex === customIndex) {
588
- state.isTypingCustom = true;
589
- }
590
- }
591
- else if (isDownArrow) {
592
- state.selectedIndex = Math.min(optionCount - 1, state.selectedIndex + 1);
593
- // Auto-enable typing when navigating to custom option (single-select only)
594
- if (!isMultiSelect && allowCustom && state.selectedIndex === customIndex) {
595
- state.isTypingCustom = true;
596
- }
597
- }
598
- else if (isLeftArrow) {
599
- prevQuestion();
600
- }
601
- else if (isRightArrow || isTab) {
602
- // Tab or right arrow: move to next question (or Submit tab)
603
- nextQuestion();
604
- }
605
- else if (isSpace && isMultiSelect) {
606
- // Toggle multi-select
607
- if (state.multiSelections.has(state.selectedIndex)) {
608
- state.multiSelections.delete(state.selectedIndex);
609
- }
610
- else {
611
- state.multiSelections.add(state.selectedIndex);
612
- }
613
- }
614
- else if (isEnter) {
615
- const submitted = submitAnswer();
616
- if (submitted) {
617
- // Clear warning on successful submit
618
- state.warningMessage = '';
619
- // Move to next question (or Submit tab)
620
- nextQuestion();
621
- }
622
- }
623
- else if (data.length === 1 && data[0] >= 0x31 && data[0] <= 0x39) {
624
- // Number keys 1-9
625
- const numIndex = data[0] - 0x31; // 0-indexed
626
- if (numIndex < optionCount) {
627
- state.selectedIndex = numIndex;
628
- if (!isMultiSelect) {
629
- // Auto-select for single select
630
- const submitted = submitAnswer();
631
- if (submitted) {
632
- // Clear warning on successful submit
633
- state.warningMessage = '';
634
- // Move to next question (or Submit tab)
635
- nextQuestion();
636
- }
637
- }
638
- }
639
- }
640
- }
641
- // Re-render - use maxLineCount for both clearing AND target height to prevent cursor drift
642
- lineCount = render(questions, state, context, maxLineCount, maxLineCount);
643
- maxLineCount = Math.max(maxLineCount, lineCount);
644
- };
645
- process.stdin.on('data', handleData);
646
- });
544
+ }
545
+ }
546
+ // =============================================================================
547
+ // Main Export
548
+ // =============================================================================
549
+ /**
550
+ * Show the ask user overlay
551
+ */
552
+ export async function showAskUserOverlay(options) {
553
+ const overlay = new AskUserOverlayImpl(options);
554
+ return overlay.show();
647
555
  }