opencastle 0.5.1 → 0.7.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 (231) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/adapters/claude-code.d.ts +2 -2
  3. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/cli/adapters/claude-code.js +31 -4
  5. package/dist/cli/adapters/claude-code.js.map +1 -1
  6. package/dist/cli/adapters/cursor.d.ts +2 -2
  7. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  8. package/dist/cli/adapters/cursor.js +28 -4
  9. package/dist/cli/adapters/cursor.js.map +1 -1
  10. package/dist/cli/adapters/opencode.d.ts +20 -0
  11. package/dist/cli/adapters/opencode.d.ts.map +1 -0
  12. package/dist/cli/adapters/opencode.js +265 -0
  13. package/dist/cli/adapters/opencode.js.map +1 -0
  14. package/dist/cli/adapters/vscode.d.ts +2 -2
  15. package/dist/cli/adapters/vscode.d.ts.map +1 -1
  16. package/dist/cli/adapters/vscode.js +38 -7
  17. package/dist/cli/adapters/vscode.js.map +1 -1
  18. package/dist/cli/copy.d.ts +12 -0
  19. package/dist/cli/copy.d.ts.map +1 -1
  20. package/dist/cli/copy.js +27 -0
  21. package/dist/cli/copy.js.map +1 -1
  22. package/dist/cli/detect.d.ts +18 -0
  23. package/dist/cli/detect.d.ts.map +1 -0
  24. package/dist/cli/detect.js +434 -0
  25. package/dist/cli/detect.js.map +1 -0
  26. package/dist/cli/gitignore.d.ts.map +1 -1
  27. package/dist/cli/gitignore.js +0 -2
  28. package/dist/cli/gitignore.js.map +1 -1
  29. package/dist/cli/init.d.ts.map +1 -1
  30. package/dist/cli/init.js +154 -91
  31. package/dist/cli/init.js.map +1 -1
  32. package/dist/cli/manifest.d.ts +1 -1
  33. package/dist/cli/manifest.d.ts.map +1 -1
  34. package/dist/cli/manifest.js +2 -1
  35. package/dist/cli/manifest.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -6
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +105 -34
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/prompt.d.ts +22 -0
  41. package/dist/cli/prompt.d.ts.map +1 -1
  42. package/dist/cli/prompt.js +239 -0
  43. package/dist/cli/prompt.js.map +1 -1
  44. package/dist/cli/stack-config.d.ts +26 -3
  45. package/dist/cli/stack-config.d.ts.map +1 -1
  46. package/dist/cli/stack-config.js +140 -125
  47. package/dist/cli/stack-config.js.map +1 -1
  48. package/dist/cli/types.d.ts +46 -10
  49. package/dist/cli/types.d.ts.map +1 -1
  50. package/dist/cli/types.js +26 -1
  51. package/dist/cli/types.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +66 -19
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/orchestrator/plugins/chrome-devtools/config.d.ts +3 -0
  56. package/dist/orchestrator/plugins/chrome-devtools/config.d.ts.map +1 -0
  57. package/dist/orchestrator/plugins/chrome-devtools/config.js +28 -0
  58. package/dist/orchestrator/plugins/chrome-devtools/config.js.map +1 -0
  59. package/dist/orchestrator/plugins/contentful/config.d.ts +3 -0
  60. package/dist/orchestrator/plugins/contentful/config.d.ts.map +1 -0
  61. package/dist/orchestrator/plugins/contentful/config.js +48 -0
  62. package/dist/orchestrator/plugins/contentful/config.js.map +1 -0
  63. package/dist/orchestrator/plugins/convex/config.d.ts +3 -0
  64. package/dist/orchestrator/plugins/convex/config.d.ts.map +1 -0
  65. package/dist/orchestrator/plugins/convex/config.js +32 -0
  66. package/dist/orchestrator/plugins/convex/config.js.map +1 -0
  67. package/dist/orchestrator/plugins/index.d.ts +28 -0
  68. package/dist/orchestrator/plugins/index.d.ts.map +1 -0
  69. package/dist/orchestrator/plugins/index.js +63 -0
  70. package/dist/orchestrator/plugins/index.js.map +1 -0
  71. package/dist/orchestrator/plugins/jira/config.d.ts +3 -0
  72. package/dist/orchestrator/plugins/jira/config.d.ts.map +1 -0
  73. package/dist/orchestrator/plugins/jira/config.js +29 -0
  74. package/dist/orchestrator/plugins/jira/config.js.map +1 -0
  75. package/dist/orchestrator/plugins/linear/config.d.ts +3 -0
  76. package/dist/orchestrator/plugins/linear/config.d.ts.map +1 -0
  77. package/dist/orchestrator/plugins/linear/config.js +33 -0
  78. package/dist/orchestrator/plugins/linear/config.js.map +1 -0
  79. package/dist/orchestrator/plugins/nx/config.d.ts +3 -0
  80. package/dist/orchestrator/plugins/nx/config.d.ts.map +1 -0
  81. package/dist/orchestrator/plugins/nx/config.js +28 -0
  82. package/dist/orchestrator/plugins/nx/config.js.map +1 -0
  83. package/dist/orchestrator/plugins/sanity/config.d.ts +3 -0
  84. package/dist/orchestrator/plugins/sanity/config.d.ts.map +1 -0
  85. package/dist/orchestrator/plugins/sanity/config.js +43 -0
  86. package/dist/orchestrator/plugins/sanity/config.js.map +1 -0
  87. package/dist/orchestrator/plugins/slack/config.d.ts +3 -0
  88. package/dist/orchestrator/plugins/slack/config.d.ts.map +1 -0
  89. package/dist/orchestrator/plugins/slack/config.js +34 -0
  90. package/dist/orchestrator/plugins/slack/config.js.map +1 -0
  91. package/dist/orchestrator/plugins/strapi/config.d.ts +3 -0
  92. package/dist/orchestrator/plugins/strapi/config.d.ts.map +1 -0
  93. package/dist/orchestrator/plugins/strapi/config.js +40 -0
  94. package/dist/orchestrator/plugins/strapi/config.js.map +1 -0
  95. package/dist/orchestrator/plugins/supabase/config.d.ts +3 -0
  96. package/dist/orchestrator/plugins/supabase/config.d.ts.map +1 -0
  97. package/dist/orchestrator/plugins/supabase/config.js +33 -0
  98. package/dist/orchestrator/plugins/supabase/config.js.map +1 -0
  99. package/dist/orchestrator/plugins/teams/config.d.ts +3 -0
  100. package/dist/orchestrator/plugins/teams/config.d.ts.map +1 -0
  101. package/dist/orchestrator/plugins/teams/config.js +43 -0
  102. package/dist/orchestrator/plugins/teams/config.js.map +1 -0
  103. package/dist/orchestrator/plugins/types.d.ts +61 -0
  104. package/dist/orchestrator/plugins/types.d.ts.map +1 -0
  105. package/dist/orchestrator/plugins/types.js +2 -0
  106. package/dist/orchestrator/plugins/types.js.map +1 -0
  107. package/dist/orchestrator/plugins/vercel/config.d.ts +3 -0
  108. package/dist/orchestrator/plugins/vercel/config.d.ts.map +1 -0
  109. package/dist/orchestrator/plugins/vercel/config.js +32 -0
  110. package/dist/orchestrator/plugins/vercel/config.js.map +1 -0
  111. package/package.json +1 -1
  112. package/src/cli/adapters/claude-code.ts +40 -6
  113. package/src/cli/adapters/cursor.ts +46 -6
  114. package/src/cli/adapters/opencode.ts +320 -0
  115. package/src/cli/adapters/vscode.ts +43 -9
  116. package/src/cli/copy.ts +32 -0
  117. package/src/cli/detect.ts +483 -0
  118. package/src/cli/gitignore.ts +0 -3
  119. package/src/cli/init.ts +169 -96
  120. package/src/cli/manifest.ts +2 -1
  121. package/src/cli/mcp.ts +131 -51
  122. package/src/cli/prompt.ts +299 -0
  123. package/src/cli/stack-config.ts +187 -145
  124. package/src/cli/types.ts +60 -9
  125. package/src/cli/update.ts +78 -20
  126. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  127. package/src/orchestrator/agent-workflows/README.md +1 -1
  128. package/src/orchestrator/agent-workflows/bug-fix.md +12 -12
  129. package/src/orchestrator/agent-workflows/data-pipeline.md +21 -20
  130. package/src/orchestrator/agent-workflows/database-migration.md +11 -11
  131. package/src/orchestrator/agent-workflows/feature-implementation.md +10 -10
  132. package/src/orchestrator/agent-workflows/performance-optimization.md +6 -6
  133. package/src/orchestrator/agent-workflows/refactoring.md +10 -10
  134. package/src/orchestrator/agent-workflows/schema-changes.md +8 -8
  135. package/src/orchestrator/agent-workflows/security-audit.md +12 -12
  136. package/src/orchestrator/agent-workflows/shared-delivery-phase.md +5 -5
  137. package/src/orchestrator/agents/api-designer.agent.md +2 -2
  138. package/src/orchestrator/agents/architect.agent.md +2 -2
  139. package/src/orchestrator/agents/content-engineer.agent.md +4 -4
  140. package/src/orchestrator/agents/copywriter.agent.md +2 -2
  141. package/src/orchestrator/agents/data-expert.agent.md +6 -6
  142. package/src/orchestrator/agents/database-engineer.agent.md +4 -4
  143. package/src/orchestrator/agents/developer.agent.md +5 -5
  144. package/src/orchestrator/agents/devops-expert.agent.md +5 -5
  145. package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
  146. package/src/orchestrator/agents/performance-expert.agent.md +3 -3
  147. package/src/orchestrator/agents/release-manager.agent.md +4 -4
  148. package/src/orchestrator/agents/researcher.agent.md +19 -3
  149. package/src/orchestrator/agents/reviewer.agent.md +2 -4
  150. package/src/orchestrator/agents/security-expert.agent.md +4 -4
  151. package/src/orchestrator/agents/seo-specialist.agent.md +2 -2
  152. package/src/orchestrator/agents/team-lead.agent.md +97 -101
  153. package/src/orchestrator/agents/testing-expert.agent.md +5 -5
  154. package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -7
  155. package/src/orchestrator/copilot-instructions.md +1 -1
  156. package/src/orchestrator/customizations/AGENT-FAILURES.md +1 -1
  157. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +12 -12
  158. package/src/orchestrator/customizations/DISPUTES.md +5 -5
  159. package/src/orchestrator/customizations/KNOWN-ISSUES.md +30 -0
  160. package/src/orchestrator/customizations/LESSONS-LEARNED.md +7 -7
  161. package/src/orchestrator/customizations/README.md +5 -2
  162. package/src/orchestrator/customizations/agents/agent-registry.md +1 -1
  163. package/src/orchestrator/customizations/agents/skill-matrix.md +12 -7
  164. package/src/orchestrator/customizations/logs/README.md +1 -1
  165. package/src/orchestrator/customizations/project/decisions.md +31 -0
  166. package/src/orchestrator/customizations/project/docs-structure.md +16 -5
  167. package/src/orchestrator/customizations/project/roadmap.md +24 -0
  168. package/src/orchestrator/customizations/project/tracker-config.md +1 -1
  169. package/src/orchestrator/customizations/stack/cms-config.md +1 -1
  170. package/src/orchestrator/customizations/stack/notifications-config.md +1 -1
  171. package/src/orchestrator/instructions/ai-optimization.instructions.md +2 -2
  172. package/src/orchestrator/instructions/general.instructions.md +102 -40
  173. package/src/orchestrator/{skills/browser-testing → plugins/chrome-devtools}/SKILL.md +1 -1
  174. package/src/orchestrator/plugins/chrome-devtools/config.ts +29 -0
  175. package/src/orchestrator/{skills/contentful-cms → plugins/contentful}/SKILL.md +1 -1
  176. package/src/orchestrator/plugins/contentful/config.ts +49 -0
  177. package/src/orchestrator/{skills/convex-database → plugins/convex}/SKILL.md +1 -1
  178. package/src/orchestrator/plugins/convex/config.ts +33 -0
  179. package/src/orchestrator/plugins/index.ts +85 -0
  180. package/src/orchestrator/{skills/jira-management → plugins/jira}/SKILL.md +3 -3
  181. package/src/orchestrator/plugins/jira/config.ts +30 -0
  182. package/src/orchestrator/{skills/task-management → plugins/linear}/SKILL.md +3 -3
  183. package/src/orchestrator/plugins/linear/config.ts +34 -0
  184. package/src/orchestrator/{skills/nx-workspace → plugins/nx}/SKILL.md +1 -1
  185. package/src/orchestrator/plugins/nx/config.ts +29 -0
  186. package/src/orchestrator/{skills/sanity-cms → plugins/sanity}/SKILL.md +1 -1
  187. package/src/orchestrator/plugins/sanity/config.ts +44 -0
  188. package/src/orchestrator/{skills/slack-notifications → plugins/slack}/SKILL.md +2 -2
  189. package/src/orchestrator/plugins/slack/config.ts +35 -0
  190. package/src/orchestrator/{skills/strapi-cms → plugins/strapi}/SKILL.md +1 -1
  191. package/src/orchestrator/plugins/strapi/config.ts +41 -0
  192. package/src/orchestrator/{skills/supabase-database → plugins/supabase}/SKILL.md +1 -1
  193. package/src/orchestrator/plugins/supabase/config.ts +34 -0
  194. package/src/orchestrator/{skills/teams-notifications → plugins/teams}/SKILL.md +2 -2
  195. package/src/orchestrator/plugins/teams/config.ts +44 -0
  196. package/src/orchestrator/plugins/types.ts +79 -0
  197. package/src/orchestrator/plugins/vercel/config.ts +33 -0
  198. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +59 -12
  199. package/src/orchestrator/prompts/brainstorm.prompt.md +3 -3
  200. package/src/orchestrator/prompts/bug-fix.prompt.md +18 -18
  201. package/src/orchestrator/prompts/create-skill.prompt.md +50 -32
  202. package/src/orchestrator/prompts/generate-task-spec.prompt.md +3 -3
  203. package/src/orchestrator/prompts/implement-feature.prompt.md +26 -26
  204. package/src/orchestrator/prompts/metrics-report.prompt.md +11 -11
  205. package/src/orchestrator/prompts/quick-refinement.prompt.md +16 -16
  206. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +2 -2
  207. package/src/orchestrator/skills/accessibility-standards/SKILL.md +1 -1
  208. package/src/orchestrator/skills/agent-hooks/SKILL.md +27 -18
  209. package/src/orchestrator/skills/agent-memory/SKILL.md +7 -7
  210. package/src/orchestrator/skills/api-patterns/SKILL.md +6 -6
  211. package/src/orchestrator/skills/code-commenting/SKILL.md +1 -1
  212. package/src/orchestrator/skills/context-map/SKILL.md +4 -4
  213. package/src/orchestrator/skills/data-engineering/SKILL.md +7 -4
  214. package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +2 -2
  215. package/src/orchestrator/skills/documentation-standards/SKILL.md +1 -1
  216. package/src/orchestrator/skills/fast-review/SKILL.md +3 -3
  217. package/src/orchestrator/skills/frontend-design/SKILL.md +1 -1
  218. package/src/orchestrator/skills/memory-merger/SKILL.md +8 -8
  219. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +1 -1
  220. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +2 -2
  221. package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +1 -1
  222. package/src/orchestrator/skills/performance-optimization/SKILL.md +1 -1
  223. package/src/orchestrator/skills/react-development/SKILL.md +3 -3
  224. package/src/orchestrator/skills/security-hardening/SKILL.md +27 -27
  225. package/src/orchestrator/skills/self-improvement/SKILL.md +14 -13
  226. package/src/orchestrator/skills/seo-patterns/SKILL.md +1 -1
  227. package/src/orchestrator/skills/session-checkpoints/SKILL.md +19 -19
  228. package/src/orchestrator/skills/team-lead-reference/SKILL.md +9 -9
  229. package/src/orchestrator/skills/testing-workflow/SKILL.md +13 -13
  230. package/src/orchestrator/skills/validation-gates/SKILL.md +8 -15
  231. package/src/orchestrator/mcp.json +0 -61
