@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,175 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { existsSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ export type PackageManager = "pnpm" | "npm" | "yarn"
6
+
7
+ const managers: PackageManager[] = ["pnpm", "npm", "yarn"]
8
+
9
+ /**
10
+ * ponytail: every probe in this module runs a package manager we did not write,
11
+ * found on `PATH`, purely to ask it a question. A broken one must not be able to
12
+ * take the daemon down with it, so the probes are bounded on three axes:
13
+ *
14
+ * - `timeout` + `killSignal`, because a hung manager used to hang startup
15
+ * forever with no output at all. A corrupt pnpm shim that re-`exec`s itself
16
+ * made `pnpm root --global` never return; `execFileSync` has no timeout by
17
+ * default, so the daemon simply stopped there, before it bound a port or lit
18
+ * a single key. SIGTERM is not enough — a manager that is busy spawning does
19
+ * not get round to handling it — so the deadline is enforced with SIGKILL.
20
+ * - `maxBuffer`, so a manager that writes without end fails the probe instead
21
+ * of growing the daemon's heap.
22
+ * - no network. These probes only ask "are you there?" and "where do you keep
23
+ * global packages?". Corepack reads that as licence to go and fetch the
24
+ * manager first — probing for `yarn` downloaded Yarn 1.22.22 on a machine
25
+ * that had never asked for it. A read-only question installs nothing.
26
+ */
27
+ const DEFAULT_PROBE_TIMEOUT_MS = 10_000
28
+ const PROBE_KILL_SIGNAL = "SIGKILL"
29
+
30
+ /**
31
+ * `SIRENO_PACKAGE_MANAGER_TIMEOUT_MS` raises the deadline for a machine where a
32
+ * cold Corepack legitimately needs longer than ten seconds to answer, and lets
33
+ * the tests use a deadline they can wait for. Anything unparseable or
34
+ * non-positive falls back to the default rather than disabling the bound.
35
+ */
36
+ export const packageManagerProbeTimeoutMs = (): number => {
37
+ const raw = process.env["SIRENO_PACKAGE_MANAGER_TIMEOUT_MS"]
38
+ const parsed = raw === undefined ? Number.NaN : Number.parseInt(raw, 10)
39
+ return Number.isFinite(parsed) && parsed > 0
40
+ ? parsed
41
+ : DEFAULT_PROBE_TIMEOUT_MS
42
+ }
43
+
44
+ const probeEnv = (): NodeJS.ProcessEnv => ({
45
+ ...process.env,
46
+ COREPACK_ENABLE_NETWORK: "0",
47
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
48
+ })
49
+
50
+ const probeOptions = () =>
51
+ ({
52
+ timeout: packageManagerProbeTimeoutMs(),
53
+ killSignal: PROBE_KILL_SIGNAL,
54
+ maxBuffer: 1024 * 1024,
55
+ }) as const
56
+
57
+ const hasCommand = (command: string): boolean => {
58
+ try {
59
+ execFileSync(command, ["--version"], {
60
+ ...probeOptions(),
61
+ stdio: "ignore",
62
+ env: probeEnv(),
63
+ })
64
+ return true
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ export const detectPackageManager = (projectDir: string): PackageManager => {
71
+ const lockfile = managers.find((manager) =>
72
+ existsSync(
73
+ join(
74
+ projectDir,
75
+ manager === "pnpm"
76
+ ? "pnpm-lock.yaml"
77
+ : manager === "npm"
78
+ ? "package-lock.json"
79
+ : "yarn.lock",
80
+ ),
81
+ ),
82
+ )
83
+ if (lockfile !== undefined && hasCommand(lockfile)) return lockfile
84
+ const available = managers.find(hasCommand)
85
+ if (available === undefined)
86
+ throw new Error(
87
+ "Install pnpm, npm, or yarn before installing an addon or theme.",
88
+ )
89
+ return available
90
+ }
91
+
92
+ export const assertPackageManager = (manager: PackageManager): void => {
93
+ if (!hasCommand(manager))
94
+ throw new Error(`Package manager '${manager}' is not installed.`)
95
+ }
96
+
97
+ export const packageManagerArgs = (
98
+ manager: PackageManager,
99
+ specifier: string,
100
+ global: boolean,
101
+ ): string[] => {
102
+ if (global) {
103
+ return manager === "pnpm"
104
+ ? ["add", "--global", specifier]
105
+ : manager === "npm"
106
+ ? ["install", "--global", specifier]
107
+ : ["global", "add", specifier]
108
+ }
109
+ return manager === "pnpm"
110
+ ? ["add", "--save-exact", specifier]
111
+ : manager === "npm"
112
+ ? ["install", "--save-exact", specifier]
113
+ : ["add", "--exact", specifier]
114
+ }
115
+
116
+ type RootProbe =
117
+ | { readonly kind: "root"; readonly root: string }
118
+ | { readonly kind: "unavailable" }
119
+ | { readonly kind: "timeout" }
120
+
121
+ /**
122
+ * Only the deadline is worth reporting. A manager that is absent, that exits
123
+ * non-zero, or that declines the question ("the global bin directory is not in
124
+ * PATH", or Corepack refusing a manager this project does not pin) has answered
125
+ * — it simply has no root to offer, which is the ordinary case on a machine
126
+ * that uses one of the three. Warning about those would fire on every start.
127
+ *
128
+ * Being killed at the deadline is different: the manager never answered at all,
129
+ * and before the deadline existed that is precisely what hung the daemon.
130
+ */
131
+ const probeRoot = (manager: PackageManager): RootProbe => {
132
+ try {
133
+ const root = execFileSync(manager, ["root", "--global"], {
134
+ ...probeOptions(),
135
+ encoding: "utf8",
136
+ env: probeEnv(),
137
+ }).trim()
138
+ return root.length > 0 ? { kind: "root", root } : { kind: "unavailable" }
139
+ } catch (error) {
140
+ const err = error as NodeJS.ErrnoException & { signal?: string | null }
141
+ // `execFileSync` reports the deadline as the signal it killed the child
142
+ // with; `ETIMEDOUT` covers the platforms that surface it as an error code.
143
+ return err.signal === PROBE_KILL_SIGNAL || err.code === "ETIMEDOUT"
144
+ ? { kind: "timeout" }
145
+ : { kind: "unavailable" }
146
+ }
147
+ }
148
+
149
+ export const globalPackageRoot = (manager: PackageManager): string | null => {
150
+ const probe = probeRoot(manager)
151
+ return probe.kind === "root" ? probe.root : null
152
+ }
153
+
154
+ export interface GlobalPackageRootProbe {
155
+ readonly roots: ReadonlyArray<string>
156
+ readonly timedOut: ReadonlyArray<PackageManager>
157
+ readonly timeoutMs: number
158
+ }
159
+
160
+ /**
161
+ * Ask every package manager where it keeps its global packages, so an addon or
162
+ * theme installed globally can be resolved from there. Managers that cannot
163
+ * answer contribute nothing; the ones that ran out of time are named, because
164
+ * a silent ten-second stall per manager needs explaining.
165
+ */
166
+ export const probeGlobalPackageRoots = (): GlobalPackageRootProbe => {
167
+ const roots: string[] = []
168
+ const timedOut: PackageManager[] = []
169
+ for (const manager of managers) {
170
+ const probe = probeRoot(manager)
171
+ if (probe.kind === "root") roots.push(probe.root)
172
+ else if (probe.kind === "timeout") timedOut.push(manager)
173
+ }
174
+ return { roots, timedOut, timeoutMs: packageManagerProbeTimeoutMs() }
175
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+
4
+ import type { CommandExecutor } from "@/system/providers/shared"
5
+ import type { ProbeDeps } from "@/system/setup-wizard"
6
+
7
+ // ponytail: shared probe deps for the startup banner and the first-run check.
8
+ // Both call sites use these identical deps, so probeAllCached's single-slot
9
+ // TTL cache returns the same report to both — the banner's probe and the
10
+ // first-run probe don't double the subprocess cost.
11
+
12
+ export const wizardCommandExecutor: CommandExecutor = {
13
+ async run(command, args, options) {
14
+ const { execa } = await import("execa")
15
+ const result = await execa(command, [...args], {
16
+ reject: false,
17
+ timeout: options?.timeoutMs ?? 5_000,
18
+ })
19
+ return {
20
+ exitCode: result.exitCode ?? -1,
21
+ stdout: result.stdout ?? "",
22
+ stderr: result.stderr ?? "",
23
+ }
24
+ },
25
+ }
26
+
27
+ export const buildStandardProbeDeps = (): ProbeDeps => {
28
+ const home = homedir()
29
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"] ?? `${home}/.config`
30
+ return {
31
+ platform: process.platform,
32
+ homeDir: home,
33
+ xdgConfigHome,
34
+ env: process.env,
35
+ executor: wizardCommandExecutor,
36
+ fileExists: (p) => existsSync(p),
37
+ readFile: (p) => {
38
+ try {
39
+ return readFileSync(p, "utf8")
40
+ } catch {
41
+ return null
42
+ }
43
+ },
44
+ }
45
+ }
@@ -0,0 +1,22 @@
1
+ // ponytail: thin re-export of @clack/prompts so backend modules
2
+ // (`packages/cli/src/cli/...`) can pull clack's primitives without
3
+ // each one importing the upstream package name. Lives in `cli/` not
4
+ // `ui/` because the `ui/` tree is reserved for React surface code
5
+ // consumed by `packages/cli/frontend/` and `packages/cli/config-ui/`.
6
+ //
7
+ // Anything that imports this file MUST be in the CLI backend — never
8
+ // the frontend or emulator SPAs.
9
+ export {
10
+ intro,
11
+ outro,
12
+ note,
13
+ log,
14
+ tasks,
15
+ spinner,
16
+ select,
17
+ confirm,
18
+ text,
19
+ password,
20
+ isCancel,
21
+ cancel,
22
+ } from "@clack/prompts"
@@ -0,0 +1,452 @@
1
+ import { connect, type Server, createServer, type Socket } from "node:net"
2
+
3
+ import qrcode from "qrcode"
4
+
5
+ import { listDevices, type DeviceDescriptor } from "@/device"
6
+ import type { AddonCheckOutcome } from "@/addon/check-runner"
7
+
8
+ import {
9
+ probeAllCached,
10
+ probeCommandExecution,
11
+ probeInternetAccess,
12
+ resetProbeCache,
13
+ type RuntimeFeatureProbe,
14
+ type SystemReport,
15
+ } from "@/system/setup-wizard"
16
+
17
+ import { cancel, intro, log, outro } from "@/cli/prompt"
18
+
19
+ import {
20
+ readDaemonEventsFromSnapshot,
21
+ type DaemonEvent,
22
+ type DaemonLogSnapshot,
23
+ } from "@/util/log-reader"
24
+ import {
25
+ readRuntimeState,
26
+ resolveDaemonPaths,
27
+ type RuntimeState,
28
+ } from "@/util/daemon"
29
+ import { requestDaemonToken } from "@/util/daemon-control"
30
+
31
+ import { buildStandardProbeDeps } from "./probe-deps"
32
+
33
+ interface BannerOptions {
34
+ readonly emulator: boolean
35
+ readonly deviceModel?: string
36
+ readonly port?: number
37
+ }
38
+
39
+ interface BannerDeps {
40
+ readonly probeAll: typeof probeAllCached
41
+ readonly probeExec: typeof probeCommandExecution
42
+ readonly probeHttp: typeof probeInternetAccess
43
+ readonly listDevices: typeof listDevices
44
+ readonly resetCache: typeof resetProbeCache
45
+ }
46
+
47
+ const defaultBannerDeps: BannerDeps = {
48
+ probeAll: probeAllCached,
49
+ probeExec: probeCommandExecution,
50
+ probeHttp: probeInternetAccess,
51
+ listDevices,
52
+ resetCache: resetProbeCache,
53
+ }
54
+
55
+ export const isLogSuppressed = (
56
+ argv: Readonly<{ quiet?: boolean; logLevel?: string }>,
57
+ ): boolean =>
58
+ argv.quiet === true || argv.logLevel === "silent" || argv.logLevel === "none"
59
+
60
+ const DEFAULT_PORT = 52937
61
+ const READY_TIMEOUT_MS = 30_000
62
+ const READY_INTERVAL_MS = 100
63
+ const RUNTIME_STATE_TIMEOUT_MS = 30_000
64
+ const PORT_FREE_TIMEOUT_MS = 3_000
65
+
66
+ const checkTcp = (
67
+ host: string,
68
+ port: number,
69
+ timeoutMs: number,
70
+ ): Promise<boolean> =>
71
+ new Promise((resolve) => {
72
+ const sock: Socket = connect(port, host)
73
+ let settled = false
74
+ const finish = (ok: boolean): void => {
75
+ if (settled) return
76
+ settled = true
77
+ sock.destroy()
78
+ resolve(ok)
79
+ }
80
+ sock.once("connect", () => finish(true))
81
+ sock.once("error", () => finish(false))
82
+ setTimeout(() => finish(false), timeoutMs)
83
+ })
84
+
85
+ // ponytail: wrapper around `checkTcp` that polls until the daemon's
86
+ // WS port accepts a connection, or until `timeoutMs` elapses. The
87
+ // caller decides what to do on timeout (CLI: fail loudly with the
88
+ // accumulated daemon events). The TCP probe is intentionally cheap —
89
+ // a single `connect()` per interval — and Node handles the OS-level
90
+ // `ECONNREFUSED` on a closed port without holding the event loop.
91
+ export const waitForDaemonReady = async (
92
+ port: number = DEFAULT_PORT,
93
+ timeoutMs: number = READY_TIMEOUT_MS,
94
+ ): Promise<void> => {
95
+ const deadline = Date.now() + timeoutMs
96
+ while (Date.now() < deadline) {
97
+ if (await checkTcp("127.0.0.1", port, READY_INTERVAL_MS)) return
98
+ await new Promise((r) => setTimeout(r, READY_INTERVAL_MS))
99
+ }
100
+ throw new DaemonNotReadyError(port, timeoutMs)
101
+ }
102
+
103
+ export class DaemonNotReadyError extends Error {
104
+ readonly port: number
105
+ readonly timeoutMs: number
106
+ constructor(port: number, timeoutMs: number) {
107
+ super(
108
+ `daemon: port ${port} did not accept connections within ${timeoutMs}ms — daemon may have failed to start`,
109
+ )
110
+ this.name = "DaemonNotReadyError"
111
+ this.port = port
112
+ this.timeoutMs = timeoutMs
113
+ }
114
+ }
115
+
116
+ // ponytail: the daemon writes `runtime-state.json` only after its WS
117
+ // bridge + vite supervisors are up. Polling for the file is the
118
+ // canonical "fully ready" signal — much stronger than TCP-on-port
119
+ // (which fires the moment the WS server binds, before supervisors
120
+ // have stabilized). Returns the parsed state, or `null` on timeout.
121
+ export const waitForRuntimeState = async (
122
+ timeoutMs: number = RUNTIME_STATE_TIMEOUT_MS,
123
+ notBefore?: number,
124
+ ): Promise<RuntimeState | null> => {
125
+ const deadline = Date.now() + timeoutMs
126
+ while (Date.now() < deadline) {
127
+ const state = readRuntimeState()
128
+ if (
129
+ state !== null &&
130
+ (notBefore === undefined || state.startedAt >= notBefore)
131
+ ) {
132
+ return state
133
+ }
134
+ await new Promise((r) => setTimeout(r, READY_INTERVAL_MS))
135
+ }
136
+ return null
137
+ }
138
+
139
+ // ponytail: bind a `Server` to the target port. If `EADDRINUSE` fires,
140
+ // the port is bound by some other process (most likely the daemon we
141
+ // just stopped, with a stuck TIME_WAIT or an orphan vite). If the
142
+ // bind succeeds, the port is genuinely free. Used by `stop` to
143
+ // confirm the daemon has fully released its socket.
144
+ export const waitForPortFree = async (
145
+ port: number,
146
+ timeoutMs: number = PORT_FREE_TIMEOUT_MS,
147
+ ): Promise<boolean> => {
148
+ const deadline = Date.now() + timeoutMs
149
+ while (Date.now() < deadline) {
150
+ const ok = await tryBind(port)
151
+ if (ok) return true
152
+ await new Promise((r) => setTimeout(r, READY_INTERVAL_MS))
153
+ }
154
+ return false
155
+ }
156
+
157
+ const tryBind = (port: number): Promise<boolean> =>
158
+ new Promise<boolean>((resolve) => {
159
+ let server: Server | null = null
160
+ const cleanup = (ok: boolean): void => {
161
+ if (server !== null) {
162
+ server.close()
163
+ server = null
164
+ }
165
+ resolve(ok)
166
+ }
167
+ server = createServer()
168
+ server.once("error", () => cleanup(false))
169
+ server.once("listening", () => cleanup(true))
170
+ try {
171
+ server.listen(port, "127.0.0.1")
172
+ } catch {
173
+ cleanup(false)
174
+ }
175
+ })
176
+
177
+ interface FeatureItem {
178
+ readonly name: string
179
+ readonly available: boolean
180
+ readonly reason?: string
181
+ }
182
+
183
+ const formatFeaturesLine = (items: ReadonlyArray<FeatureItem>): string =>
184
+ items
185
+ .map((it) =>
186
+ it.available
187
+ ? `[✓ ${it.name}]`
188
+ : `[✗ ${it.name}${it.reason !== undefined ? ` — ${it.reason}` : ""}]`,
189
+ )
190
+ .join(" ")
191
+
192
+ const toFeatureItem = (
193
+ name: string,
194
+ probe: RuntimeFeatureProbe,
195
+ ): FeatureItem =>
196
+ probe.reason !== undefined
197
+ ? { name, available: probe.available, reason: probe.reason }
198
+ : { name, available: probe.available }
199
+
200
+ const deviceLabel = (
201
+ emulator: boolean,
202
+ deviceModel: string | undefined,
203
+ devices: ReadonlyArray<DeviceDescriptor>,
204
+ ): string => {
205
+ if (emulator) {
206
+ return `Emulator (${deviceModel ?? "mk2"})`
207
+ }
208
+ if (devices.length === 0) {
209
+ return "detecting…"
210
+ }
211
+ const d = devices[0]
212
+ return d !== undefined ? `${d.model} (${d.id})` : "detecting…"
213
+ }
214
+
215
+ export interface BannerResult {
216
+ readonly featuresLine: string
217
+ readonly deviceLabel: string
218
+ }
219
+
220
+ export const buildStartupBanner = async (
221
+ options: BannerOptions,
222
+ argv: Readonly<{ quiet?: boolean; logLevel?: string }>,
223
+ deps: BannerDeps = defaultBannerDeps,
224
+ ): Promise<BannerResult | null> => {
225
+ if (isLogSuppressed(argv) || !process.stdout.isTTY) return null
226
+
227
+ deps.resetCache()
228
+
229
+ const [report, exec, http, devices] = await Promise.all([
230
+ deps.probeAll(buildStandardProbeDeps()),
231
+ deps.probeExec(),
232
+ deps.probeHttp(),
233
+ deps.listDevices(),
234
+ ])
235
+
236
+ // ponytail: drop the misleading "media — chromium not installed" probe from
237
+ // the system-feature line. That probe is for Playwright tests (frontend
238
+ // chromium install), not the media addon. The actual media tooling check
239
+ // (playerctl / wpctl / osascript / powershell) lives in the addon-checks
240
+ // section printed after the URL — see runBuiltinAddonChecks / printAddonCheckResults.
241
+ const items: FeatureItem[] = [
242
+ toFeatureItem("keystrokes", toSystemCap(report, "keyMacro")),
243
+ toFeatureItem("clipboard", toSystemCap(report, "clipboard")),
244
+ toFeatureItem("notifications", toSystemCap(report, "notification")),
245
+ toFeatureItem("active-win", toSystemCap(report, "activeApp")),
246
+ toFeatureItem("exec", exec),
247
+ toFeatureItem("http", http),
248
+ ]
249
+
250
+ const featuresLine = formatFeaturesLine(items)
251
+ const label = deviceLabel(options.emulator, options.deviceModel, devices)
252
+
253
+ intro("Starting SirenoDeck")
254
+ log.info(`Device: ${label}`)
255
+ log.info(featuresLine)
256
+ return { featuresLine, deviceLabel: label }
257
+ }
258
+
259
+ const toSystemCap = (
260
+ report: SystemReport,
261
+ key: "keyMacro" | "clipboard" | "notification" | "activeApp",
262
+ ): RuntimeFeatureProbe => {
263
+ const cap = report.capabilities[key]
264
+ if (cap.available) return { available: true }
265
+ return {
266
+ available: false,
267
+ reason:
268
+ cap.missing.length > 0
269
+ ? cap.missing.join(", ")
270
+ : cap.reason.length > 0
271
+ ? cap.reason
272
+ : undefined,
273
+ }
274
+ }
275
+
276
+ // ponytail: warning / error lines from the daemon, surfaced inline. The
277
+ // CLI is the operator's interface to the daemon — they shouldn't have
278
+ // to grep `service.log` to find out what went wrong during start /
279
+ // ponytail: clack's log.* tools give us a color-coded icon + │ indent
280
+ // for free, matching the rest of the banner. Available → log.info,
281
+ // warn/fatal/error → log.warn/log.error. No manual ANSI escapes; no
282
+ // trailing (HH:MM:SS) — timestamps stay in service.log for correlation.
283
+ export const printDaemonEvents = (events: ReadonlyArray<DaemonEvent>): void => {
284
+ if (events.length === 0) return
285
+ for (const ev of events) {
286
+ const ctx = ev.component.length > 0 ? ` [${ev.component}]` : ""
287
+ const line = `${ev.message}${ctx}`
288
+ if (ev.level === "fatal" || ev.level === "error") log.error(line)
289
+ else if (ev.level === "warn") log.warn(line)
290
+ else log.info(line)
291
+ }
292
+ }
293
+
294
+ // ponytail: the URL the operator needs to open in their browser. The
295
+ // token is regenerated per daemon session, so we always show it in the
296
+ // URL — operators copy-paste the URL into the browser. For `--remote`
297
+ // we use the existing QR banner (phone-friendly); for plain
298
+ // `--emulator` we print a plain text URL — no QR/LAN section, since the daemon only binds 127.0.0.1.
299
+ // For real (hardware) mode we print the Config UI URL. The Config UI owns the
300
+ // clickable preview and embeds the frontend, so the raw frontend URL is not an
301
+ // operator-facing startup endpoint.
302
+ export const printDaemonUrl = async (
303
+ state: RuntimeState,
304
+ tokenOrOutput: string | ((text: string) => void) = "",
305
+ output: (text: string) => void = (text) => process.stdout.write(text),
306
+ ): Promise<void> => {
307
+ const token = typeof tokenOrOutput === "string" ? tokenOrOutput : ""
308
+ const write = typeof tokenOrOutput === "function" ? tokenOrOutput : output
309
+ if (state.emulatorMode) {
310
+ const port = state.configUiUrl.split(":").pop() ?? ""
311
+ const buildUrl = (host: string, deckOnly: boolean): string => {
312
+ const params = new URLSearchParams()
313
+ if (token.length > 0) params.set("token", token)
314
+ if (deckOnly) params.set("deckOnly", "1")
315
+ return `http://${host}:${port}?${params.toString()}`
316
+ }
317
+ const localUrl = buildUrl("127.0.0.1", false)
318
+ write(` Config UI: ${localUrl}#/config\n`)
319
+ write(`\n Emulator: ${localUrl}\n`)
320
+ if (state.remote && state.addresses.length > 0) {
321
+ const isTty = Boolean(process.stdout.isTTY)
322
+ if (isTty) {
323
+ write("\n Emulator (LAN):\n")
324
+ for (const addr of state.addresses) {
325
+ const url = buildUrl(addr, true)
326
+ const qr = await qrcode.toString(url, {
327
+ type: "terminal",
328
+ small: true,
329
+ })
330
+ write(`\n${qr} ${url}\n`)
331
+ }
332
+ } else {
333
+ for (const addr of state.addresses) {
334
+ write(` ${addr}: ${buildUrl(addr, false)}\n`)
335
+ }
336
+ }
337
+ }
338
+ write("\n Manage with: `p dev status`, `p dev reload`, `p dev stop`.\n")
339
+ } else {
340
+ const configUiUrl = state.configUiUrl
341
+ const configUiWithToken =
342
+ token.length > 0
343
+ ? `${configUiUrl}${configUiUrl.includes("?") ? "&" : "?"}token=${token}`
344
+ : configUiUrl
345
+ const configUi = `${configUiWithToken}#/config`
346
+ const content = ` Config UI: ${configUi} `
347
+ const border = `+${"-".repeat(content.length)}+`
348
+ write(`\n${border}\n|${content}|\n${border}\n`)
349
+ write("\n Manage with: `p dev status`, `p dev reload`, `p dev stop`.\n")
350
+ }
351
+ }
352
+
353
+ // ponytail: the load-bearing function for the CLI's "wait until fully
354
+ // started, then show the URL" flow. The CLI calls this from the
355
+ // `start` handler and surfaces events + URL inline. On any failure
356
+ // path the caller exits non-zero — operators expect the CLI to fail
357
+ // loudly if the daemon didn't come up, not silently exit 0.
358
+ export interface StartOutcome {
359
+ readonly state: RuntimeState | null
360
+ readonly token: string | null
361
+ readonly events: ReadonlyArray<DaemonEvent>
362
+ readonly tcpReady: boolean
363
+ readonly runtimeReady: boolean
364
+ }
365
+
366
+ export interface WaitForStartOptions {
367
+ readonly port: number
368
+ readonly tcpTimeoutMs: number
369
+ readonly runtimeTimeoutMs: number
370
+ readonly logPath: string
371
+ readonly logSnapshot: DaemonLogSnapshot
372
+ readonly notBefore?: number
373
+ }
374
+
375
+ export const waitForFullStart = async (
376
+ options: WaitForStartOptions,
377
+ ): Promise<StartOutcome> => {
378
+ let tcpReady = false
379
+ try {
380
+ await waitForDaemonReady(options.port, options.tcpTimeoutMs)
381
+ tcpReady = true
382
+ } catch (err) {
383
+ if (!(err instanceof DaemonNotReadyError)) throw err
384
+ }
385
+ const runtimeReady = tcpReady
386
+ const state = runtimeReady
387
+ ? await waitForRuntimeState(options.runtimeTimeoutMs, options.notBefore)
388
+ : null
389
+ const events = readDaemonEventsFromSnapshot(
390
+ options.logPath,
391
+ options.logSnapshot,
392
+ )
393
+ const token = runtimeReady
394
+ ? await requestDaemonToken(resolveDaemonPaths())
395
+ : null
396
+ return { state, token, events, tcpReady, runtimeReady }
397
+ }
398
+
399
+ // ponytail: per-addon requirement check results, surfaced inline next
400
+ // to the URL. Clack's log.* tools give us a color-coded icon + │ indent
401
+ // for free. Available checks → log.info (green ✓), failed → log.warn
402
+ // (yellow ✗) with the reason inline. One log line per outcome so the
403
+ // status banner shows every check independently. Grouping by addon
404
+ // is preserved by the addon name in the message prefix.
405
+ export const printAddonCheckResults = (
406
+ outcomes: ReadonlyArray<AddonCheckOutcome>,
407
+ ): void => {
408
+ if (outcomes.length === 0) return
409
+ for (const outcome of outcomes) {
410
+ const reason = outcome.reason !== undefined ? ` — ${outcome.reason}` : ""
411
+ if (outcome.available) {
412
+ log.info(`${outcome.addonName}/${outcome.checkName}`)
413
+ } else {
414
+ log.warn(`${outcome.addonName}/${outcome.checkName}${reason}`)
415
+ }
416
+ }
417
+ }
418
+
419
+ export const printStartupComplete = (): void => {
420
+ if (!process.stdout.isTTY) return
421
+ outro("✓ Sireno Deck started")
422
+ }
423
+
424
+ export const printStartupFailed = (err: unknown): void => {
425
+ if (!process.stdout.isTTY) return
426
+ cancel(
427
+ `✗ Failed to start SirenoDeck: ${err instanceof Error ? err.message : String(err)}`,
428
+ )
429
+ }
430
+
431
+ export const printRestartComplete = (): void => {
432
+ if (!process.stdout.isTTY) return
433
+ outro("✓ Sireno Deck restarted")
434
+ }
435
+
436
+ export const printRestartFailed = (err: unknown): void => {
437
+ if (!process.stdout.isTTY) return
438
+ cancel(
439
+ `✗ Failed to restart SirenoDeck: ${err instanceof Error ? err.message : String(err)}`,
440
+ )
441
+ }
442
+
443
+ export const printStopComplete = (portFree: boolean): void => {
444
+ if (!process.stdout.isTTY) return
445
+ if (portFree) {
446
+ outro("✓ Sireno Deck stopped")
447
+ } else {
448
+ cancel(
449
+ "✗ Sireno Deck process exited but port 52937 is still bound — see `p dev status` for orphans",
450
+ )
451
+ }
452
+ }