@interactive-inc/claude-funnel 0.4.1 → 0.8.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 (349) hide show
  1. package/README.md +233 -111
  2. package/dist/bin.js +1417 -0
  3. package/dist/gateway/daemon.js +513 -0
  4. package/dist/highlights-eq9cgrbb.scm +604 -0
  5. package/dist/highlights-ghv9g403.scm +205 -0
  6. package/dist/highlights-hk7bwhj4.scm +284 -0
  7. package/dist/highlights-r812a2qc.scm +150 -0
  8. package/dist/highlights-x6tmsnaa.scm +115 -0
  9. package/dist/injections-73j83es3.scm +27 -0
  10. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/lib/bin.ts +78 -0
  16. package/lib/{modules → cli}/router/to-request.ts +13 -20
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
  21. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
  22. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
  23. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
  24. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
  25. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
  26. package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
  27. package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
  28. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
  29. package/lib/cli/routes/channels.$channel.ts +34 -0
  30. package/lib/cli/routes/channels.add.$channel.ts +33 -0
  31. package/lib/cli/routes/channels.remove.$channel.ts +20 -0
  32. package/lib/cli/routes/channels.ts +39 -0
  33. package/lib/cli/routes/claude.ts +69 -0
  34. package/lib/cli/routes/gateway.listeners.ts +41 -0
  35. package/lib/cli/routes/gateway.logs.ts +123 -0
  36. package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
  37. package/lib/cli/routes/gateway.run.ts +41 -0
  38. package/lib/cli/routes/gateway.start.ts +50 -0
  39. package/lib/cli/routes/gateway.status.ts +19 -0
  40. package/lib/cli/routes/gateway.stop.ts +32 -0
  41. package/lib/cli/routes/gateway.ts +55 -0
  42. package/lib/cli/routes/index.ts +202 -0
  43. package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
  44. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
  45. package/lib/cli/routes/profiles.$profile.run.ts +36 -0
  46. package/lib/cli/routes/profiles.add.$profile.ts +46 -0
  47. package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
  48. package/lib/cli/routes/profiles.set.$profile.ts +46 -0
  49. package/lib/cli/routes/profiles.ts +40 -0
  50. package/lib/cli/routes/status.ts +93 -0
  51. package/lib/cli/routes/update.ts +27 -0
  52. package/lib/connectors/connector-config-schema.ts +16 -0
  53. package/lib/connectors/connector-factory.ts +94 -0
  54. package/lib/connectors/connector-listener.ts +20 -0
  55. package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
  56. package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
  57. package/lib/connectors/discord-listener.ts +111 -0
  58. package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
  59. package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
  60. package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +57 -22
  61. package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
  62. package/lib/connectors/schedule-connector-schema.ts +33 -0
  63. package/lib/connectors/schedule-listener.ts +207 -0
  64. package/lib/connectors/schedule-state-store.ts +54 -0
  65. package/lib/connectors/slack-adapter.ts +36 -0
  66. package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
  67. package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
  68. package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +39 -14
  69. package/lib/engine/channels/channels.ts +520 -0
  70. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +47 -62
  71. package/lib/engine/claude/gateway-controller.ts +4 -0
  72. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +9 -0
  73. package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
  74. package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
  75. package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
  76. package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
  77. package/lib/engine/id/id-generator.ts +7 -0
  78. package/lib/engine/id/memory-id-generator.ts +20 -0
  79. package/lib/engine/id/node-id-generator.ts +7 -0
  80. package/lib/engine/logger/logger.ts +11 -0
  81. package/lib/engine/logger/memory-logger.ts +28 -0
  82. package/lib/engine/logger/node-logger.ts +49 -0
  83. package/lib/engine/logger/noop-logger.ts +9 -0
  84. package/lib/engine/mcp/channel-server.ts +204 -0
  85. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +29 -10
  86. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  87. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  88. package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +5 -0
  89. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  90. package/lib/engine/profiles/profiles.ts +126 -0
  91. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  92. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  93. package/lib/engine/settings/settings-schema.ts +46 -0
  94. package/lib/engine/settings/settings-store.ts +110 -0
  95. package/lib/engine/time/clock.ts +15 -0
  96. package/lib/engine/time/memory-clock.ts +26 -0
  97. package/lib/engine/time/node-clock.ts +7 -0
  98. package/lib/funnel.ts +148 -56
  99. package/lib/gateway/auth-middleware.ts +44 -0
  100. package/lib/gateway/broadcaster.ts +319 -0
  101. package/lib/gateway/daemon.ts +47 -0
  102. package/lib/gateway/factory.ts +10 -0
  103. package/lib/gateway/funnel-event-store.ts +155 -0
  104. package/lib/gateway/gateway-server.ts +414 -0
  105. package/lib/gateway/gateway-token.ts +79 -0
  106. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +70 -27
  107. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +7 -3
  108. package/lib/gateway/listener-supervisor.ts +339 -0
  109. package/lib/gateway/listeners-client.ts +128 -0
  110. package/lib/gateway/resolve-daemon-script.ts +26 -0
  111. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  112. package/lib/gateway/routes/health.ts +13 -0
  113. package/lib/gateway/routes/index.ts +24 -0
  114. package/lib/gateway/routes/listeners.list.ts +6 -0
  115. package/lib/gateway/routes/listeners.restart.ts +15 -0
  116. package/lib/gateway/routes/listeners.start.ts +15 -0
  117. package/lib/gateway/routes/listeners.stop.ts +15 -0
  118. package/lib/gateway/routes/route-deps.ts +11 -0
  119. package/lib/gateway/routes/status.ts +15 -0
  120. package/lib/gateway/routes/validator.ts +17 -0
  121. package/lib/index.ts +50 -92
  122. package/lib/logger/leuco-human-file-writer.ts +65 -0
  123. package/lib/logger/leuco-human-logger.ts +98 -0
  124. package/lib/logger/leuco-human-record.ts +16 -0
  125. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  126. package/lib/logger/leuco-human-writer.ts +14 -0
  127. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  128. package/lib/logger/leuco-logger-record.ts +13 -0
  129. package/lib/logger/leuco-logger-sink.ts +33 -0
  130. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  131. package/lib/logger/leuco-logger.ts +135 -0
  132. package/lib/tui/app.tsx +357 -0
  133. package/lib/tui/components/add-row.tsx +18 -0
  134. package/lib/tui/components/brand.tsx +27 -0
  135. package/lib/tui/components/card.tsx +44 -0
  136. package/lib/tui/components/detail-bar.tsx +46 -0
  137. package/lib/tui/components/editable-field.tsx +33 -0
  138. package/lib/tui/components/empty-state.tsx +11 -0
  139. package/lib/tui/components/gateway-status.tsx +66 -0
  140. package/lib/tui/components/keymap.tsx +29 -0
  141. package/lib/tui/components/menu-item.tsx +73 -0
  142. package/lib/tui/components/menu.tsx +26 -0
  143. package/lib/tui/components/panel-header.tsx +22 -0
  144. package/lib/tui/components/readonly-field.tsx +18 -0
  145. package/lib/tui/components/section-header.tsx +25 -0
  146. package/lib/tui/components/selection-accent.tsx +32 -0
  147. package/lib/tui/components/session-item.tsx +33 -0
  148. package/lib/tui/components/session-list.tsx +33 -0
  149. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  150. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  151. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  152. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  153. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  154. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  155. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  156. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  157. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  158. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  159. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  160. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  161. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  162. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  163. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  164. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  165. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  166. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  167. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  168. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  169. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  170. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  171. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  172. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  173. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  174. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  175. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  176. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  177. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  178. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  179. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  180. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  181. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  182. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  183. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  184. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  185. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  186. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  187. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  188. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  189. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  190. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  191. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  192. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  193. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  194. package/lib/tui/components/view-shell.tsx +44 -0
  195. package/lib/tui/filter-input.tsx +33 -0
  196. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  197. package/lib/tui/parse-comma-list.ts +14 -0
  198. package/lib/tui/profile-launcher.tsx +61 -0
  199. package/lib/tui/scrollbar-options.ts +19 -0
  200. package/lib/tui/sidebar.tsx +50 -0
  201. package/lib/tui/theme.ts +40 -0
  202. package/lib/tui/tui.tsx +20 -0
  203. package/lib/tui/types.ts +38 -0
  204. package/lib/tui/unique-name.ts +18 -0
  205. package/lib/tui/use-event-stream.ts +133 -0
  206. package/lib/tui/use-snapshot.ts +99 -0
  207. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  208. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  209. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  210. package/lib/tui/utils/hascii/theme.ts +176 -0
  211. package/lib/tui/views/channels-view.tsx +108 -0
  212. package/lib/tui/views/connectors-view.tsx +164 -0
  213. package/lib/tui/views/events-view.tsx +160 -0
  214. package/lib/tui/views/listeners-view.tsx +80 -0
  215. package/lib/tui/views/profiles-view.tsx +152 -0
  216. package/package.json +51 -34
  217. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  218. package/lib/modules/channels/funnel-channels.ts +0 -155
  219. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  220. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  221. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  222. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  223. package/lib/modules/connectors/funnel-connector-stores.ts +0 -24
  224. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  225. package/lib/modules/connectors/funnel-connectors.ts +0 -145
  226. package/lib/modules/connectors/funnel-discord-listener.ts +0 -65
  227. package/lib/modules/connectors/funnel-discord-store.ts +0 -84
  228. package/lib/modules/connectors/funnel-gh-store.ts +0 -84
  229. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  230. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -124
  231. package/lib/modules/connectors/funnel-schedule-store.ts +0 -178
  232. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  233. package/lib/modules/connectors/funnel-slack-store.ts +0 -86
  234. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -77
  235. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  236. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  237. package/lib/modules/gateway/daemon.ts +0 -207
  238. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  239. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  240. package/lib/modules/logger.ts +0 -26
  241. package/lib/modules/mcp/channel-server.ts +0 -76
  242. package/lib/modules/profiles/funnel-profiles.ts +0 -123
  243. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  244. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  245. package/lib/modules/repos/funnel-repositories.ts +0 -107
  246. package/lib/modules/schedule/funnel-schedule.ts +0 -34
  247. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  248. package/lib/modules/settings/settings-schema.ts +0 -33
  249. package/lib/modules/tui/app.tsx +0 -44
  250. package/lib/modules/tui/tui.tsx +0 -13
  251. package/lib/routes/channels/add.help.ts +0 -3
  252. package/lib/routes/channels/add.ts +0 -21
  253. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  254. package/lib/routes/channels/connectors-attach.ts +0 -17
  255. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  256. package/lib/routes/channels/connectors-detach.ts +0 -17
  257. package/lib/routes/channels/group.help.ts +0 -16
  258. package/lib/routes/channels/group.ts +0 -22
  259. package/lib/routes/channels/remove.help.ts +0 -3
  260. package/lib/routes/channels/remove.ts +0 -17
  261. package/lib/routes/channels/rename.help.ts +0 -5
  262. package/lib/routes/channels/rename.ts +0 -17
  263. package/lib/routes/channels/routes.ts +0 -19
  264. package/lib/routes/channels/show.help.ts +0 -1
  265. package/lib/routes/channels/show.ts +0 -26
  266. package/lib/routes/claude/claude.help.ts +0 -16
  267. package/lib/routes/claude/claude.ts +0 -76
  268. package/lib/routes/claude/routes.ts +0 -4
  269. package/lib/routes/connectors/add.help.ts +0 -28
  270. package/lib/routes/connectors/add.ts +0 -64
  271. package/lib/routes/connectors/group.help.ts +0 -14
  272. package/lib/routes/connectors/group.ts +0 -18
  273. package/lib/routes/connectors/remove.help.ts +0 -3
  274. package/lib/routes/connectors/remove.ts +0 -17
  275. package/lib/routes/connectors/rename.help.ts +0 -5
  276. package/lib/routes/connectors/rename.ts +0 -17
  277. package/lib/routes/connectors/routes.ts +0 -23
  278. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  279. package/lib/routes/connectors/schedules-add.ts +0 -33
  280. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  281. package/lib/routes/connectors/schedules-group.ts +0 -38
  282. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  283. package/lib/routes/connectors/schedules-remove.ts +0 -17
  284. package/lib/routes/connectors/set.help.ts +0 -8
  285. package/lib/routes/connectors/set.ts +0 -72
  286. package/lib/routes/connectors/show.help.ts +0 -1
  287. package/lib/routes/connectors/show.ts +0 -41
  288. package/lib/routes/gateway/group.help.ts +0 -15
  289. package/lib/routes/gateway/group.ts +0 -28
  290. package/lib/routes/gateway/logs.help.ts +0 -13
  291. package/lib/routes/gateway/logs.ts +0 -100
  292. package/lib/routes/gateway/restart.help.ts +0 -10
  293. package/lib/routes/gateway/routes.ts +0 -18
  294. package/lib/routes/gateway/run.help.ts +0 -12
  295. package/lib/routes/gateway/run.ts +0 -35
  296. package/lib/routes/gateway/start.help.ts +0 -15
  297. package/lib/routes/gateway/start.ts +0 -32
  298. package/lib/routes/gateway/status.help.ts +0 -9
  299. package/lib/routes/gateway/status.ts +0 -28
  300. package/lib/routes/gateway/stop.help.ts +0 -8
  301. package/lib/routes/gateway/stop.ts +0 -21
  302. package/lib/routes/profiles/add.help.ts +0 -3
  303. package/lib/routes/profiles/add.ts +0 -33
  304. package/lib/routes/profiles/group.help.ts +0 -16
  305. package/lib/routes/profiles/group.ts +0 -25
  306. package/lib/routes/profiles/launch.help.ts +0 -4
  307. package/lib/routes/profiles/launch.ts +0 -36
  308. package/lib/routes/profiles/remove.help.ts +0 -3
  309. package/lib/routes/profiles/remove.ts +0 -17
  310. package/lib/routes/profiles/rename.help.ts +0 -5
  311. package/lib/routes/profiles/rename.ts +0 -17
  312. package/lib/routes/profiles/routes.ts +0 -18
  313. package/lib/routes/profiles/set.help.ts +0 -5
  314. package/lib/routes/profiles/set.ts +0 -32
  315. package/lib/routes/repos/add.help.ts +0 -6
  316. package/lib/routes/repos/add.ts +0 -20
  317. package/lib/routes/repos/group.help.ts +0 -11
  318. package/lib/routes/repos/group.ts +0 -18
  319. package/lib/routes/repos/remove.help.ts +0 -3
  320. package/lib/routes/repos/remove.ts +0 -17
  321. package/lib/routes/repos/rename.help.ts +0 -5
  322. package/lib/routes/repos/rename.ts +0 -17
  323. package/lib/routes/repos/routes.ts +0 -17
  324. package/lib/routes/repos/set.help.ts +0 -5
  325. package/lib/routes/repos/set.ts +0 -21
  326. package/lib/routes/repos/show.help.ts +0 -1
  327. package/lib/routes/repos/show.ts +0 -19
  328. package/lib/routes/request/discord-help.ts +0 -9
  329. package/lib/routes/request/discord.help.ts +0 -19
  330. package/lib/routes/request/discord.ts +0 -65
  331. package/lib/routes/request/group.help.ts +0 -15
  332. package/lib/routes/request/group.ts +0 -9
  333. package/lib/routes/request/routes.ts +0 -14
  334. package/lib/routes/request/slack-help.ts +0 -9
  335. package/lib/routes/request/slack.help.ts +0 -19
  336. package/lib/routes/request/slack.ts +0 -61
  337. package/lib/routes/status/routes.ts +0 -4
  338. package/lib/routes/status/status.help.ts +0 -6
  339. package/lib/routes/status/status.ts +0 -77
  340. package/lib/routes/update/routes.ts +0 -4
  341. package/lib/routes/update/update.help.ts +0 -5
  342. package/lib/routes/update/update.ts +0 -21
  343. package/lib/routes.ts +0 -40
  344. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  345. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  346. /package/lib/{modules → cli}/router/validator.ts +0 -0
  347. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  348. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  349. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