package/src/cli/prompt.ts CHANGED
@@ -2,6 +2,31 @@ import { createInterface, type Interface } from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import type { SelectOption } from './types.js';
4
4
 
5
+ // ── ANSI helpers ──────────────────────────────────────────────────
6
+
7
+ const ESC = '\x1B';
8
+ const CSI = `${ESC}[`;
9
+ const HIDE_CURSOR = `${CSI}?25l`;
10
+ const SHOW_CURSOR = `${CSI}?25h`;
11
+ const ERASE_LINE = `${CSI}2K`;
12
+
13
+ function moveUp(n: number): string {
14
+ return n > 0 ? `${CSI}${n}A` : '';
15
+ }
16
+
17
+ // ── Color helpers ─────────────────────────────────────────────────
18
+
19
+ /** ANSI color helpers for CLI output. */
20
+ export const c = {
21
+ cyan: (s: string) => `\x1B[36m${s}\x1B[0m`,
22
+ green: (s: string) => `\x1B[32m${s}\x1B[0m`,
23
+ yellow: (s: string) => `\x1B[33m${s}\x1B[0m`,
24
+ red: (s: string) => `\x1B[31m${s}\x1B[0m`,
25
+ bold: (s: string) => `\x1B[1m${s}\x1B[0m`,
26
+ dim: (s: string) => `\x1B[2m${s}\x1B[0m`,
27
+ magenta: (s: string) => `\x1B[35m${s}\x1B[0m`,
28
+ };
29
+
5
30
  // ── Line-buffered readline ────────────────────────────────────────
