@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,575 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ existsSync,
5
+ fchmodSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ statSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs"
14
+ import { randomBytes } from "node:crypto"
15
+ import { homedir, tmpdir } from "node:os"
16
+ import { join } from "node:path"
17
+ import { getuid, platform } from "node:process"
18
+
19
+ const getUid = (): number | null => {
20
+ try {
21
+ if (typeof getuid !== "function") return null
22
+ return getuid()
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ import type pino from "pino"
29
+
30
+ export interface DaemonPaths {
31
+ // XDG_RUNTIME_DIR — tmpfs in Linux user scope; vanishes on reboot.
32
+ // pidFile identifies the current daemon process.
33
+ // and child PIDs are re-spawned on every boot, so neither is worth
34
+ // persisting. childrenFile is XDG_RUNTIME_DIR too — the SPA/vite/playwright
35
+ // children re-derive on boot anyway, so the file's value across reboots
36
+ // is post-crash child tracking only.
37
+ runtimeDir: string
38
+ pidFile: string
39
+ controlSocket: string
40
+ childrenFile: string
41
+ // XDG_STATE_HOME — persistent. Survives reboot. Holds the bits the
42
+ // daemon needs to hand the same live experience back to the user on
43
+ // next boot: last published snapshot (provider agent counts, statuses,
44
+ // attention flags), restart-mode preferences, and the resolved config
45
+ // path pointer so the daemon re-loads the user's config without
46
+ // requiring a fresh `p dev` invocation.
47
+ dataDir: string
48
+ configPathFile: string
49
+ flagsFile: string
50
+ runtimeStateFile: string
51
+ // Ponytail: stateFile is the legacy alias for runtimeStateFile —
52
+ // callers from older builds reference this name. Kept as a re-export
53
+ // of `runtimeStateFile` so the value is shared regardless of which
54
+ // an addon reads.
55
+ stateFile: string
56
+ }
57
+
58
+ const writeAtomic = (path: string, content: string, mode: number): void => {
59
+ const tmp = `${path}.tmp.${process.pid}`
60
+ const fd = openSync(tmp, "w", mode)
61
+ try {
62
+ writeFileSync(fd, content, { encoding: "utf8" })
63
+ fchmodSync(fd, mode)
64
+ } finally {
65
+ closeSync(fd)
66
+ }
67
+ renameSync(tmp, path)
68
+ }
69
+
70
+ export interface RuntimeFlags {
71
+ emulator: boolean
72
+ remote?: boolean
73
+ deviceModel?: string
74
+ port?: number
75
+ httpPort: number
76
+ config?: string
77
+ noAutoOpen?: boolean
78
+ }
79
+
80
+ const DAEMON_NAME = "sirenodeck"
81
+
82
+ const defaultRuntimeDir = (): string => {
83
+ const xdg = process.env["XDG_RUNTIME_DIR"]
84
+ if (xdg) return xdg
85
+
86
+ switch (platform) {
87
+ case "darwin":
88
+ return join(homedir(), "Library", "Application Support", DAEMON_NAME)
89
+ case "win32":
90
+ return join(process.env["LOCALAPPDATA"] ?? tmpdir(), DAEMON_NAME)
91
+ default: {
92
+ // ponytail: getuid is undefined-typed on win32 per @types/node, but this
93
+ // branch only runs on linux; the runtime check satisfies the type.
94
+ const uid = typeof getuid === "function" ? getuid() : null
95
+ return uid !== null
96
+ ? join(tmpdir(), `sirenodeck-${uid}`)
97
+ : join(tmpdir(), DAEMON_NAME)
98
+ }
99
+ }
100
+ }
101
+
102
+ // ponytail: persistent storage location. On Linux, XDG_STATE_HOME
103
+ // defaults to ~/.local/state; macOS uses Library/Application Support;
104
+ // Windows uses %LOCALAPPDATA%. Always prefer the env var when set so
105
+ // CI / sandboxed environments can override.
106
+ const defaultDataDir = (): string => {
107
+ const xdg = process.env["XDG_STATE_HOME"]
108
+ if (xdg) return join(xdg, DAEMON_NAME)
109
+ switch (platform) {
110
+ case "darwin":
111
+ return join(
112
+ process.env["XDG_DATA_HOME"] ??
113
+ join(homedir(), "Library", "Application Support"),
114
+ DAEMON_NAME,
115
+ )
116
+ case "win32":
117
+ return join(process.env["LOCALAPPDATA"] ?? tmpdir(), DAEMON_NAME)
118
+ default:
119
+ return join(
120
+ process.env["XDG_STATE_HOME"] ??
121
+ process.env["XDG_DATA_HOME"] ??
122
+ join(homedir(), ".local", "state"),
123
+ DAEMON_NAME,
124
+ )
125
+ }
126
+ }
127
+
128
+ // ponytail: AF_UNIX paths are capped by `sun_path` — 104 bytes on macOS/BSD,
129
+ // 108 on Linux — and `bind()` fails with a bare EINVAL when you exceed it, with
130
+ // nothing in the message naming the length as the cause. The macOS default
131
+ // runtime dir (~/Library/Application Support/sirenodeck) already spends ~70 of
132
+ // those bytes, so a long username or an XDG_RUNTIME_DIR override blows the cap.
133
+ // Fall back to a short, uid-scoped path under the system temp dir, which is the
134
+ // same place Linux puts it when XDG_RUNTIME_DIR is unset.
135
+ const SUN_PATH_MAX = platform === "linux" ? 108 : 104
136
+
137
+ export const resolveSocketPath = (
138
+ runtimeDir: string,
139
+ basename = "",
140
+ ): string => {
141
+ const preferred = join(runtimeDir, `${DAEMON_NAME}${basename}.sock`)
142
+ if (platform === "win32") return preferred
143
+ if (Buffer.byteLength(preferred, "utf8") < SUN_PATH_MAX) return preferred
144
+ const uid = typeof getuid === "function" ? getuid() : 0
145
+ return join(tmpdir(), `${DAEMON_NAME}-${uid}${basename}.sock`)
146
+ }
147
+
148
+ const resolveControlSocket = (runtimeDir: string): string =>
149
+ resolveSocketPath(runtimeDir)
150
+
151
+ export const resolveDaemonPaths = (): DaemonPaths => {
152
+ const runtimeDir = defaultRuntimeDir()
153
+ if (!existsSync(runtimeDir)) {
154
+ mkdirSync(runtimeDir, { recursive: true })
155
+ if (platform !== "win32") {
156
+ chmodSync(runtimeDir, 0o700)
157
+ }
158
+ }
159
+ const dataDir = defaultDataDir()
160
+ if (!existsSync(dataDir)) {
161
+ mkdirSync(dataDir, { recursive: true })
162
+ if (platform !== "win32") {
163
+ chmodSync(dataDir, 0o700)
164
+ }
165
+ }
166
+ const runtimeStateFile = join(dataDir, RUNTIME_STATE_FILE)
167
+ return {
168
+ runtimeDir,
169
+ pidFile: join(runtimeDir, `${DAEMON_NAME}.pid`),
170
+ controlSocket: resolveControlSocket(runtimeDir),
171
+ childrenFile: join(runtimeDir, `${DAEMON_NAME}.children.json`),
172
+ // ponytail: restart-survival — files that have to outlive a reboot
173
+ // live under dataDir (XDG_STATE_HOME). runtime-state.json is the live
174
+ // snapshot the daemon rehydrates from; flags.json is the user's
175
+ // restart-mode preferences (emulator vs real, remote, port); the
176
+ // config-path pointer lets the daemon re-load the same user config
177
+ // without requiring a fresh invocation. config and flags used to live
178
+ // in tmpfs — they now persist.
179
+ dataDir,
180
+ configPathFile: join(dataDir, `${DAEMON_NAME}.config`),
181
+ flagsFile: join(dataDir, `${DAEMON_NAME}.flags.json`),
182
+ runtimeStateFile,
183
+ stateFile: runtimeStateFile,
184
+ }
185
+ }
186
+
187
+ export interface StartLock {
188
+ readonly release: () => void
189
+ }
190
+
191
+ export const acquireStartLock = (
192
+ paths = resolveDaemonPaths(),
193
+ ): StartLock | null => {
194
+ const lockFile = `${paths.pidFile}.lock`
195
+ let fd: number
196
+ try {
197
+ fd = openSync(lockFile, "wx", 0o600)
198
+ } catch (err) {
199
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err
200
+ // ponytail: lock already exists. Two ways out:
201
+ // 1. The PID inside it is dead (previous daemon crashed or was
202
+ // SIGKILL'd without releasing). Clear and re-acquire immediately.
203
+ // 2. The lock is stale by mtime (>60s) — same outcome, defensive
204
+ // fallback if the PID is unreadable or the live-check is unsure.
205
+ // Either case unblocks a fast retry that would otherwise have to wait
206
+ // out the full window. Without this, the user's exact symptom
207
+ // ("another start is already in progress") recurs after every daemon
208
+ // crash during a session of repeated restarts.
209
+ const holderPid = readLockHolderPid(lockFile)
210
+ const holderDead = holderPid !== null && !isRunning(holderPid)
211
+ const ageMs = safeStatAgeMs(lockFile)
212
+ if (holderDead || (ageMs !== null && ageMs > 60_000)) {
213
+ try {
214
+ unlinkSync(lockFile)
215
+ fd = openSync(lockFile, "wx", 0o600)
216
+ } catch {
217
+ return null
218
+ }
219
+ } else {
220
+ return null
221
+ }
222
+ }
223
+ if (fd === undefined!) return null
224
+ writeFileSync(fd, `${process.pid}\n`, { encoding: "utf8" })
225
+ return {
226
+ release: () => {
227
+ try {
228
+ closeSync(fd!)
229
+ unlinkSync(lockFile)
230
+ } catch {
231
+ // ignore
232
+ }
233
+ },
234
+ }
235
+ }
236
+
237
+ const readLockHolderPid = (lockFile: string): number | null => {
238
+ try {
239
+ const raw = readFileSync(lockFile, "utf8").trim()
240
+ const pid = Number.parseInt(raw, 10)
241
+ return Number.isFinite(pid) && pid > 0 ? pid : null
242
+ } catch {
243
+ return null
244
+ }
245
+ }
246
+
247
+ const safeStatAgeMs = (lockFile: string): number | null => {
248
+ try {
249
+ return Date.now() - statSync(lockFile).mtimeMs
250
+ } catch {
251
+ return null
252
+ }
253
+ }
254
+
255
+ export const removeStartLock = (paths = resolveDaemonPaths()): void => {
256
+ const lockFile = `${paths.pidFile}.lock`
257
+ if (existsSync(lockFile)) {
258
+ try {
259
+ unlinkSync(lockFile)
260
+ } catch {
261
+ // ignore — best-effort cleanup of an orphaned lock from a dead daemon
262
+ }
263
+ }
264
+ }
265
+
266
+ export const readPid = (paths = resolveDaemonPaths()): number | null => {
267
+ if (!existsSync(paths.pidFile)) return null
268
+ const raw = readFileSync(paths.pidFile, "utf8").trim()
269
+ const pid = Number.parseInt(raw, 10)
270
+ return Number.isFinite(pid) && pid > 0 ? pid : null
271
+ }
272
+
273
+ export const writePid = (pid: number, paths = resolveDaemonPaths()): void => {
274
+ writeAtomic(paths.pidFile, `${pid}\n`, 0o600)
275
+ }
276
+
277
+ export const removePidFile = (paths = resolveDaemonPaths()): void => {
278
+ if (existsSync(paths.pidFile)) unlinkSync(paths.pidFile)
279
+ }
280
+
281
+ /**
282
+ * Liveness, and nothing else: does this pid exist and can we signal it?
283
+ *
284
+ * ponytail: this used to additionally require the target's cmdline to contain
285
+ * a per-daemon sentinel, which is a test it could never pass. The sentinel was
286
+ * written only into the daemon's OWN environment, never into its argv, and the
287
+ * daemon rewrites its process title to `sirenodeck:dm` — so on macOS
288
+ * `ps -o command=` returns that title and on Linux the argv never carried the
289
+ * token either. Any caller that had inherited SIRENO_DAEMON_SENTINEL from the
290
+ * daemon therefore saw a live daemon as dead, and the damage was not cosmetic:
291
+ * `stopExisting` took its "stale pid file" branch, deleted the file and
292
+ * returned WITHOUT killing the daemon, leaving start free to boot a second one
293
+ * alongside the first. Both then ran, fighting over the same ports.
294
+ *
295
+ * Liveness and identity are separate questions. This answers the first; the
296
+ * `isOurDaemon` fingerprint in port-identity answers the second; and the
297
+ * single-instance lock, not a pid comparison, is what actually keeps two
298
+ * daemons apart.
299
+ */
300
+ export const isRunning = (pid: number): boolean => {
301
+ if (pid <= 0) return false
302
+ try {
303
+ process.kill(pid, 0)
304
+ return true
305
+ } catch (error) {
306
+ // EPERM means the pid exists but belongs to another user — still running.
307
+ return (error as NodeJS.ErrnoException).code === "EPERM"
308
+ }
309
+ }
310
+
311
+ export const generateToken = (): string => randomBytes(32).toString("base64url")
312
+
313
+ export interface ChildrenState {
314
+ pids: number[]
315
+ }
316
+
317
+ export const readChildren = (
318
+ paths = resolveDaemonPaths(),
319
+ ): ChildrenState | null => {
320
+ if (!existsSync(paths.childrenFile)) return null
321
+ try {
322
+ const raw = readFileSync(paths.childrenFile, "utf8")
323
+ const parsed = JSON.parse(raw) as unknown
324
+ if (
325
+ parsed !== null &&
326
+ typeof parsed === "object" &&
327
+ "pids" in parsed &&
328
+ Array.isArray((parsed as { pids: unknown }).pids)
329
+ ) {
330
+ const pids = (parsed as { pids: unknown[] }).pids
331
+ .map((p) => Number.parseInt(String(p), 10))
332
+ .filter((p) => Number.isFinite(p) && p > 0)
333
+ return { pids }
334
+ }
335
+ } catch {
336
+ return null
337
+ }
338
+ return null
339
+ }
340
+
341
+ export const writeChildren = (
342
+ state: ChildrenState,
343
+ paths = resolveDaemonPaths(),
344
+ ): void => {
345
+ writeAtomic(paths.childrenFile, JSON.stringify(state), 0o600)
346
+ }
347
+
348
+ export const removeChildrenFile = (paths = resolveDaemonPaths()): void => {
349
+ if (existsSync(paths.childrenFile)) unlinkSync(paths.childrenFile)
350
+ }
351
+
352
+ export const readConfigPath = (paths = resolveDaemonPaths()): string | null => {
353
+ if (!existsSync(paths.configPathFile)) return null
354
+ const raw = readFileSync(paths.configPathFile, "utf8").trim()
355
+ return raw.length > 0 ? raw : null
356
+ }
357
+
358
+ export const writeConfigPath = (
359
+ configPath: string,
360
+ paths = resolveDaemonPaths(),
361
+ ): void => {
362
+ writeAtomic(paths.configPathFile, `${configPath}\n`, 0o600)
363
+ }
364
+
365
+ export const removeConfigPathFile = (paths = resolveDaemonPaths()): void => {
366
+ if (existsSync(paths.configPathFile)) unlinkSync(paths.configPathFile)
367
+ }
368
+
369
+ export const readFlags = (
370
+ paths = resolveDaemonPaths(),
371
+ ): RuntimeFlags | null => {
372
+ if (!existsSync(paths.flagsFile)) return null
373
+ try {
374
+ const raw = readFileSync(paths.flagsFile, "utf8")
375
+ const parsed = JSON.parse(raw) as Partial<RuntimeFlags>
376
+ if (
377
+ typeof parsed.emulator !== "boolean" ||
378
+ typeof parsed.httpPort !== "number"
379
+ ) {
380
+ return null
381
+ }
382
+ return {
383
+ emulator: parsed.emulator,
384
+ httpPort: parsed.httpPort,
385
+ ...(typeof parsed.remote === "boolean" ? { remote: parsed.remote } : {}),
386
+ ...(typeof parsed.deviceModel === "string"
387
+ ? { deviceModel: parsed.deviceModel }
388
+ : {}),
389
+ ...(typeof parsed.port === "number" ? { port: parsed.port } : {}),
390
+ }
391
+ } catch {
392
+ return null
393
+ }
394
+ }
395
+
396
+ export const writeFlags = (
397
+ flags: RuntimeFlags,
398
+ paths = resolveDaemonPaths(),
399
+ ): void => {
400
+ writeAtomic(paths.flagsFile, JSON.stringify(flags), 0o600)
401
+ }
402
+
403
+ export const removeFlagsFile = (paths = resolveDaemonPaths()): void => {
404
+ if (existsSync(paths.flagsFile)) unlinkSync(paths.flagsFile)
405
+ }
406
+
407
+ export interface RuntimeState {
408
+ readonly configUiUrl: string
409
+ readonly wsUrl: string
410
+ readonly frontendUrl: string
411
+ readonly lanHost: string
412
+ readonly addresses: ReadonlyArray<string>
413
+ readonly emulatorMode: boolean
414
+ readonly remote: boolean
415
+ readonly startedAt: number
416
+ readonly theme?: string
417
+ }
418
+
419
+ export const RUNTIME_STATE_FILE = "runtime-state.json"
420
+
421
+ export const writeRuntimeState = (
422
+ state: RuntimeState,
423
+ paths = resolveDaemonPaths(),
424
+ ): void => {
425
+ writeAtomic(paths.runtimeStateFile, JSON.stringify(state, null, 2), 0o600)
426
+ }
427
+
428
+ export const readRuntimeState = (
429
+ paths = resolveDaemonPaths(),
430
+ ): RuntimeState | null => {
431
+ const filePath = paths.runtimeStateFile
432
+ if (!existsSync(filePath)) return null
433
+ try {
434
+ const raw = readFileSync(filePath, "utf8")
435
+ const parsed = JSON.parse(raw) as unknown
436
+ if (
437
+ parsed !== null &&
438
+ typeof parsed === "object" &&
439
+ typeof (parsed as RuntimeState).configUiUrl === "string" &&
440
+ typeof (parsed as RuntimeState).wsUrl === "string" &&
441
+ typeof (parsed as RuntimeState).frontendUrl === "string" &&
442
+ typeof (parsed as RuntimeState).lanHost === "string" &&
443
+ Array.isArray((parsed as RuntimeState).addresses) &&
444
+ typeof (parsed as RuntimeState).emulatorMode === "boolean" &&
445
+ typeof (parsed as RuntimeState).remote === "boolean"
446
+ ) {
447
+ const p = parsed as RuntimeState
448
+ return {
449
+ configUiUrl: p.configUiUrl,
450
+ wsUrl: p.wsUrl,
451
+ frontendUrl: p.frontendUrl,
452
+ lanHost: p.lanHost,
453
+ addresses: p.addresses,
454
+ emulatorMode: p.emulatorMode,
455
+ remote: p.remote,
456
+ startedAt: typeof p.startedAt === "number" ? p.startedAt : Date.now(),
457
+ theme: typeof p.theme === "string" ? p.theme : "default",
458
+ }
459
+ }
460
+ } catch {
461
+ return null
462
+ }
463
+ return null
464
+ }
465
+
466
+ export const removeRuntimeStateFile = (paths = resolveDaemonPaths()): void => {
467
+ if (existsSync(paths.runtimeStateFile)) {
468
+ unlinkSync(paths.runtimeStateFile)
469
+ }
470
+ }
471
+
472
+ // ponytail: child tracking is best-effort — orphans from a hard kill are reaped
473
+ // ponytail: terminateChildren first tries the pid as a process (the child
474
+ // itself) and then falls back to the pid as a NEGATIVE pid (process group
475
+ // leader). When vite is spawned with `detached: true` its pid IS the pgid,
476
+ // so kill(-pgid, SIGTERM) takes down the whole group in one shot — that's
477
+ // how we guarantee the frontend port doesn't stay bound by orphans after
478
+ // the daemon dies. Windows ignores the negative pid form silently.
479
+ const signalChildGroup = (pid: number, signal: NodeJS.Signals): void => {
480
+ try {
481
+ process.kill(pid, signal)
482
+ } catch {
483
+ // pid already dead or no permission — try as process group
484
+ }
485
+ try {
486
+ process.kill(-pid, signal)
487
+ } catch {
488
+ // not a pgid or already gone
489
+ }
490
+ }
491
+
492
+ export const terminateChildren = async ({
493
+ timeoutMs = 5000,
494
+ logger,
495
+ paths = resolveDaemonPaths(),
496
+ }: {
497
+ timeoutMs?: number
498
+ logger?: pino.Logger
499
+ paths?: DaemonPaths
500
+ }): Promise<void> => {
501
+ const state = readChildren(paths)
502
+ if (state === null || state.pids.length === 0) return
503
+
504
+ const alive = state.pids.filter((p) => isRunning(p))
505
+ if (alive.length === 0) {
506
+ writeChildren({ pids: [] }, paths)
507
+ return
508
+ }
509
+
510
+ // Phase 1: SIGTERM all
511
+ for (const pid of alive) {
512
+ signalChildGroup(pid, "SIGTERM")
513
+ logger?.debug({ pid }, "daemon: sent SIGTERM to child (+ group)")
514
+ }
515
+
516
+ // Wait for graceful shutdown
517
+ const deadline = Date.now() + timeoutMs
518
+ let remaining = alive.filter((p) => isRunning(p))
519
+ while (remaining.length > 0 && Date.now() < deadline) {
520
+ await new Promise((r) => setTimeout(r, 200))
521
+ remaining = remaining.filter((p) => isRunning(p))
522
+ }
523
+
524
+ // Phase 2: SIGKILL stragglers
525
+ for (const pid of remaining) {
526
+ signalChildGroup(pid, "SIGKILL")
527
+ logger?.debug({ pid }, "daemon: sent SIGKILL to child (+ group)")
528
+ }
529
+
530
+ // Prune dead from tracked list
531
+ const stillAlive = state.pids.filter((p) => isRunning(p))
532
+ writeChildren({ pids: stillAlive }, paths)
533
+ if (stillAlive.length < state.pids.length) {
534
+ logger?.info(
535
+ { killed: state.pids.length - stillAlive.length },
536
+ "daemon: cleaned up terminated children",
537
+ )
538
+ }
539
+ }
540
+
541
+ export const appendChild = (
542
+ pid: number,
543
+ paths = resolveDaemonPaths(),
544
+ ): void => {
545
+ const current = readChildren(paths) ?? { pids: [] }
546
+ if (current.pids.includes(pid)) return
547
+ writeChildren({ pids: [...current.pids, pid] }, paths)
548
+ }
549
+
550
+ export const removeChild = (
551
+ pid: number,
552
+ paths = resolveDaemonPaths(),
553
+ ): void => {
554
+ const current = readChildren(paths)
555
+ if (current === null) return
556
+ const next = current.pids.filter((p) => p !== pid)
557
+ if (next.length === current.pids.length) return
558
+ writeChildren({ pids: next }, paths)
559
+ }
560
+
561
+ export const pruneStaleChildren = (
562
+ paths = resolveDaemonPaths(),
563
+ logger?: pino.Logger,
564
+ ): number => {
565
+ const current = readChildren(paths)
566
+ if (current === null) return 0
567
+ const alive = current.pids.filter((p) => isRunning(p))
568
+ const pruned = current.pids.length - alive.length
569
+ if (pruned === 0) return 0
570
+ writeChildren({ pids: alive }, paths)
571
+ for (const dead of current.pids.filter((p) => !isRunning(p))) {
572
+ logger?.info({ pid: dead }, "daemon: pruned stale child pid")
573
+ }
574
+ return pruned
575
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ writeFileSync,
3
+ renameSync,
4
+ existsSync,
5
+ readFileSync,
6
+ unlinkSync,
7
+ mkdirSync,
8
+ } from "node:fs"
9
+ import { dirname } from "node:path"
10
+
11
+ export interface DeviceConfig {
12
+ readonly serial: string
13
+ readonly path: string
14
+ readonly model: string
15
+ }
16
+
17
+ export interface LoadDeviceConfigOptions {
18
+ readonly xdgConfigHome: string
19
+ }
20
+
21
+ export interface SaveDeviceConfigOptions extends LoadDeviceConfigOptions {
22
+ readonly config: DeviceConfig
23
+ }
24
+
25
+ const configPathFor = (xdgConfigHome: string): string =>
26
+ `${xdgConfigHome}/sirenodeck/device.json`
27
+
28
+ export const loadDeviceConfig = (
29
+ options: LoadDeviceConfigOptions,
30
+ ): DeviceConfig | null => {
31
+ const path = configPathFor(options.xdgConfigHome)
32
+ if (!existsSync(path)) return null
33
+ try {
34
+ const raw = readFileSync(path, "utf8")
35
+ const parsed = JSON.parse(raw) as Partial<DeviceConfig>
36
+ if (
37
+ typeof parsed.serial === "string" &&
38
+ typeof parsed.path === "string" &&
39
+ typeof parsed.model === "string"
40
+ ) {
41
+ return { serial: parsed.serial, path: parsed.path, model: parsed.model }
42
+ }
43
+ return null
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ export const saveDeviceConfig = (options: SaveDeviceConfigOptions): void => {
50
+ const path = configPathFor(options.xdgConfigHome)
51
+ mkdirSync(dirname(path), { recursive: true })
52
+ const tmp = `${path}.tmp`
53
+ writeFileSync(tmp, JSON.stringify(options.config, null, 2), "utf8")
54
+ renameSync(tmp, path)
55
+ if (existsSync(tmp)) unlinkSync(tmp)
56
+ }
@@ -0,0 +1,13 @@
1
+ export class NotImplementedError extends Error {
2
+ constructor(feature: string) {
3
+ super(`Not implemented: ${feature}`)
4
+ this.name = "NotImplementedError"
5
+ }
6
+ }
7
+
8
+ export class ConfigError extends Error {
9
+ constructor(message: string) {
10
+ super(message)
11
+ this.name = "ConfigError"
12
+ }
13
+ }