@@ -0,0 +1,131 @@
1
+ import type { SelectOption } from "@opentui/core"
2
+ import { useKeyboard } from "@opentui/react"
3
+ import { useState } from "react"
4
+ import type { HasciiTheme } from "@/tui/utils/hascii/theme"
5
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
6
+ import { usePressable } from "@/tui/hooks/hascii/use-pressable"
7
+
8
+ export type Props = {
9
+ options?: SelectOption[]
10
+ width?: number
11
+ height?: number
12
+ defaultIndex?: number
13
+ focusedIndex?: number
14
+ isFocused?: boolean
15
+ onChange?: (index: number, option: SelectOption | null) => void
16
+ onSelect?: (index: number, option: SelectOption | null) => void
17
+ }
18
+
19
+ const pickItemBg = (
20
+ isActive: boolean,
21
+ hovered: boolean,
22
+ pressed: boolean,
23
+ theme: HasciiTheme,
24
+ ): string | undefined => {
25
+ if (pressed) return theme.color.secondaryActive
26
+ if (hovered && isActive) return theme.color.hoverActive
27
+ if (hovered) return theme.color.secondaryHover
28
+ if (isActive) return theme.color.secondaryActive
29
+ return undefined
30
+ }
31
+
32
+ type ItemProps = {
33
+ option: SelectOption
34
+ isActive: boolean
35
+ onPress: () => void
36
+ }
37
+
38
+ /** Internal row used by HasciiSelect. Tracks hover/press state and renders the active left bar. */
39
+ function HasciiSelectItem(props: ItemProps) {
40
+ const theme = useHasciiTheme()
41
+ const press = usePressable({ onPress: props.onPress })
42
+
43
+ const bg = pickItemBg(props.isActive, press.isHovered, press.isPressed, theme)
44
+
45
+ const nameColor =
46
+ props.isActive || press.isHovered ? theme.color.foreground : theme.color.mutedForeground
47
+ const descColor = theme.color.mutedForeground
48
+
49
+ const rowHeight = props.option.description ? 4 : 3
50
+
51
+ return (
52
+ <box flexDirection="row" backgroundColor={bg} {...press.bind}>
53
+ {props.isActive ? (
54
+ <box position="absolute" left={0} top={0} bottom={0} flexDirection="column">
55
+ {Array.from({ length: rowHeight }, (_, index) => (
56
+ <text key={index} fg={theme.color.primary}>
57
+
58
+ </text>
59
+ ))}
60
+ </box>
61
+ ) : null}
62
+ <box
63
+ flexGrow={1}
64
+ flexDirection="column"
65
+ paddingTop={1}
66
+ paddingBottom={1}
67
+ paddingLeft={2}
68
+ paddingRight={2}
69
+ gap={0}
70
+ >
71
+ <text fg={nameColor}>{props.option.name}</text>
72
+ {props.option.description ? <text fg={descColor}>{props.option.description}</text> : null}
73
+ </box>
74
+ </box>
75
+ )
76
+ }
77
+
78
+ /** Vertical option list with a muted background, a flush left bar on the active row, and vertical scrolling when items overflow. */
79
+ export function HasciiSelect(props: Props) {
80
+ const width = props.width ?? 36
81
+ const height = props.height ?? 16
82
+ const isFocused = props.isFocused ?? true
83
+ const options = props.options ?? []
84
+
85
+ const theme = useHasciiTheme()
86
+
87
+ const internalState = useState(props.defaultIndex ?? 0)
88
+ const internal = internalState[0]
89
+ const setInternal = internalState[1]
90
+
91
+ const current = props.focusedIndex ?? internal
92
+
93
+ const moveTo = (next: number) => {
94
+ if (options.length === 0) return
95
+
96
+ const clamped = Math.max(0, Math.min(options.length - 1, next))
97
+ if (props.focusedIndex === undefined) setInternal(clamped)
98
+ props.onChange?.(clamped, options[clamped] ?? null)
99
+ }
100
+
101
+ useKeyboard((key) => {
102
+ if (!isFocused || options.length === 0) return
103
+
104
+ if (key.name === "up") moveTo(current - 1)
105
+ if (key.name === "down") moveTo(current + 1)
106
+ if (key.name === "return" || key.name === "space") {
107
+ props.onSelect?.(current, options[current] ?? null)
108
+ }
109
+ })
110
+
111
+ return (
112
+ <box flexDirection="column" width={width} height={height} backgroundColor={theme.color.muted}>
113
+ <scrollbox
114
+ flexGrow={1}
115
+ focused={isFocused}
116
+ scrollY
117
+ stickyScroll={false}
118
+ contentOptions={{ flexDirection: "column", gap: 0 }}
119
+ >
120
+ {options.map((option, index) => (
121
+ <HasciiSelectItem
122
+ key={`${option.value}-${index}`}
123
+ option={option}
124
+ isActive={index === current}
125
+ onPress={() => moveTo(index)}
126
+ />
127
+ ))}
128
+ </scrollbox>
129
+ </box>
130
+ )
131
+ }
@@ -0,0 +1,35 @@
1
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
2
+
3
+ type Orientation = "horizontal" | "vertical"
4
+
5
+ export type Props = {
6
+ orientation?: Orientation
7
+ color?: string
8
+ length?: number
9
+ }
10
+
11
+ /** Hairline divider drawn with box-drawing characters. Length is the run in the chosen orientation. */
12
+ export function HasciiSeparator(props: Props) {
13
+ const orientation = props.orientation ?? "horizontal"
14
+ const length = props.length ?? (orientation === "horizontal" ? 32 : 8)
15
+
16
+ const theme = useHasciiTheme()
17
+ const color = props.color ?? theme.color.border
18
+
19
+ if (orientation === "vertical") {
20
+ const lines: string[] = []
21
+ for (let index = 0; index < length; index++) lines.push("│")
22
+
23
+ return (
24
+ <box width={1} height={length} flexShrink={0}>
25
+ <text fg={color}>{lines.join("\n")}</text>
26
+ </box>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <box width={length} height={1} flexShrink={0}>
32
+ <text fg={color}>{"─".repeat(length)}</text>
33
+ </box>
34
+ )
35
+ }
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export type Props = {
4
+ isFocused?: boolean
5
+ children?: ReactNode
6
+ }
7
+
8
+ /** Scrollable middle region of a HasciiSidebar. Wheel + arrow keys scroll vertically. */
9
+ export function HasciiSidebarContent(props: Props) {
10
+ const isFocused = props.isFocused ?? true
11
+
12
+ return (
13
+ <scrollbox
14
+ flexGrow={1}
15
+ focused={isFocused}
16
+ scrollY
17
+ stickyScroll={false}
18
+ contentOptions={{ flexDirection: "column", gap: 0 }}
19
+ >
20
+ {props.children}
21
+ </scrollbox>
22
+ )
23
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export type Props = {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /** Top section of a HasciiSidebar. Renders children at the sidebar's text padding. */
8
+ export function HasciiSidebarHeader(props: Props) {
9
+ return (
10
+ <box flexDirection="column" paddingLeft={2} paddingRight={2} paddingBottom={1} gap={0}>
11
+ {props.children}
12
+ </box>
13
+ )
14
+ }
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react"
2
+ import type { HasciiTheme } from "@/tui/utils/hascii/theme"
3
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
4
+ import { usePressable } from "@/tui/hooks/hascii/use-pressable"
5
+
6
+ export type Props = {
7
+ isActive?: boolean
8
+ isDisabled?: boolean
9
+ onPress?: () => void
10
+ children?: ReactNode
11
+ }
12
+
13
+ const pickBg = (
14
+ isDisabled: boolean,
15
+ isActive: boolean,
16
+ isHovered: boolean,
17
+ isPressed: boolean,
18
+ theme: HasciiTheme,
19
+ ): string | undefined => {
20
+ if (isDisabled) return undefined
21
+ if (isPressed) return theme.color.secondaryActive
22
+ if (isHovered && isActive) return theme.color.hoverActive
23
+ if (isHovered) return theme.color.secondaryHover
24
+ if (isActive) return theme.color.secondaryActive
25
+ return undefined
26
+ }
27
+
28
+ const ROW_HEIGHT = 3
29
+
30
+ /** Single pressable row inside HasciiSidebarContent. Active items show a thin left rule using ▏ glyphs. */
31
+ export function HasciiSidebarMenuItem(props: Props) {
32
+ const isActive = props.isActive ?? false
33
+ const isDisabled = props.isDisabled ?? false
34
+ const theme = useHasciiTheme()
35
+
36
+ const press = usePressable({ isDisabled, onPress: props.onPress })
37
+
38
+ const bg = pickBg(isDisabled, isActive, press.isHovered, press.isPressed, theme)
39
+
40
+ const fg = isDisabled
41
+ ? theme.color.mutedForeground
42
+ : isActive || press.isHovered
43
+ ? theme.color.foreground
44
+ : theme.color.mutedForeground
45
+
46
+ return (
47
+ <box
48
+ paddingTop={1}
49
+ paddingBottom={1}
50
+ paddingLeft={2}
51
+ paddingRight={2}
52
+ backgroundColor={bg}
53
+ {...press.bind}
54
+ >
55
+ {isActive ? (
56
+ <box position="absolute" left={0} top={0} bottom={0} flexDirection="column">
57
+ {Array.from({ length: ROW_HEIGHT }, (_, index) => (
58
+ <text key={index} fg={theme.color.primary}>
59
+
60
+ </text>
61
+ ))}
62
+ </box>
63
+ ) : null}
64
+ <text fg={fg}>{props.children}</text>
65
+ </box>
66
+ )
67
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+
4
+ export type Props = {
5
+ width?: number
6
+ children?: ReactNode
7
+ }
8
+
9
+ /** Fixed-width vertical sidebar. Compose with HasciiSidebarHeader, HasciiSidebarContent, HasciiSidebarMenuItem. */
10
+ export function HasciiSidebar(props: Props) {
11
+ const theme = useHasciiTheme()
12
+
13
+ return (
14
+ <box
15
+ flexDirection="column"
16
+ width={props.width ?? 24}
17
+ backgroundColor={theme.color.muted}
18
+ paddingTop={1}
19
+ gap={0}
20
+ >
21
+ {props.children}
22
+ </box>
23
+ )
24
+ }
@@ -0,0 +1,60 @@
1
+ import { useEffect, useState } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+
4
+ export type Props = {
5
+ width?: number
6
+ height?: number
7
+ intervalMs?: number
8
+ cycleMs?: number
9
+ }
10
+
11
+ const lerpChannel = (a: number, b: number, t: number): number => Math.round(a + (b - a) * t)
12
+
13
+ const parseHex = (hex: string): [number, number, number] => {
14
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex
15
+ const r = parseInt(clean.slice(0, 2), 16)
16
+ const g = parseInt(clean.slice(2, 4), 16)
17
+ const b = parseInt(clean.slice(4, 6), 16)
18
+ return [r, g, b]
19
+ }
20
+
21
+ const toHex = (channel: number): string => channel.toString(16).padStart(2, "0")
22
+
23
+ const lerpHex = (a: string, b: string, t: number): string => {
24
+ const colorA = parseHex(a)
25
+ const colorB = parseHex(b)
26
+ const r = lerpChannel(colorA[0], colorB[0], t)
27
+ const g = lerpChannel(colorA[1], colorB[1], t)
28
+ const blue = lerpChannel(colorA[2], colorB[2], t)
29
+ return `#${toHex(r)}${toHex(g)}${toHex(blue)}`
30
+ }
31
+
32
+ /** Placeholder block that pulses smoothly between two muted shades using cosine easing. */
33
+ export function HasciiSkeleton(props: Props) {
34
+ const intervalMs = props.intervalMs ?? 60
35
+ const cycleMs = props.cycleMs ?? 1800
36
+ const theme = useHasciiTheme()
37
+
38
+ const startState = useState<number>(performance.now())
39
+ const start = startState[0]
40
+
41
+ const elapsedState = useState(0)
42
+ const elapsed = elapsedState[0]
43
+ const setElapsed = elapsedState[1]
44
+
45
+ // useEffect drives the pulse — necessary for time-based color interpolation.
46
+ useEffect(() => {
47
+ const id = setInterval(() => {
48
+ setElapsed(performance.now() - start)
49
+ }, intervalMs)
50
+
51
+ return () => clearInterval(id)
52
+ }, [intervalMs, start, setElapsed])
53
+
54
+ const phase = ((elapsed % cycleMs) / cycleMs) * 2 * Math.PI
55
+ const t = (1 - Math.cos(phase)) / 2
56
+
57
+ const bg = lerpHex(theme.color.muted, theme.color.secondaryActive, t)
58
+
59
+ return <box width={props.width} height={props.height ?? 1} backgroundColor={bg} flexShrink={0} />
60
+ }
@@ -0,0 +1,91 @@
1
+ import { RGBA, SliderRenderable } from "@opentui/core"
2
+ import { extend } from "@opentui/react"
3
+ import { useState } from "react"
4
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
5
+
6
+ declare module "@opentui/react" {
7
+ interface OpenTUIComponents {
8
+ slider: typeof SliderRenderable
9
+ }
10
+ }
11
+
12
+ extend({ slider: SliderRenderable })
13
+
14
+ export type Props = {
15
+ value?: number
16
+ defaultValue?: number
17
+ min?: number
18
+ max?: number
19
+ width?: number
20
+ thumbSize?: number
21
+ onChange?: (next: number) => void
22
+ }
23
+
24
+ const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0)
25
+
26
+ const viewPortSizeFor = (thumbCells: number, range: number, width: number): number => {
27
+ const virtualThumb = thumbCells * 2
28
+ const denominator = width * 2 - virtualThumb
29
+
30
+ if (denominator <= 0) return range
31
+
32
+ return Math.max(1, Math.round((virtualThumb * range) / denominator))
33
+ }
34
+
35
+ /** Horizontal slider backed by OpenTUI's SliderRenderable. A ─ track sits behind a thumbSize-cell thumb that supports native click + drag. */
36
+ export function HasciiSlider(props: Props) {
37
+ const min = props.min ?? 0
38
+ const max = props.max ?? 100
39
+ const width = props.width ?? 32
40
+ const thumbSize = props.thumbSize ?? 3
41
+
42
+ const theme = useHasciiTheme()
43
+
44
+ const internalState = useState(props.defaultValue ?? min)
45
+ const internal = internalState[0]
46
+ const setInternal = internalState[1]
47
+
48
+ const value = props.value ?? internal
49
+
50
+ const onChange = (next: number) => {
51
+ if (props.value === undefined) setInternal(next)
52
+ props.onChange?.(next)
53
+ }
54
+
55
+ const hoveredState = useState(false)
56
+ const isHovered = hoveredState[0]
57
+ const setHovered = hoveredState[1]
58
+
59
+ const range = Math.max(1, max - min)
60
+ const viewPortSize = viewPortSizeFor(thumbSize, range, width)
61
+
62
+ const thumbFg = isHovered ? theme.color.primaryHover : theme.color.primary
63
+
64
+ return (
65
+ <box
66
+ width={width}
67
+ height={1}
68
+ onMouseOver={() => setHovered(true)}
69
+ onMouseOut={() => setHovered(false)}
70
+ >
71
+ <box position="absolute" left={0} top={0}>
72
+ <text fg={theme.color.border}>{"─".repeat(width)}</text>
73
+ </box>
74
+ <slider
75
+ position="absolute"
76
+ left={0}
77
+ top={0}
78
+ orientation="horizontal"
79
+ width={width}
80
+ height={1}
81
+ min={min}
82
+ max={max}
83
+ value={value}
84
+ viewPortSize={viewPortSize}
85
+ foregroundColor={thumbFg}
86
+ backgroundColor={TRANSPARENT}
87
+ onChange={onChange}
88
+ />
89
+ </box>
90
+ )
91
+ }
@@ -0,0 +1,75 @@
1
+ import { useEffect, useState } from "react"
2
+ import type { ReactNode } from "react"
3
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
4
+
5
+ type Variant = "default" | "secondary" | "destructive"
6
+
7
+ export type Props = {
8
+ variant?: Variant
9
+ width?: number
10
+ slideMs?: number
11
+ isOpen?: boolean
12
+ children?: ReactNode
13
+ }
14
+
15
+ /** Toast-like overlay that slides in from the right edge. Render inside an end-aligned column to anchor bottom-right. */
16
+ export function HasciiSnackbar(props: Props) {
17
+ const variant = props.variant ?? "default"
18
+ const width = props.width ?? 28
19
+ const slideMs = props.slideMs ?? 90
20
+ const isOpen = props.isOpen ?? true
21
+
22
+ const theme = useHasciiTheme()
23
+
24
+ const offsetState = useState(width)
25
+ const offset = offsetState[0]
26
+ const setOffset = offsetState[1]
27
+
28
+ // useEffect drives the slide animation by stepping marginRight from `width` to 0 (or back).
29
+ useEffect(() => {
30
+ const target = isOpen ? 0 : width
31
+ if (offset === target) return
32
+
33
+ const start = performance.now()
34
+ const from = offset
35
+ let frame = 0
36
+
37
+ const tick = () => {
38
+ const progress = Math.min(1, (performance.now() - start) / slideMs)
39
+ const next = Math.round(from + (target - from) * progress)
40
+ setOffset(next)
41
+
42
+ if (progress < 1) {
43
+ frame = setTimeout(tick, 16) as unknown as number
44
+ }
45
+ }
46
+
47
+ tick()
48
+
49
+ return () => clearTimeout(frame)
50
+ }, [isOpen, width, slideMs, offset, setOffset])
51
+
52
+ const palette = {
53
+ default: { bg: theme.color.primary, fg: theme.color.primaryForeground },
54
+ secondary: { bg: theme.color.secondary, fg: theme.color.secondaryForeground },
55
+ destructive: {
56
+ bg: theme.color.destructive,
57
+ fg: theme.color.destructiveForeground,
58
+ },
59
+ }[variant]
60
+
61
+ return (
62
+ <box
63
+ flexDirection="row"
64
+ width={width}
65
+ paddingTop={1}
66
+ paddingBottom={1}
67
+ paddingLeft={2}
68
+ paddingRight={2}
69
+ marginRight={-offset}
70
+ backgroundColor={palette.bg}
71
+ >
72
+ <text fg={palette.fg}>{props.children}</text>
73
+ </box>
74
+ )
75
+ }
@@ -0,0 +1,53 @@
1
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
2
+
3
+ export type Props = {
4
+ values: number[]
5
+ width?: number
6
+ color?: string
7
+ }
8
+
9
+ const BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const
10
+
11
+ /** Single-row Unicode bar chart. Maps each value to one of eight block heights. */
12
+ export function HasciiSparkline(props: Props) {
13
+ const theme = useHasciiTheme()
14
+ const color = props.color ?? theme.color.primary
15
+
16
+ const samples = props.width !== undefined ? takeSamples(props.values, props.width) : props.values
17
+
18
+ if (samples.length === 0) {
19
+ return <text fg={color}> </text>
20
+ }
21
+
22
+ let min = Infinity
23
+ let max = -Infinity
24
+
25
+ for (const value of samples) {
26
+ if (value < min) min = value
27
+ if (value > max) max = value
28
+ }
29
+
30
+ const range = max - min || 1
31
+
32
+ const glyphs = samples.map((value) => {
33
+ const ratio = (value - min) / range
34
+ const index = Math.min(BARS.length - 1, Math.round(ratio * (BARS.length - 1)))
35
+ return BARS[index]
36
+ })
37
+
38
+ return <text fg={color}>{glyphs.join("")}</text>
39
+ }
40
+
41
+ const takeSamples = (values: number[], targetWidth: number): number[] => {
42
+ if (values.length <= targetWidth) return values
43
+
44
+ const samples: number[] = []
45
+ const stride = values.length / targetWidth
46
+
47
+ for (let index = 0; index < targetWidth; index++) {
48
+ const sourceIndex = Math.min(values.length - 1, Math.floor(index * stride))
49
+ samples.push(values[sourceIndex] ?? 0)
50
+ }
51
+
52
+ return samples
53
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from "react"
2
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
3
+
4
+ export const SPINNER_KINDS = {
5
+ braille: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
6
+ dots: ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"],
7
+ line: ["|", "/", "-", "\\"],
8
+ noise: ["▓", "▒", "░", "▒"],
9
+ pipe: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
10
+ block: ["▌", "▀", "▐", "▄"],
11
+ growVert: ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"],
12
+ toggle: ["▢", "▣", "▤", "▥", "▦", "▧", "▨", "▩"],
13
+ } as const satisfies Record<string, readonly string[]>
14
+
15
+ export type SpinnerKind = keyof typeof SPINNER_KINDS
16
+
17
+ export type Props = {
18
+ variant?: SpinnerKind
19
+ intervalMs?: number
20
+ color?: string
21
+ }
22
+
23
+ /** Animated single-cell spinner. variant chooses the glyph cycle, intervalMs the cadence. */
24
+ export function HasciiSpinner(props: Props) {
25
+ const variant: SpinnerKind = props.variant ?? "braille"
26
+ const intervalMs = props.intervalMs ?? 80
27
+
28
+ const theme = useHasciiTheme()
29
+ const color = props.color ?? theme.color.foreground
30
+
31
+ const frames = SPINNER_KINDS[variant]
32
+
33
+ const frameState = useState(0)
34
+ const frame = frameState[0]
35
+ const setFrame = frameState[1]
36
+
37
+ // useEffect is necessary for time-based frame advancement in a TUI loop.
38
+ useEffect(() => {
39
+ const id = setInterval(() => {
40
+ setFrame((current) => (current + 1) % frames.length)
41
+ }, intervalMs)
42
+
43
+ return () => clearInterval(id)
44
+ }, [frames, intervalMs, setFrame])
45
+
46
+ return <text fg={color}>{frames[frame]}</text>
47
+ }
@@ -0,0 +1,54 @@
1
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
2
+
3
+ export type StepperItem = {
4
+ label: string
5
+ }
6
+
7
+ export type Props = {
8
+ steps: StepperItem[]
9
+ current: number
10
+ }
11
+
12
+ /** Horizontal multi-step indicator. Past steps show ■, current shows ▣, future shows □. */
13
+ export function HasciiStepper(props: Props) {
14
+ const theme = useHasciiTheme()
15
+
16
+ const items: import("react").ReactNode[] = []
17
+
18
+ for (let index = 0; index < props.steps.length; index++) {
19
+ const step = props.steps[index]
20
+ if (step === undefined) continue
21
+
22
+ const isPast = index < props.current
23
+ const isCurrent = index === props.current
24
+
25
+ const markerFg = isPast || isCurrent ? theme.color.primary : theme.color.mutedForeground
26
+ const labelFg = isCurrent ? theme.color.foreground : theme.color.mutedForeground
27
+ const marker = isPast ? "■" : isCurrent ? "▣" : "□"
28
+
29
+ items.push(
30
+ <box key={`step-${index}`} flexDirection="row" alignItems="center">
31
+ <text fg={markerFg}>{marker}</text>
32
+ <box paddingLeft={2}>
33
+ <text fg={labelFg}>{step.label}</text>
34
+ </box>
35
+ </box>,
36
+ )
37
+
38
+ if (index < props.steps.length - 1) {
39
+ const lineFg = isPast ? theme.color.primary : theme.color.mutedForeground
40
+
41
+ items.push(
42
+ <box key={`line-${index}`} paddingLeft={1} paddingRight={1}>
43
+ <text fg={lineFg}>──</text>
44
+ </box>,
45
+ )
46
+ }
47
+ }
48
+
49
+ return (
50
+ <box flexDirection="row" alignItems="center">
51
+ {items}
52
+ </box>
53
+ )
54
+ }