6
31
  // readline.question() drops lines that arrived between calls because
7
32
  // it only listens for the NEXT 'line' event. When piped input
@@ -70,10 +95,121 @@ export function closePrompts(): void {
70
95
 
71
96
  /**
72
97
  * Interactive single-choice selection prompt.
98
+ *
99
+ * TTY mode: arrow-key navigation (↑/↓) with Enter to confirm.
100
+ * Piped mode: falls back to number-based selection for scripts.
73
101
  */
74
102
  export async function select(
75
103
  message: string,
76
104
  options: SelectOption[]
105
+ ): Promise<string> {
106
+ if (stdin.isTTY) {
107
+ return selectInteractive(message, options);
108
+ }
109
+ return selectNumbered(message, options);
110
+ }
111
+
112
+ // ── Arrow-key selection (TTY) ─────────────────────────────────────
113
+
114
+ function renderOptions(
115
+ options: SelectOption[],
116
+ cursor: number,
117
+ initial: boolean
118
+ ): void {
119
+ // Move back up to overwrite previous render (skip on first draw)
120
+ if (!initial) {
121
+ stdout.write(moveUp(options.length));
122
+ }
123
+
124
+ for (let i = 0; i < options.length; i++) {
125
+ const active = i === cursor;
126
+ const marker = active ? '❯' : ' ';
127
+ const hint = options[i].hint ? ` — ${options[i].hint}` : '';
128
+ const label = active
129
+ ? `\x1B[36m${options[i].label}\x1B[0m${hint}`
130
+ : `${options[i].label}${hint}`;
131
+ stdout.write(`${ERASE_LINE}\r ${marker} ${label}\n`);
132
+ }
133
+ }
134
+
135
+ function selectInteractive(
136
+ message: string,
137
+ options: SelectOption[]
138
+ ): Promise<string> {
139
+ return new Promise<string>((resolve) => {
140
+ let cursor = 0;
141
+
142
+ // Pause the readline interface so raw mode can take over
143
+ if (_rl) _rl.pause();
144
+
145
+ stdout.write(`\n ${message}\n\n`);
146
+ stdout.write(HIDE_CURSOR);
147
+ renderOptions(options, cursor, true);
148
+
149
+ stdin.setRawMode(true);
150
+ stdin.resume();
151
+
152
+ const onData = (data: Buffer): void => {
153
+ const key = data.toString();
154
+
155
+ // Arrow up or k
156
+ if (key === `${ESC}[A` || key === 'k') {
157
+ cursor = (cursor - 1 + options.length) % options.length;
158
+ renderOptions(options, cursor, false);
159
+ return;
160
+ }
161
+
162
+ // Arrow down or j
163
+ if (key === `${ESC}[B` || key === 'j') {
164
+ cursor = (cursor + 1) % options.length;
165
+ renderOptions(options, cursor, false);
166
+ return;
167
+ }
168
+
169
+ // Enter
170
+ if (key === '\r' || key === '\n') {
171
+ cleanup();
172
+ // Re-render final state with the selected option highlighted
173
+ stdout.write(moveUp(options.length));
174
+ for (let i = 0; i < options.length; i++) {
175
+ const active = i === cursor;
176
+ const hint = options[i].hint ? ` — ${options[i].hint}` : '';
177
+ const label = active
178
+ ? `\x1B[36m${options[i].label}\x1B[0m${hint}`
179
+ : `\x1B[2m${options[i].label}${hint}\x1B[0m`;
180
+ const marker = active ? '✔' : ' ';
181
+ stdout.write(`${ERASE_LINE}\r ${marker} ${label}\n`);
182
+ }
183
+ stdout.write('\n');
184
+ resolve(options[cursor].value);
185
+ return;
186
+ }
187
+
188
+ // Ctrl+C
189
+ if (key === '\x03') {
190
+ cleanup();
191
+ stdout.write('\n');
192
+ process.exit(130);
193
+ }
194
+ };
195
+
196
+ function cleanup(): void {
197
+ stdin.removeListener('data', onData);
198
+ stdin.setRawMode(false);
199
+ stdout.write(SHOW_CURSOR);
200
+ // Resume readline for subsequent confirm() calls
201
+ if (_rl) _rl.resume();
202
+ }
203
+
204
+ stdin.on('data', onData);
205
+ });
206
+ }
207
+
208
+ // ── Number-based selection (piped / non-TTY) ──────────────────────
209
+
210
+ async function selectNumbered(
211
+ message: string,
212
+ options: SelectOption[]
77
213
  ): Promise<string> {
78
214
  console.log(`\n ${message}\n`);
79
215
  options.forEach((opt, i) => {
@@ -113,3 +249,166 @@ export async function confirm(
113
249
  if (!answer.trim()) return defaultYes;
114
250
  return answer.trim().toLowerCase().startsWith('y');
115
251
  }
252
+
253
+ // ── Multiselect ───────────────────────────────────────────────────
254
+
255
+ /**
256
+ * Interactive multi-choice selection prompt.
257
+ *
258
+ * TTY mode: arrow-key navigation (↑/↓), Space to toggle, Enter to confirm.
259
+ * Piped mode: falls back to comma-separated number input.
260
+ *
261
+ * Returns an array of selected values (possibly empty).
262
+ */
263
+ export async function multiselect(
264
+ message: string,
265
+ options: SelectOption[]
266
+ ): Promise<string[]> {
267
+ if (stdin.isTTY) {
268
+ return multiselectInteractive(message, options);
269
+ }
270
+ return multiselectNumbered(message, options);
271
+ }
272
+
273
+ // ── Arrow-key multiselect (TTY) ───────────────────────────────────
274
+
275
+ function renderMultiselectOptions(
276
+ options: SelectOption[],
277
+ cursor: number,
278
+ selected: Set<number>,
279
+ initial: boolean
280
+ ): void {
281
+ if (!initial) {
282
+ stdout.write(moveUp(options.length));
283
+ }
284
+
285
+ for (let i = 0; i < options.length; i++) {
286
+ const active = i === cursor;
287
+ const checked = selected.has(i);
288
+ const checkbox = checked ? `\x1B[32m✔\x1B[0m` : ' ';
289
+ const marker = active ? '❯' : ' ';
290
+ const hint = options[i].hint ? ` ${c.dim('—')} ${c.dim(options[i].hint!)}` : '';
291
+ const label = active
292
+ ? `\x1B[36m${options[i].label}\x1B[0m${hint}`
293
+ : `${options[i].label}${hint}`;
294
+ stdout.write(`${ERASE_LINE}\r ${marker} [${checkbox}] ${label}\n`);
295
+ }
296
+ }
297
+
298
+ function multiselectInteractive(
299
+ message: string,
300
+ options: SelectOption[]
301
+ ): Promise<string[]> {
302
+ return new Promise<string[]>((resolve) => {
303
+ let cursor = 0;
304
+ const selected = new Set<number>();
305
+ // Pre-select options marked as selected
306
+ for (let i = 0; i < options.length; i++) {
307
+ if (options[i].selected) selected.add(i);
308
+ }
309
+
310
+ if (_rl) _rl.pause();
311
+
312
+ stdout.write(`\n ${message} ${c.dim('(↑/↓ navigate, Space toggle, Enter confirm)')}\n\n`);
313
+ stdout.write(HIDE_CURSOR);
314
+ renderMultiselectOptions(options, cursor, selected, true);
315
+
316
+ stdin.setRawMode(true);
317
+ stdin.resume();
318
+
319
+ const onData = (data: Buffer): void => {
320
+ const key = data.toString();
321
+
322
+ // Arrow up or k
323
+ if (key === `${ESC}[A` || key === 'k') {
324
+ cursor = (cursor - 1 + options.length) % options.length;
325
+ renderMultiselectOptions(options, cursor, selected, false);
326
+ return;
327
+ }
328
+
329
+ // Arrow down or j
330
+ if (key === `${ESC}[B` || key === 'j') {
331
+ cursor = (cursor + 1) % options.length;
332
+ renderMultiselectOptions(options, cursor, selected, false);
333
+ return;
334
+ }
335
+
336
+ // Space — toggle selection
337
+ if (key === ' ') {
338
+ if (selected.has(cursor)) {
339
+ selected.delete(cursor);
340
+ } else {
341
+ selected.add(cursor);
342
+ }
343
+ renderMultiselectOptions(options, cursor, selected, false);
344
+ return;
345
+ }
346
+
347
+ // Enter — confirm
348
+ if (key === '\r' || key === '\n') {
349
+ cleanup();
350
+ // Final render
351
+ stdout.write(moveUp(options.length));
352
+ for (let i = 0; i < options.length; i++) {
353
+ const checked = selected.has(i);
354
+ const hint = options[i].hint ? ` ${c.dim('—')} ${c.dim(options[i].hint!)}` : '';
355
+ const checkbox = checked ? `\x1B[32m✔\x1B[0m` : ' ';
356
+ const label = checked
357
+ ? `\x1B[36m${options[i].label}\x1B[0m${hint}`
358
+ : `\x1B[2m${options[i].label}${hint}\x1B[0m`;
359
+ stdout.write(`${ERASE_LINE}\r [${checkbox}] ${label}\n`);
360
+ }
361
+ stdout.write('\n');
362
+ resolve(Array.from(selected).sort().map(i => options[i].value));
363
+ return;
364
+ }
365
+
366
+ // Ctrl+C
367
+ if (key === '\x03') {
368
+ cleanup();
369
+ stdout.write('\n');
370
+ process.exit(130);
371
+ }
372
+ };
373
+
374
+ function cleanup(): void {
375
+ stdin.removeListener('data', onData);
376
+ stdin.setRawMode(false);
377
+ stdout.write(SHOW_CURSOR);
378
+ if (_rl) _rl.resume();
379
+ }
380
+
381
+ stdin.on('data', onData);
382
+ });
383
+ }
384
+
385
+ // ── Number-based multiselect (piped / non-TTY) ────────────────────
386
+
387
+ async function multiselectNumbered(
388
+ message: string,
389
+ options: SelectOption[]
390
+ ): Promise<string[]> {
391
+ console.log(`\n ${message}\n`);
392
+ options.forEach((opt, i) => {
393
+ const hint = opt.hint ? ` — ${opt.hint}` : '';
394
+ console.log(` ${i + 1}) ${opt.label}${hint}`);
395
+ });
396
+
397
+ const preselected = options
398
+ .map((opt, i) => (opt.selected ? i + 1 : null))
399
+ .filter((n): n is number => n !== null);
400
+ const defaultHint = preselected.length > 0 ? preselected.join(',') : 'none';
401
+ const answer = await nextLine(`\n Select [comma-separated, e.g. 1,3] or Enter for ${defaultHint}: `);
402
+ if (!answer.trim()) {
403
+ return preselected.map(n => options[n - 1].value);
404
+ }
405
+
406
+ const nums = answer.split(',').map(s => parseInt(s.trim(), 10));
407
+ const result: string[] = [];
408
+ for (const num of nums) {
409
+ if (num >= 1 && num <= options.length) {
410
+ result.push(options[num - 1].value);
411
+ }
412
+ }
413
+ return result;
414
+ }