@sirenodeck/sirenodeck 0.1.1-alpha.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 (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/bin/_wrapper-shared.js +42 -0
  4. package/bin/dev.js +95 -0
  5. package/bin/sirenodeck.js +42 -0
  6. package/config/default-config.yml +117 -0
  7. package/config/starter-config.yml +54 -0
  8. package/dist/main.mjs +64702 -0
  9. package/package.json +140 -0
  10. package/src/action/executor.ts +156 -0
  11. package/src/action/index.ts +7 -0
  12. package/src/addon/api-types.ts +15 -0
  13. package/src/addon/api.ts +461 -0
  14. package/src/addon/check-runner.ts +84 -0
  15. package/src/addon/index.ts +5 -0
  16. package/src/addon/loader.ts +441 -0
  17. package/src/addon/registry.ts +230 -0
  18. package/src/addon/spec.ts +69 -0
  19. package/src/api/addon.ts +33 -0
  20. package/src/api/asset-url.ts +24 -0
  21. package/src/api/index.ts +50 -0
  22. package/src/api/protocol-internal.ts +656 -0
  23. package/src/api/protocol.ts +2 -0
  24. package/src/api/react/index.ts +11 -0
  25. package/src/api/react/registry.ts +91 -0
  26. package/src/api/react/use-addon-channel.ts +26 -0
  27. package/src/api/react/use-button-state.tsx +23 -0
  28. package/src/api/react/use-deck.ts +24 -0
  29. package/src/assets/logo.png +0 -0
  30. package/src/assets/logo72x72.png +0 -0
  31. package/src/assets/logoFull.png +0 -0
  32. package/src/builtin-addons/brightness/README.md +51 -0
  33. package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
  34. package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
  35. package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
  36. package/src/builtin-addons/brightness/domain/macos.ts +46 -0
  37. package/src/builtin-addons/brightness/index.d.ts +5 -0
  38. package/src/builtin-addons/brightness/index.ts +20 -0
  39. package/src/builtin-addons/brightness/poller.ts +27 -0
  40. package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
  41. package/src/builtin-addons/brightness/providers/index.ts +81 -0
  42. package/src/builtin-addons/brightness/providers/linux.ts +108 -0
  43. package/src/builtin-addons/brightness/providers/windows.ts +77 -0
  44. package/src/builtin-addons/brightness/sirenodeck.json +6 -0
  45. package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
  46. package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
  47. package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
  48. package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
  49. package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
  50. package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
  51. package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
  52. package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
  53. package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
  54. package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
  55. package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
  56. package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
  57. package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
  58. package/src/builtin-addons/coding-agents/index.ts +15 -0
  59. package/src/builtin-addons/coding-agents/manifest.ts +115 -0
  60. package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
  61. package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
  62. package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
  63. package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
  64. package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
  65. package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
  66. package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
  67. package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
  68. package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
  69. package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
  70. package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
  71. package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
  72. package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
  73. package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
  74. package/src/builtin-addons/coding-agents/types/types.ts +33 -0
  75. package/src/builtin-addons/core/README.md +89 -0
  76. package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
  77. package/src/builtin-addons/core/buttons/action/config.ts +13 -0
  78. package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
  79. package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
  80. package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
  81. package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
  82. package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
  83. package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
  84. package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
  85. package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
  86. package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
  87. package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
  88. package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
  89. package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
  90. package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
  91. package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
  92. package/src/builtin-addons/core/index.ts +120 -0
  93. package/src/builtin-addons/core/sirenodeck.json +6 -0
  94. package/src/builtin-addons/date-time/README.md +73 -0
  95. package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
  96. package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
  97. package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
  98. package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
  99. package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
  100. package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
  101. package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
  102. package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
  103. package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
  104. package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
  105. package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
  106. package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
  107. package/src/builtin-addons/date-time/index.d.ts +5 -0
  108. package/src/builtin-addons/date-time/index.ts +30 -0
  109. package/src/builtin-addons/date-time/poller.ts +11 -0
  110. package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
  111. package/src/builtin-addons/date-time/shared/format.ts +105 -0
  112. package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
  113. package/src/builtin-addons/date-time/sirenodeck.json +6 -0
  114. package/src/builtin-addons/emoji-selector/README.md +45 -0
  115. package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
  116. package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
  117. package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
  118. package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
  119. package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
  120. package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
  121. package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
  122. package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
  123. package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
  124. package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
  125. package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
  126. package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
  127. package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
  128. package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
  129. package/src/builtin-addons/emoji-selector/index.ts +29 -0
  130. package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
  131. package/src/builtin-addons/emoji-selector/support.ts +49 -0
  132. package/src/builtin-addons/index.ts +13 -0
  133. package/src/builtin-addons/internal-settings/README.md +26 -0
  134. package/src/builtin-addons/internal-settings/backend.ts +58 -0
  135. package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
  136. package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
  137. package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
  138. package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
  139. package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
  140. package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
  141. package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
  142. package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
  143. package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
  144. package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
  145. package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
  146. package/src/builtin-addons/internal-settings/index.d.ts +4 -0
  147. package/src/builtin-addons/internal-settings/index.ts +53 -0
  148. package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
  149. package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
  150. package/src/builtin-addons/media/README.md +45 -0
  151. package/src/builtin-addons/media/backend.ts +143 -0
  152. package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
  153. package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
  154. package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
  155. package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
  156. package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
  157. package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
  158. package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
  159. package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
  160. package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
  161. package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
  162. package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
  163. package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
  164. package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
  165. package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
  166. package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
  167. package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
  168. package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
  169. package/src/builtin-addons/media/checks.ts +104 -0
  170. package/src/builtin-addons/media/executor.ts +36 -0
  171. package/src/builtin-addons/media/index.d.ts +5 -0
  172. package/src/builtin-addons/media/index.ts +53 -0
  173. package/src/builtin-addons/media/progress.ts +16 -0
  174. package/src/builtin-addons/media/providers/darwin.ts +391 -0
  175. package/src/builtin-addons/media/providers/index.ts +27 -0
  176. package/src/builtin-addons/media/providers/linux.ts +196 -0
  177. package/src/builtin-addons/media/providers/types.ts +48 -0
  178. package/src/builtin-addons/media/providers/windows.ts +121 -0
  179. package/src/builtin-addons/media/sirenodeck.json +6 -0
  180. package/src/builtin-addons/media/state.ts +19 -0
  181. package/src/builtin-addons/register-builtins.ts +65 -0
  182. package/src/builtin-addons/session/README.md +14 -0
  183. package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
  184. package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
  185. package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
  186. package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
  187. package/src/builtin-addons/session/buttons/time/config.ts +6 -0
  188. package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
  189. package/src/builtin-addons/session/index.d.ts +5 -0
  190. package/src/builtin-addons/session/index.ts +18 -0
  191. package/src/builtin-addons/session/sirenodeck.json +6 -0
  192. package/src/builtin-addons/system-status/README.md +169 -0
  193. package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
  194. package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
  195. package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
  196. package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
  197. package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
  198. package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
  199. package/src/builtin-addons/system-status/domain/index.ts +10 -0
  200. package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
  201. package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
  202. package/src/builtin-addons/system-status/index.ts +16 -0
  203. package/src/builtin-addons/system-status/manifest.ts +105 -0
  204. package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
  205. package/src/builtin-addons/system-status/sirenodeck.json +6 -0
  206. package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
  207. package/src/builtin-addons/test-buildin/index.d.ts +5 -0
  208. package/src/builtin-addons/test-buildin/index.ts +16 -0
  209. package/src/builtin-addons/value-display/README.md +59 -0
  210. package/src/builtin-addons/value-display/backend.ts +86 -0
  211. package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
  212. package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
  213. package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
  214. package/src/builtin-addons/value-display/index.d.ts +5 -0
  215. package/src/builtin-addons/value-display/index.ts +23 -0
  216. package/src/builtin-addons/value-display/sirenodeck.json +6 -0
  217. package/src/builtin-addons/weather/README.md +46 -0
  218. package/src/builtin-addons/weather/backend.ts +121 -0
  219. package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
  220. package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
  221. package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
  222. package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
  223. package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
  224. package/src/builtin-addons/weather/domain/codes.ts +35 -0
  225. package/src/builtin-addons/weather/domain/fetch.ts +47 -0
  226. package/src/builtin-addons/weather/index.d.ts +5 -0
  227. package/src/builtin-addons/weather/index.ts +23 -0
  228. package/src/builtin-addons/weather/poller.ts +70 -0
  229. package/src/builtin-addons/weather/provider/city-key.ts +9 -0
  230. package/src/builtin-addons/weather/provider/codes.ts +35 -0
  231. package/src/builtin-addons/weather/provider/fetch.ts +97 -0
  232. package/src/builtin-addons/weather/provider/geocode.ts +67 -0
  233. package/src/builtin-addons/weather/provider/types.ts +42 -0
  234. package/src/builtin-addons/weather/sirenodeck.json +6 -0
  235. package/src/cli/coding-agents-onboarding.ts +434 -0
  236. package/src/cli/commands/addon-decks.ts +441 -0
  237. package/src/cli/commands/addon-registry.ts +639 -0
  238. package/src/cli/commands/emulator-mode.ts +289 -0
  239. package/src/cli/commands/install.ts +128 -0
  240. package/src/cli/commands/logs.ts +74 -0
  241. package/src/cli/commands/network-bind.ts +111 -0
  242. package/src/cli/commands/pipeline/helpers.ts +75 -0
  243. package/src/cli/commands/pipeline/preflight.ts +20 -0
  244. package/src/cli/commands/port-identity.ts +152 -0
  245. package/src/cli/commands/reload.ts +37 -0
  246. package/src/cli/commands/restart.ts +139 -0
  247. package/src/cli/commands/run.ts +2270 -0
  248. package/src/cli/commands/service/install.ts +121 -0
  249. package/src/cli/commands/service/render-template.ts +158 -0
  250. package/src/cli/commands/service-manager.ts +184 -0
  251. package/src/cli/commands/spawn-daemon.ts +251 -0
  252. package/src/cli/commands/start.ts +1061 -0
  253. package/src/cli/commands/status.ts +110 -0
  254. package/src/cli/commands/stop.ts +121 -0
  255. package/src/cli/commands/subprocess-supervisor.ts +203 -0
  256. package/src/cli/commands/system-requirements.ts +699 -0
  257. package/src/cli/cwd.ts +28 -0
  258. package/src/cli/first-run.ts +149 -0
  259. package/src/cli/http-server.ts +244 -0
  260. package/src/cli/index.ts +355 -0
  261. package/src/cli/install-root.ts +26 -0
  262. package/src/cli/main.ts +194 -0
  263. package/src/cli/package-manager.ts +175 -0
  264. package/src/cli/probe-deps.ts +45 -0
  265. package/src/cli/prompt.ts +22 -0
  266. package/src/cli/startup-display.ts +452 -0
  267. package/src/config/builtin-icons.ts +57 -0
  268. package/src/config/config-diff.ts +54 -0
  269. package/src/config/discovery.ts +64 -0
  270. package/src/config/icon-resolver.ts +90 -0
  271. package/src/config/include-resolver.ts +202 -0
  272. package/src/config/index.ts +9 -0
  273. package/src/config/loader.ts +187 -0
  274. package/src/config/mutation.ts +514 -0
  275. package/src/config/reference-expander.ts +84 -0
  276. package/src/config/schemas.ts +223 -0
  277. package/src/config/validation.ts +254 -0
  278. package/src/core/asset-id.ts +21 -0
  279. package/src/core/gesture-state.ts +344 -0
  280. package/src/core/icon-asset-registry.ts +127 -0
  281. package/src/core/icon-source.ts +86 -0
  282. package/src/core/index.ts +30 -0
  283. package/src/core/mime.ts +16 -0
  284. package/src/core/pagination.ts +90 -0
  285. package/src/core/pub-sub.ts +126 -0
  286. package/src/core/settings.ts +153 -0
  287. package/src/core/store.ts +88 -0
  288. package/src/core/watcher.ts +73 -0
  289. package/src/deck/addon-handler-bridge.ts +516 -0
  290. package/src/deck/deck-config.ts +271 -0
  291. package/src/deck/editor-surfaces.ts +87 -0
  292. package/src/deck/host-context.ts +18 -0
  293. package/src/deck/index.ts +102 -0
  294. package/src/deck/macro-parse.ts +182 -0
  295. package/src/deck/methods.ts +348 -0
  296. package/src/deck/paginate-deck.ts +98 -0
  297. package/src/deck/position-buttons.ts +57 -0
  298. package/src/deck/runtime/index.ts +5 -0
  299. package/src/deck/runtime/runtime.ts +1060 -0
  300. package/src/deck/runtime-subscriptions.ts +45 -0
  301. package/src/deck/system-back-injection.ts +62 -0
  302. package/src/deck/system-buttons/registry.tsx +97 -0
  303. package/src/deck/system-buttons/types.ts +14 -0
  304. package/src/device/black-frame.ts +73 -0
  305. package/src/device/index.ts +13 -0
  306. package/src/device/linux-udev.ts +36 -0
  307. package/src/device/models.ts +89 -0
  308. package/src/device/reconnecting.ts +193 -0
  309. package/src/device/registry.ts +38 -0
  310. package/src/device/stream-deck.ts +155 -0
  311. package/src/index.ts +74 -0
  312. package/src/outputClient/emulator.ts +357 -0
  313. package/src/outputClient/index.ts +26 -0
  314. package/src/outputClient/open-browser.ts +26 -0
  315. package/src/outputClient/real.ts +393 -0
  316. package/src/outputClient/types.ts +83 -0
  317. package/src/render/browser-renderer.ts +240 -0
  318. package/src/render/buffer-hash.ts +20 -0
  319. package/src/render/editor-handler.ts +197 -0
  320. package/src/render/emulator-server.ts +128 -0
  321. package/src/render/icon-source-resolver.ts +69 -0
  322. package/src/render/index.ts +16 -0
  323. package/src/render/protocol.ts +61 -0
  324. package/src/render/push-raw-image.ts +100 -0
  325. package/src/render/screenshot-cadence.ts +87 -0
  326. package/src/render/state-publisher.ts +110 -0
  327. package/src/render/vite-server.ts +138 -0
  328. package/src/render/ws-bridge.ts +315 -0
  329. package/src/system/device-selection.ts +80 -0
  330. package/src/system/glob-match.ts +74 -0
  331. package/src/system/providers/active-app/darwin.ts +117 -0
  332. package/src/system/providers/active-app/linux.ts +245 -0
  333. package/src/system/providers/active-app/wayland-gnome.ts +303 -0
  334. package/src/system/providers/active-app/windows.ts +94 -0
  335. package/src/system/providers/active-app.ts +97 -0
  336. package/src/system/providers/clipboard/darwin.ts +65 -0
  337. package/src/system/providers/clipboard/linux.ts +163 -0
  338. package/src/system/providers/clipboard/windows.ts +94 -0
  339. package/src/system/providers/clipboard.ts +84 -0
  340. package/src/system/providers/error.ts +16 -0
  341. package/src/system/providers/key-macro/darwin.ts +221 -0
  342. package/src/system/providers/key-macro/linux.ts +651 -0
  343. package/src/system/providers/key-macro/parser.ts +215 -0
  344. package/src/system/providers/key-macro/windows.ts +476 -0
  345. package/src/system/providers/key-macro.ts +79 -0
  346. package/src/system/providers/notification/darwin.ts +36 -0
  347. package/src/system/providers/notification/linux.ts +115 -0
  348. package/src/system/providers/notification/windows.ts +72 -0
  349. package/src/system/providers/notification.ts +75 -0
  350. package/src/system/providers/session/darwin.ts +82 -0
  351. package/src/system/providers/session/linux.ts +175 -0
  352. package/src/system/providers/session/windows.ts +73 -0
  353. package/src/system/providers/session.ts +89 -0
  354. package/src/system/providers/shared.ts +65 -0
  355. package/src/system/requirements.ts +235 -0
  356. package/src/system/setup-wizard/config-seed.ts +30 -0
  357. package/src/system/setup-wizard/format.ts +275 -0
  358. package/src/system/setup-wizard/index.ts +45 -0
  359. package/src/system/setup-wizard/open-settings.ts +26 -0
  360. package/src/system/setup-wizard/plan.ts +230 -0
  361. package/src/system/setup-wizard/probe-cache.ts +34 -0
  362. package/src/system/setup-wizard/probe.ts +486 -0
  363. package/src/system/setup-wizard/runtime-features.ts +85 -0
  364. package/src/system/setup-wizard/sudo.ts +178 -0
  365. package/src/system/setup-wizard/types.ts +133 -0
  366. package/src/system/virtual-stream-deck.ts +67 -0
  367. package/src/themes/css.ts +210 -0
  368. package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  369. package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  370. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  371. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
  372. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
  373. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
  374. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
  375. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
  376. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
  377. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  378. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
  379. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  380. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
  381. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
  382. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
  383. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
  384. package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
  385. package/src/themes/default/assets/OFL.txt +93 -0
  386. package/src/themes/default/assets/README.txt +106 -0
  387. package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
  388. package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
  389. package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
  390. package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
  391. package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
  392. package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
  393. package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  394. package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
  395. package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
  396. package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
  397. package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
  398. package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
  399. package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
  400. package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
  401. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
  402. package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
  403. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
  404. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
  405. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
  406. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
  407. package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
  408. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
  409. package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
  410. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
  411. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
  412. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
  413. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
  414. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
  415. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
  416. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
  417. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
  418. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
  419. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
  420. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
  421. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
  422. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
  423. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
  424. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
  425. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
  426. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  427. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
  428. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
  429. package/src/themes/default/components.css +81 -0
  430. package/src/themes/default/sirenodeck.json +170 -0
  431. package/src/themes/index.ts +14 -0
  432. package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  433. package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  434. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  435. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  436. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  437. package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
  438. package/src/themes/light/assets/OFL.txt +93 -0
  439. package/src/themes/light/assets/README.txt +106 -0
  440. package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
  441. package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  442. package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
  443. package/src/themes/light/components.css +70 -0
  444. package/src/themes/light/sirenodeck.json +170 -0
  445. package/src/themes/loader.ts +343 -0
  446. package/src/themes/manifest.ts +164 -0
  447. package/src/themes/use-resolved-theme.tsx +32 -0
  448. package/src/ui/ButtonFrame.tsx +230 -0
  449. package/src/ui/contexts/AssetCacheContext.tsx +43 -0
  450. package/src/ui/index.ts +20 -0
  451. package/src/ui/primitives/Chip.tsx +53 -0
  452. package/src/ui/primitives/Icon.tsx +209 -0
  453. package/src/ui/primitives/Label.tsx +82 -0
  454. package/src/ui/primitives/ProgressBar.tsx +38 -0
  455. package/src/ui/primitives/TapIndicator.tsx +67 -0
  456. package/src/ui/primitives/Text.tsx +810 -0
  457. package/src/ui/primitives/index.ts +6 -0
  458. package/src/ui/surfaces/BarsSurface.tsx +211 -0
  459. package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
  460. package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
  461. package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
  462. package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
  463. package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
  464. package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
  465. package/src/ui/surfaces/ValueChart.tsx +218 -0
  466. package/src/ui/surfaces/index.ts +32 -0
  467. package/src/ui/surfaces/paginated-schema.ts +22 -0
  468. package/src/ui/theme-presentation-builder.ts +184 -0
  469. package/src/ui/theme-presentation.tsx +146 -0
  470. package/src/ui/utils/cn.ts +12 -0
  471. package/src/util/cache-paths.ts +38 -0
  472. package/src/util/config-diff.ts +16 -0
  473. package/src/util/daemon-control.ts +85 -0
  474. package/src/util/daemon.ts +575 -0
  475. package/src/util/device-config.ts +56 -0
  476. package/src/util/errors.ts +13 -0
  477. package/src/util/log-reader.ts +114 -0
  478. package/src/util/log-tail.ts +103 -0
  479. package/src/util/logger.ts +417 -0
  480. package/src/util/process-title.ts +79 -0
  481. package/src/util/single-instance.ts +202 -0
  482. package/src/version.ts +10 -0
  483. package/src/vite/index.ts +6 -0
  484. package/src/vite/virtual-modules.ts +631 -0
  485. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,240 @@
1
+ import sharp from "sharp"
2
+
3
+ import { BUTTON_SIZE_PX, gridForKeyCount } from "@/device/models"
4
+ import type { PubSub } from "@/core/pub-sub"
5
+ import { createPubSub } from "@/core/pub-sub"
6
+ import type pino from "pino"
7
+
8
+ import { BufferChangeTracker } from "./buffer-hash"
9
+ import { CadenceTimer, EventDebouncer } from "./screenshot-cadence"
10
+
11
+ export interface PlaywrightLike {
12
+ chromium: {
13
+ launch(opts?: { headless?: boolean; args?: string[] }): Promise<BrowserLike>
14
+ }
15
+ }
16
+ export interface BrowserLike {
17
+ newContext(opts?: {
18
+ viewport?: { width: number; height: number }
19
+ }): Promise<ContextLike>
20
+ close(): Promise<void>
21
+ }
22
+ export interface ContextLike {
23
+ newPage(): Promise<PageLike>
24
+ close(): Promise<void>
25
+ }
26
+ export interface PageLike {
27
+ goto(url: string, opts?: { waitUntil?: string }): Promise<unknown>
28
+ screenshot(opts: { type: "png" }): Promise<Buffer>
29
+ close(): Promise<void>
30
+ route(url: string, handler: (route: RouteLike) => void): void
31
+ }
32
+
33
+ export interface RouteLike {
34
+ request(): { url(): string }
35
+ continue(opts: { url: string }): void
36
+ }
37
+
38
+ export interface DeviceSink {
39
+ getKeyCount(): number
40
+ fillKeyBuffer(keyIndex: number, buffer: Buffer): Promise<void>
41
+ }
42
+
43
+ export interface BrowserRendererOptions {
44
+ readonly frontendUrl: string
45
+ readonly device: DeviceSink
46
+ readonly logger: pino.Logger
47
+ readonly pubSub?: PubSub
48
+ readonly intervalMs?: number
49
+ readonly eventDebounceMs?: number
50
+ readonly playwrightFactory?: () => PlaywrightLike | Promise<PlaywrightLike>
51
+ }
52
+
53
+ const DEFAULT_INTERVAL_MS = 500
54
+ const DEFAULT_EVENT_DEBOUNCE_MS = 50
55
+
56
+ interface Subscription {
57
+ unsubscribe: () => void
58
+ }
59
+
60
+ export class BrowserRenderer {
61
+ private readonly intervalMs: number
62
+ private readonly eventDebounceMs: number
63
+ private readonly pubSub: PubSub | undefined
64
+ private readonly cadence: CadenceTimer
65
+ private readonly debouncer: EventDebouncer
66
+ private readonly tracker = new BufferChangeTracker()
67
+ private readonly subscriptions: Subscription[] = []
68
+ /**
69
+ * Number of consecutive ticks where every key buffer was unchanged.
70
+ * Once it crosses `BLANK_THRESHOLD`, we suspect the React tree has been
71
+ * unmounted (typically by an HMR error that escaped the ErrorBoundary)
72
+ * and warn the user via the logger. The renderer does not abort —
73
+ * screenshots still flow — but the operator gets a visible signal.
74
+ */
75
+ private staticBlankTickCount = 0
76
+ private browser: BrowserLike | null = null
77
+ private context: ContextLike | null = null
78
+ private page: PageLike | null = null
79
+ private running = false
80
+ private readonly logger: pino.Logger
81
+
82
+ constructor(private readonly options: BrowserRendererOptions) {
83
+ this.logger = options.logger.child({ component: "browser-renderer" })
84
+ this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS
85
+ this.eventDebounceMs = options.eventDebounceMs ?? DEFAULT_EVENT_DEBOUNCE_MS
86
+ this.pubSub = options.pubSub
87
+ this.cadence = new CadenceTimer({
88
+ intervalMs: this.intervalMs,
89
+ onTick: () => this.tick("timer"),
90
+ logger: this.logger,
91
+ })
92
+ this.debouncer = new EventDebouncer({
93
+ delayMs: this.eventDebounceMs,
94
+ onFlush: () => this.tick("event"),
95
+ logger: this.logger,
96
+ })
97
+ }
98
+
99
+ async start(): Promise<void> {
100
+ if (this.running) return
101
+ const factory =
102
+ this.options.playwrightFactory ??
103
+ (async (): Promise<PlaywrightLike> =>
104
+ (await import("playwright")) as PlaywrightLike)
105
+ const playwright = await factory()
106
+ try {
107
+ this.browser = await playwright.chromium.launch({ headless: true })
108
+ } catch (err) {
109
+ // ponytail: hardware mode renders every deck frame in headless chromium,
110
+ // so a missing browser download is fatal — but Playwright's own message
111
+ // is a box-drawing banner wrapped in a stack trace that never mentions
112
+ // sirenodeck. Dev builds never auto-install it (ensurePlaywright only
113
+ // ran for installed trees), so this is the first thing a contributor
114
+ // hits when they plug in a real deck. Say what to run.
115
+ const message = (err as Error).message ?? ""
116
+ if (
117
+ /Executable doesn't exist|please run the following command/i.test(
118
+ message,
119
+ )
120
+ ) {
121
+ throw new Error(
122
+ "Headless chromium is not installed — sirenodeck renders deck images with it.\n" +
123
+ " Fix: pnpm --filter @sirenodeck/sirenodeck exec playwright install chromium\n" +
124
+ ` (original error: ${message.split("\n")[0] ?? ""})`,
125
+ )
126
+ }
127
+ throw err
128
+ }
129
+ const { columns, rows } = gridForKeyCount(this.options.device.getKeyCount())
130
+ const vpWidth = columns * BUTTON_SIZE_PX
131
+ const vpHeight = rows * BUTTON_SIZE_PX
132
+ this.context = await this.browser.newContext({
133
+ viewport: { width: vpWidth, height: vpHeight },
134
+ })
135
+ this.page = await this.context.newPage()
136
+ await this.page.goto(this.options.frontendUrl, { waitUntil: "networkidle" })
137
+ if (this.pubSub) {
138
+ this.subscriptions.push(
139
+ {
140
+ unsubscribe: this.pubSub.subscribe("runtime:activeDeck", () =>
141
+ this.debouncer.trigger(),
142
+ ),
143
+ },
144
+ {
145
+ unsubscribe: this.pubSub.subscribe("runtime:invalidate", () =>
146
+ this.debouncer.trigger(),
147
+ ),
148
+ },
149
+ )
150
+ }
151
+ this.cadence.start()
152
+ this.running = true
153
+ }
154
+
155
+ /**
156
+ * Repaints every key on the next tick.
157
+ *
158
+ * ponytail: the change tracker skips keys whose bytes have not changed, so
159
+ * after the hardware goes away and comes back the deck would stay blank —
160
+ * the tracker still holds the hashes of what the OLD handle was showing.
161
+ * Nothing reset it before this; `runtime:invalidate` only triggers a tick.
162
+ */
163
+ forceRedraw(): void {
164
+ this.tracker.reset()
165
+ this.debouncer.trigger()
166
+ }
167
+
168
+ async stop(): Promise<void> {
169
+ if (!this.running) return
170
+ this.running = false
171
+ this.cadence.stop()
172
+ this.debouncer.dispose()
173
+ for (const sub of this.subscriptions) sub.unsubscribe()
174
+ this.subscriptions.length = 0
175
+ if (this.page) await this.page.close()
176
+ if (this.context) await this.context.close()
177
+ if (this.browser) await this.browser.close()
178
+ this.page = null
179
+ this.context = null
180
+ this.browser = null
181
+ }
182
+
183
+ private static readonly BLANK_THRESHOLD = 20
184
+
185
+ private async tick(source: "timer" | "event"): Promise<void> {
186
+ if (!this.page) return
187
+ try {
188
+ const png = await this.page.screenshot({ type: "png" })
189
+ const { columns, rows } = gridForKeyCount(
190
+ this.options.device.getKeyCount(),
191
+ )
192
+ const meta = await sharp(png).metadata()
193
+ const cellWidth = Math.floor((meta.width ?? 0) / columns)
194
+ const cellHeight = Math.floor((meta.height ?? 0) / rows)
195
+ const writes: Array<Promise<void>> = []
196
+ for (let r = 0; r < rows; r++) {
197
+ for (let c = 0; c < columns; c++) {
198
+ const keyIndex = r * columns + c
199
+ const cropped = await sharp(png)
200
+ .extract({
201
+ left: c * cellWidth,
202
+ top: r * cellHeight,
203
+ width: cellWidth,
204
+ height: cellHeight,
205
+ })
206
+ .resize(72, 72, { fit: "fill" })
207
+ .removeAlpha()
208
+ .raw()
209
+ .toBuffer()
210
+ if (!this.tracker.update(keyIndex, cropped)) continue
211
+ writes.push(this.options.device.fillKeyBuffer(keyIndex, cropped))
212
+ }
213
+ }
214
+ if (writes.length > 0) {
215
+ await Promise.all(writes)
216
+ this.staticBlankTickCount = 0
217
+ this.logger.debug(
218
+ { source, writes: writes.length },
219
+ "renderer tick wrote keys",
220
+ )
221
+ } else {
222
+ this.staticBlankTickCount += 1
223
+ this.logger.debug({ source }, "renderer tick no changes")
224
+ if (this.staticBlankTickCount === BrowserRenderer.BLANK_THRESHOLD) {
225
+ this.logger.warn(
226
+ {
227
+ consecutiveTicks: this.staticBlankTickCount,
228
+ source,
229
+ },
230
+ "frontend appears blank — React tree may have crashed (check the browser console or HMR errors)",
231
+ )
232
+ }
233
+ }
234
+ } catch (err) {
235
+ this.logger.warn({ err, source }, "renderer tick failed")
236
+ }
237
+ }
238
+ }
239
+
240
+ export const _testInternals = { createPubSub }
@@ -0,0 +1,20 @@
1
+ import { createHash } from "node:crypto"
2
+
3
+ export const hashBuffer = (buf: Buffer): string =>
4
+ createHash("sha1").update(buf).digest("hex").slice(0, 16)
5
+
6
+ export class BufferChangeTracker {
7
+ private readonly last = new Map<number, string>()
8
+
9
+ update(keyIndex: number, buf: Buffer): boolean {
10
+ const hash = hashBuffer(buf)
11
+ const previous = this.last.get(keyIndex)
12
+ if (previous === hash) return false
13
+ this.last.set(keyIndex, hash)
14
+ return true
15
+ }
16
+
17
+ reset(): void {
18
+ this.last.clear()
19
+ }
20
+ }
@@ -0,0 +1,197 @@
1
+ import type { WebSocket } from "ws"
2
+
3
+ import type { ConfigMutationService } from "@/config/mutation"
4
+ import {
5
+ editorMutationResultMessageSchema,
6
+ editorStateMessageSchema,
7
+ type EditorMutationMessage,
8
+ type EditorUndoMessage,
9
+ type EditorStateMessage,
10
+ editorValidationResultMessageSchema,
11
+ editorSourceValidationResultMessageSchema,
12
+ type EditorSourceValidationRequestMessage,
13
+ type EditorValidationRequestMessage,
14
+ type WsMessage,
15
+ } from "./protocol"
16
+
17
+ export interface EditorMessageHandlerOptions {
18
+ readonly mutationService: ConfigMutationService
19
+ readonly getState: () => Omit<
20
+ EditorStateMessage,
21
+ "type" | "revision" | "canUndo" | "buttonSchemas" | "surfaces"
22
+ > & {
23
+ buttonSchemas?: EditorStateMessage["buttonSchemas"]
24
+ surfaces?: EditorStateMessage["surfaces"]
25
+ revision?: number
26
+ }
27
+ readonly onChanged?: () => void | Promise<void>
28
+ readonly broadcast: (message: WsMessage) => void
29
+ readonly validateConfig?: (buttonType: string, config: unknown) => string[]
30
+ }
31
+
32
+ export interface EditorMessageHandler {
33
+ readonly onMessage: (message: WsMessage, socket: WebSocket) => void
34
+ readonly onConnection: (socket: WebSocket) => void
35
+ readonly invalidate: () => void
36
+ }
37
+
38
+ export const createEditorMessageHandler = (
39
+ options: EditorMessageHandlerOptions,
40
+ ): EditorMessageHandler => {
41
+ let revision = options.getState().revision ?? 0
42
+ let applyQueue = Promise.resolve()
43
+
44
+ const state = (): EditorStateMessage =>
45
+ editorStateMessageSchema.parse({
46
+ type: "editor-state",
47
+ ...options.getState(),
48
+ revision,
49
+ canUndo: options.mutationService.canUndo(),
50
+ })
51
+
52
+ const send = (socket: WebSocket, message: WsMessage): void => {
53
+ if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(message))
54
+ }
55
+
56
+ const result = (
57
+ socket: WebSocket,
58
+ requestId: string,
59
+ ok: boolean,
60
+ error?: string,
61
+ ): void => {
62
+ send(
63
+ socket,
64
+ editorMutationResultMessageSchema.parse({
65
+ type: "editor-mutation-result",
66
+ requestId,
67
+ ok,
68
+ revision,
69
+ ...(error !== undefined ? { error } : {}),
70
+ }),
71
+ )
72
+ }
73
+
74
+ const validationResult = (
75
+ socket: WebSocket,
76
+ request: EditorValidationRequestMessage,
77
+ ): void => {
78
+ const errors =
79
+ options.validateConfig?.(request.buttonType, request.config) ?? []
80
+ send(
81
+ socket,
82
+ editorValidationResultMessageSchema.parse({
83
+ type: "editor-validation-result",
84
+ requestId: request.requestId,
85
+ valid: errors.length === 0,
86
+ errors,
87
+ }),
88
+ )
89
+ }
90
+
91
+ const sourceValidationResult = (
92
+ socket: WebSocket,
93
+ request: EditorSourceValidationRequestMessage,
94
+ ): void => {
95
+ const errors = options.mutationService.validateSource(
96
+ request.path,
97
+ request.content,
98
+ )
99
+ send(
100
+ socket,
101
+ editorSourceValidationResultMessageSchema.parse({
102
+ type: "editor-source-validation-result",
103
+ requestId: request.requestId,
104
+ valid: errors.length === 0,
105
+ errors,
106
+ }),
107
+ )
108
+ }
109
+
110
+ const apply = async (
111
+ socket: WebSocket,
112
+ requestId: string,
113
+ requestRevision: number,
114
+ operation: () => Promise<boolean | void>,
115
+ ): Promise<void> => {
116
+ const task = async (): Promise<void> => {
117
+ if (requestRevision !== revision) {
118
+ result(socket, requestId, false, "stale editor revision")
119
+ send(socket, state())
120
+ return
121
+ }
122
+ try {
123
+ const changed = await operation()
124
+ if (changed === false) {
125
+ result(socket, requestId, false, "nothing to undo")
126
+ return
127
+ }
128
+ revision += 1
129
+ await options.onChanged?.()
130
+ result(socket, requestId, true)
131
+ options.broadcast(state())
132
+ } catch (error) {
133
+ result(
134
+ socket,
135
+ requestId,
136
+ false,
137
+ error instanceof Error ? error.message : String(error),
138
+ )
139
+ }
140
+ }
141
+ const queued = applyQueue.then(task, task)
142
+ applyQueue = queued.then(
143
+ () => undefined,
144
+ () => undefined,
145
+ )
146
+ await queued
147
+ }
148
+
149
+ return {
150
+ onMessage: (message, socket) => {
151
+ if (message.type === "editor-state-request") {
152
+ send(socket, state())
153
+ return
154
+ }
155
+ if (message.type === "editor-validation-request") {
156
+ validationResult(socket, message)
157
+ return
158
+ }
159
+ if (message.type === "editor-source-validation-request") {
160
+ sourceValidationResult(socket, message)
161
+ return
162
+ }
163
+ if (message.type === "editor-asset-write") {
164
+ void options.mutationService
165
+ .writeAsset(message.filename, message.data)
166
+ .then(() => result(socket, message.requestId, true))
167
+ .catch((error: unknown) =>
168
+ result(
169
+ socket,
170
+ message.requestId,
171
+ false,
172
+ error instanceof Error ? error.message : String(error),
173
+ ),
174
+ )
175
+ return
176
+ }
177
+ if (message.type === "editor-mutate") {
178
+ void apply(socket, message.requestId, message.revision, () =>
179
+ options.mutationService.apply(message.mutation),
180
+ )
181
+ return
182
+ }
183
+ if (message.type === "editor-undo") {
184
+ void apply(socket, message.requestId, message.revision, () =>
185
+ options.mutationService.undo(),
186
+ )
187
+ }
188
+ },
189
+ onConnection: (socket) => send(socket, state()),
190
+ invalidate: () => {
191
+ revision += 1
192
+ options.broadcast(state())
193
+ },
194
+ }
195
+ }
196
+
197
+ export type { EditorMutationMessage, EditorUndoMessage }
@@ -0,0 +1,128 @@
1
+ import { spawn, type ChildProcess } from "node:child_process"
2
+ import { existsSync } from "node:fs"
3
+ import { dirname, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ export interface EmulatorServerOptions {
7
+ readonly port: number
8
+ readonly frontendEmulatorDir: string
9
+ readonly env?: Readonly<Record<string, string>>
10
+ readonly logger?: (msg: string) => void
11
+ }
12
+
13
+ export interface EmulatorServerHandle {
14
+ readonly child: ChildProcess
15
+ readonly port: number
16
+ readonly url: string
17
+ close(): Promise<void>
18
+ }
19
+
20
+ const READY_PREFIX = "READY "
21
+
22
+ export const parseReadyLine = (line: string): number | null => {
23
+ const trimmed = line.trim()
24
+ if (!trimmed.startsWith(READY_PREFIX)) return null
25
+ const portStr = trimmed.slice(READY_PREFIX.length).trim()
26
+ const port = Number.parseInt(portStr, 10)
27
+ return Number.isFinite(port) ? port : null
28
+ }
29
+
30
+ export const startEmulatorServer = async (
31
+ options: EmulatorServerOptions,
32
+ ): Promise<EmulatorServerHandle> => {
33
+ const { port, frontendEmulatorDir } = options
34
+ const log = options.logger ?? ((): void => {})
35
+ const viteBin = resolve(frontendEmulatorDir, "node_modules", ".bin", "vite")
36
+ const cliEntry = resolve(frontendEmulatorDir, "src", "main")
37
+
38
+ const env: Record<string, string> = {
39
+ ...(process.env as Record<string, string>),
40
+ ...options.env,
41
+ SIRENO_EMULATOR_PORT: String(port),
42
+ }
43
+
44
+ const child = spawn(
45
+ "node",
46
+ ["--import", "tsx/esm", viteBin, "--port", String(port), cliEntry],
47
+ {
48
+ cwd: frontendEmulatorDir,
49
+ env,
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ },
52
+ )
53
+
54
+ return await new Promise<EmulatorServerHandle>((resolvePromise, reject) => {
55
+ let settled = false
56
+ const onData = (chunk: Buffer): void => {
57
+ const text = chunk.toString("utf8")
58
+ log(text)
59
+ for (const line of text.split("\n")) {
60
+ if (line.startsWith(READY_PREFIX)) {
61
+ const actualPort = Number.parseInt(
62
+ line.slice(READY_PREFIX.length).trim(),
63
+ 10,
64
+ )
65
+ if (!Number.isFinite(actualPort)) {
66
+ return
67
+ }
68
+ if (settled) return
69
+ settled = true
70
+ child.stdout?.off("data", onData)
71
+ child.stderr?.off("data", onErr)
72
+ resolvePromise({
73
+ child,
74
+ port: actualPort,
75
+ url: `http://127.0.0.1:${actualPort}`,
76
+ close: () => closeEmulator(child),
77
+ })
78
+ }
79
+ }
80
+ }
81
+ const onErr = (chunk: Buffer): void => {
82
+ log(`[emulator stderr] ${chunk.toString("utf8")}`)
83
+ }
84
+ child.stdout?.on("data", onData)
85
+ child.stderr?.on("data", onErr)
86
+ child.once("error", (err) => {
87
+ if (!settled) {
88
+ settled = true
89
+ reject(err)
90
+ }
91
+ })
92
+ child.once("exit", (code) => {
93
+ if (!settled) {
94
+ settled = true
95
+ reject(new Error(`emulator child exited early with code ${code}`))
96
+ }
97
+ })
98
+ })
99
+ }
100
+
101
+ const closeEmulator = (child: ChildProcess): Promise<void> =>
102
+ new Promise<void>((resolveClose) => {
103
+ if (child.exitCode !== null) {
104
+ resolveClose()
105
+ return
106
+ }
107
+ child.once("exit", () => resolveClose())
108
+ child.kill("SIGTERM")
109
+ setTimeout(() => {
110
+ if (child.exitCode === null) child.kill("SIGKILL")
111
+ }, 2000).unref()
112
+ })
113
+
114
+ export const emulatorServerEntryExists = (
115
+ frontendEmulatorDir: string,
116
+ ): boolean => {
117
+ const main = resolve(frontendEmulatorDir, "src", "main")
118
+ const pkg = resolve(frontendEmulatorDir, "package.json")
119
+ return existsSync(main) && existsSync(pkg)
120
+ }
121
+
122
+ export const __test_internals = {
123
+ resolveEntry: (frontendEmulatorDir: string): string =>
124
+ resolve(frontendEmulatorDir, "src", "main"),
125
+ }
126
+
127
+ const _internalDirname = dirname(fileURLToPath(import.meta.url))
128
+ void _internalDirname
@@ -0,0 +1,69 @@
1
+ import { isAbsolute, join } from "node:path"
2
+
3
+ import { EMOJI_RE } from "../core/icon-source"
4
+
5
+ export interface ResolveIconPathOptions {
6
+ readonly addonDirs?: ReadonlyMap<string, string>
7
+ readonly baseDirs?: ReadonlyArray<string>
8
+ }
9
+
10
+ export type ResolvedIconSource =
11
+ | { kind: "generic"; name: string }
12
+ | { kind: "asset"; fullPath: string }
13
+ | { kind: "emoji"; emoji: string }
14
+
15
+ export const resolveIconSource = (
16
+ source: string,
17
+ options: ResolveIconPathOptions = {},
18
+ ): ResolvedIconSource => {
19
+ if (EMOJI_RE.test(source)) {
20
+ // Emojis don't need any path resolution — the frontend renders them
21
+ // directly as a span. Returning a "generic" kind would be misleading
22
+ // (no icon name), so we have a dedicated "emoji" kind that callers
23
+ // can recognize and skip path/asset bookkeeping.
24
+ return { kind: "emoji", emoji: source }
25
+ }
26
+ if (source.startsWith("icon://")) {
27
+ return { kind: "generic", name: source.slice("icon://".length) }
28
+ }
29
+ if (source.startsWith("addon://")) {
30
+ const match = /^addon:\/\/([^/]+)\/(.*)$/.exec(source)
31
+ const [, addonName, assetPath] = match ?? []
32
+ if (addonName !== undefined && assetPath !== undefined) {
33
+ const addonDir = options.addonDirs?.get(addonName)
34
+ if (addonDir) {
35
+ return { kind: "asset", fullPath: join(addonDir, assetPath) }
36
+ }
37
+ }
38
+ throw new Error(`Cannot resolve addon icon "${source}": unknown addon dir`)
39
+ }
40
+ if (source.startsWith("builtin://")) {
41
+ const assetPath = source.slice("builtin://".length)
42
+ const builtinDir = options.addonDirs?.get("builtin")
43
+ if (builtinDir) {
44
+ return { kind: "asset", fullPath: join(builtinDir, assetPath) }
45
+ }
46
+ throw new Error(
47
+ `Cannot resolve builtin icon "${source}": builtin dir not registered`,
48
+ )
49
+ }
50
+ if (
51
+ source.startsWith("data:") ||
52
+ source.startsWith("http://") ||
53
+ source.startsWith("https://") ||
54
+ source.startsWith("file://")
55
+ ) {
56
+ throw new Error(
57
+ `Inline icon "${source.slice(0, 32)}…" must be pre-resolved to asset:// before reaching the Icon component`,
58
+ )
59
+ }
60
+ if (isAbsolute(source)) {
61
+ return { kind: "asset", fullPath: source }
62
+ }
63
+ // ponytail: returns on first baseDir; baseDirs is always 1 entry today (config dir).
64
+ // If multiple baseDirs ever get added, this needs a statSync existence check + iteration.
65
+ for (const baseDir of options.baseDirs ?? []) {
66
+ return { kind: "asset", fullPath: join(baseDir, source) }
67
+ }
68
+ throw new Error(`Cannot resolve icon "${source}": no base dirs configured`)
69
+ }
@@ -0,0 +1,16 @@
1
+ export {
2
+ BrowserRenderer,
3
+ type BrowserRendererOptions,
4
+ type DeviceSink,
5
+ type PlaywrightLike,
6
+ type BrowserLike,
7
+ type ContextLike,
8
+ type PageLike,
9
+ } from "./browser-renderer"
10
+ export {
11
+ CadenceTimer,
12
+ EventDebouncer,
13
+ type CadenceTimerOptions,
14
+ type EventDebouncerOptions,
15
+ } from "./screenshot-cadence"
16
+ export { BufferChangeTracker, hashBuffer } from "./buffer-hash"