@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,114 @@
1
+ // ponytail: the CLI is the operator's interface to the daemon — not just
2
+ // a launcher. When the daemon starts, restarts, or stops, the CLI
3
+ // reads the daemon's `service.log` and surfaces any warn/error/fatal
4
+ // events inline so the operator doesn't have to grep a file. The
5
+ // daemon's pino JSON lines are easy to parse: each line is a
6
+ // self-describing object with `level` (30/40/50/60 = info/warn/error/fatal),
7
+ // `time` (ms epoch), `msg` (string), and `component` (optional).
8
+ //
9
+ // The CLI snapshots the file size at command start, then reads the
10
+ // appended bytes when it needs to surface events. Tail-like polling
11
+ // would work too, but we only need the events that accumulated during
12
+ // the operator's command — the operator can always run `p dev logs`
13
+ // for full live tail.
14
+ import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs"
15
+
16
+ import { resolveDaemonPaths } from "./daemon"
17
+
18
+ export type DaemonLogLevel = "warn" | "error" | "fatal"
19
+
20
+ export interface DaemonEvent {
21
+ readonly level: DaemonLogLevel
22
+ readonly component: string
23
+ readonly message: string
24
+ readonly time: number
25
+ }
26
+
27
+ interface RawLogEntry {
28
+ level?: unknown
29
+ time?: unknown
30
+ msg?: unknown
31
+ component?: unknown
32
+ }
33
+
34
+ const LEVEL_BY_NUMBER: Record<number, DaemonLogLevel> = {
35
+ 40: "warn",
36
+ 50: "error",
37
+ 60: "fatal",
38
+ }
39
+
40
+ const isLevel = (v: unknown): v is DaemonLogLevel =>
41
+ v === "warn" || v === "error" || v === "fatal"
42
+
43
+ const parseEntry = (line: string): DaemonEvent | null => {
44
+ let raw: RawLogEntry
45
+ try {
46
+ raw = JSON.parse(line) as RawLogEntry
47
+ } catch {
48
+ return null
49
+ }
50
+ const levelNum = typeof raw.level === "number" ? raw.level : NaN
51
+ const level = LEVEL_BY_NUMBER[levelNum]
52
+ if (level === undefined) return null
53
+ const message = typeof raw.msg === "string" ? raw.msg : ""
54
+ if (message.length === 0) return null
55
+ const component = typeof raw.component === "string" ? raw.component : ""
56
+ const time = typeof raw.time === "number" ? raw.time : Date.now()
57
+ return { level, component, message, time }
58
+ }
59
+
60
+ // ponytail: the only DaemonEvent surface the CLI needs for command-time
61
+ // inline output. If we ever want a structured `severity` field for
62
+ // formatting (e.g. red for error, yellow for warn), add it here.
63
+ const readRange = (logPath: string, start: number, end: number): string => {
64
+ const fd = openSync(logPath, "r")
65
+ try {
66
+ const buf = Buffer.alloc(end - start)
67
+ readSync(fd, buf, 0, buf.length, start)
68
+ return buf.toString("utf8")
69
+ } finally {
70
+ closeSync(fd)
71
+ }
72
+ }
73
+
74
+ // ponytail: same rotation-resilience as `tailFile` — if the file shrinks
75
+ // (truncated/rotated), reset the read offset to 0. We never see the
76
+ // pre-rotation content twice.
77
+ export const readDaemonEventsSince = (
78
+ logPath: string,
79
+ sinceBytes: number,
80
+ ): DaemonEvent[] => {
81
+ if (!existsSync(logPath)) return []
82
+ const size = statSync(logPath).size
83
+ if (size < sinceBytes) return [] // rotated
84
+ if (size === sinceBytes) return []
85
+ const text = readRange(logPath, sinceBytes, size)
86
+ const events: DaemonEvent[] = []
87
+ for (const raw of text.split("\n")) {
88
+ if (raw.length === 0) continue
89
+ const ev = parseEntry(raw)
90
+ if (ev !== null) events.push(ev)
91
+ }
92
+ return events
93
+ }
94
+
95
+ // ponytail: combined "size now" + "events since then" helper. The CLI
96
+ // records the size at command start with `snapshotDaemonLog()` and
97
+ // calls this at the end to surface what accumulated.
98
+ export const readDaemonEventsFromSnapshot = (
99
+ logPath: string,
100
+ snapshot: { readonly sinceBytes: number },
101
+ ): DaemonEvent[] => readDaemonEventsSince(logPath, snapshot.sinceBytes)
102
+
103
+ export interface DaemonLogSnapshot {
104
+ readonly sinceBytes: number
105
+ readonly takenAt: number
106
+ }
107
+
108
+ export const snapshotDaemonLog = (): DaemonLogSnapshot => {
109
+ const logPath = `${resolveDaemonPaths().runtimeDir}/service.log`
110
+ if (!existsSync(logPath)) {
111
+ return { sinceBytes: 0, takenAt: Date.now() }
112
+ }
113
+ return { sinceBytes: statSync(logPath).size, takenAt: Date.now() }
114
+ }
@@ -0,0 +1,103 @@
1
+ import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs"
2
+
3
+ import { formatHuman } from "./logger"
4
+
5
+ export interface TailFileOptions {
6
+ readonly logPath: string
7
+ readonly lines?: number
8
+ readonly follow?: boolean
9
+ readonly pollMs?: number
10
+ /** Prefix each line with when it was logged. On by default here. */
11
+ readonly timestamps?: boolean
12
+ }
13
+
14
+ export interface TailHandle {
15
+ stop: () => void
16
+ promise: Promise<void>
17
+ }
18
+
19
+ // ponytail: tail the last N lines, then poll for appended bytes. Simpler than
20
+ // fs.watch + inode tracking, and survives rotation (new size from 0).
21
+ export const tailFile = (opts: TailFileOptions): TailHandle => {
22
+ const {
23
+ logPath,
24
+ lines = 50,
25
+ follow = true,
26
+ pollMs = 250,
27
+ // ponytail: reading a log after the fact, "when did this happen" is the
28
+ // first question — unlike live startup output, which is read as it
29
+ // happens. Opposite default to the console formatter, deliberately.
30
+ timestamps = true,
31
+ } = opts
32
+ let stopped = false
33
+ let resolveDone!: () => void
34
+ const promise = new Promise<void>((resolve) => {
35
+ resolveDone = resolve
36
+ })
37
+
38
+ const emit = (line: string): void => {
39
+ const formatted = formatHuman(line, { timestamps })
40
+ const out = formatted !== null ? formatted : line
41
+ process.stdout.write(`${out}\n`)
42
+ }
43
+
44
+ const readRange = (start: number, end: number): string => {
45
+ const fd = openSync(logPath, "r")
46
+ try {
47
+ const buf = Buffer.alloc(end - start)
48
+ readSync(fd, buf, 0, buf.length, start)
49
+ return buf.toString("utf8")
50
+ } finally {
51
+ closeSync(fd)
52
+ }
53
+ }
54
+
55
+ const emitLast = (): number => {
56
+ if (!existsSync(logPath)) return 0
57
+ const text = readRange(0, statSync(logPath).size)
58
+ const all = text.split("\n").filter((l) => l.length > 0)
59
+ for (const line of all.slice(-lines)) emit(line)
60
+ return statSync(logPath).size
61
+ }
62
+
63
+ const stop = (): void => {
64
+ if (stopped) return
65
+ stopped = true
66
+ resolveDone()
67
+ }
68
+
69
+ let lastSize = emitLast()
70
+
71
+ if (follow) {
72
+ const interval = setInterval(() => {
73
+ if (stopped) {
74
+ clearInterval(interval)
75
+ return
76
+ }
77
+ if (!existsSync(logPath)) {
78
+ lastSize = 0
79
+ return
80
+ }
81
+ const currentSize = statSync(logPath).size
82
+ if (currentSize < lastSize) lastSize = 0 // rotation
83
+ if (currentSize === lastSize) return
84
+ const text = readRange(lastSize, currentSize)
85
+ for (const line of text.split("\n").filter((l) => l.length > 0))
86
+ emit(line)
87
+ lastSize = currentSize
88
+ }, pollMs)
89
+ process.once("SIGINT", () => {
90
+ clearInterval(interval)
91
+ process.stdout.write("\n")
92
+ stop()
93
+ })
94
+ } else {
95
+ stop()
96
+ }
97
+
98
+ return { stop, promise }
99
+ }
100
+
101
+ export const tailLogs = async (opts: TailFileOptions): Promise<void> => {
102
+ await tailFile(opts).promise
103
+ }
@@ -0,0 +1,417 @@
1
+ import { Writable } from "node:stream"
2
+
3
+ import { log } from "@clack/prompts"
4
+ import pino, {
5
+ type Logger,
6
+ type LoggerOptions,
7
+ type SerializedError,
8
+ } from "pino"
9
+
10
+ export interface CreateLoggerOptions {
11
+ level?: LoggerOptions["level"]
12
+ verbose?: boolean
13
+ json?: boolean
14
+ component?: string
15
+ }
16
+
17
+ const RESET = "\u001b[0m"
18
+ const DIM = "\u001b[2m"
19
+ const RED = "\u001b[31m"
20
+ const GREEN = "\u001b[32m"
21
+ const YELLOW = "\u001b[33m"
22
+ const BLUE = "\u001b[34m"
23
+ const MAGENTA = "\u001b[35m"
24
+ const CYAN = "\u001b[36m"
25
+ const GRAY = "\u001b[90m"
26
+
27
+ const LEVEL_COLOR: Record<number, string> = {
28
+ 10: GRAY,
29
+ 20: GRAY,
30
+ 30: CYAN,
31
+ 40: YELLOW,
32
+ 50: RED,
33
+ 60: RED,
34
+ }
35
+
36
+ const LEVEL_LABEL: Record<number, string> = {
37
+ 10: "TRACE",
38
+ 20: "DEBUG",
39
+ 30: "INFO",
40
+ 40: "WARN",
41
+ 50: "ERROR",
42
+ 60: "FATAL",
43
+ }
44
+
45
+ const colorize = (color: string, text: string): string =>
46
+ process.stdout.isTTY ? `${color}${text}${RESET}` : text
47
+
48
+ const COMPONENT_COLOR: Record<string, string> = {
49
+ runtime: CYAN,
50
+ methods: CYAN,
51
+ executor: CYAN,
52
+ "state-publisher": CYAN,
53
+ real: GREEN,
54
+ emulator: GREEN,
55
+ "ws-bridge": MAGENTA,
56
+ "addon-handler": MAGENTA,
57
+ "active-app": YELLOW,
58
+ "key-macro": YELLOW,
59
+ clipboard: YELLOW,
60
+ notification: YELLOW,
61
+ session: YELLOW,
62
+ "browser-renderer": BLUE,
63
+ "emulator-server": BLUE,
64
+ daemon: BLUE,
65
+ requirements: BLUE,
66
+ orchestrator: CYAN,
67
+ cli: GRAY,
68
+ }
69
+
70
+ const colorForComponent = (component: string): string =>
71
+ COMPONENT_COLOR[component] ?? GRAY
72
+
73
+ const CONTEXT_FIELDS = [
74
+ "frontendUrl",
75
+ "wsUrl",
76
+ "tool",
77
+ "sessionType",
78
+ "platform",
79
+ "executor",
80
+ "deckId",
81
+ "position",
82
+ "gesture",
83
+ "host",
84
+ "port",
85
+ "addon",
86
+ "source",
87
+ "message",
88
+ "addonDecks",
89
+ "userDecks",
90
+ "addonConfigEntries",
91
+ "icon",
92
+ "fullPath",
93
+ ] as const
94
+
95
+ const FORWARDED_CONTEXT_FIELDS = [
96
+ "component",
97
+ "deckId",
98
+ "position",
99
+ "addonName",
100
+ "gesture",
101
+ "keyIndex",
102
+ "reason",
103
+ ] as const
104
+
105
+ export interface FormatHumanOptions {
106
+ /**
107
+ * Prefix each line with the time it was logged.
108
+ *
109
+ * ponytail: off by default, because live startup output is read as it
110
+ * happens and the clock adds noise (see the comment below). `sirenodeck
111
+ * logs` is the opposite case — you are reading a record after the fact and
112
+ * "when did this happen" is the first question — so it turns this on.
113
+ */
114
+ readonly timestamps?: boolean
115
+ }
116
+
117
+ const formatLogTime = (value: unknown): string | null => {
118
+ // pino writes `time` as epoch millis; tolerate an ISO string too.
119
+ const ms =
120
+ typeof value === "number"
121
+ ? value
122
+ : typeof value === "string"
123
+ ? Date.parse(value)
124
+ : Number.NaN
125
+ if (!Number.isFinite(ms)) return null
126
+ const d = new Date(ms)
127
+ const pad = (n: number, w = 2): string => String(n).padStart(w, "0")
128
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
129
+ }
130
+
131
+ export const formatHuman = (
132
+ jsonLine: string,
133
+ options: FormatHumanOptions = {},
134
+ ): string | null => {
135
+ let entry: Record<string, unknown>
136
+ try {
137
+ entry = JSON.parse(jsonLine) as Record<string, unknown>
138
+ } catch {
139
+ return jsonLine
140
+ }
141
+ const levelNum = typeof entry["level"] === "number" ? entry["level"] : 30
142
+ const level = LEVEL_LABEL[levelNum] ?? "INFO"
143
+ const levelColor = LEVEL_COLOR[levelNum] ?? CYAN
144
+ const msg = typeof entry["msg"] === "string" ? entry["msg"] : ""
145
+ // ponytail: operator-facing logs drop the [HH:MM:SS] prefix and the
146
+ // continuation-line indent. Timestamps belong in service.log (where the
147
+ // structured `time` field stays intact for forensics); the terminal only
148
+ // needs enough info to read the line at a glance.
149
+ const head = colorize(levelColor, level.padEnd(5))
150
+ const time = options.timestamps === true ? formatLogTime(entry["time"]) : null
151
+ const timeTag = time !== null ? `${colorize(DIM, time)} ` : ""
152
+
153
+ const component =
154
+ typeof entry["component"] === "string" ? entry["component"] : ""
155
+ const componentTag =
156
+ component.length > 0
157
+ ? ` ${colorize(colorForComponent(component), `[${component}]`)}`
158
+ : ""
159
+
160
+ const ctxParts: string[] = []
161
+ for (const key of CONTEXT_FIELDS) {
162
+ const value = entry[key]
163
+ if (value === undefined || value === null) continue
164
+ const display = typeof value === "string" ? value : JSON.stringify(value)
165
+ if (display.length === 0) continue
166
+ ctxParts.push(`${colorize(DIM, `${key}:`)} ${display}`)
167
+ }
168
+ const err = entry["err"]
169
+ if (err !== null && typeof err === "object") {
170
+ const e = err as { type?: unknown; message?: unknown }
171
+ const errType = typeof e.type === "string" ? e.type : "Error"
172
+ const errMsg = typeof e.message === "string" ? e.message : ""
173
+ if (errMsg.length > 0) {
174
+ ctxParts.push(
175
+ `${colorize(DIM, "err:")} ${colorize(RED, `${errType}: ${errMsg}`)}`,
176
+ )
177
+ }
178
+ }
179
+ const ctxStr = ctxParts.length > 0 ? ` (${ctxParts.join(", ")})` : ""
180
+ return `${timeTag}${head}${componentTag} ${msg}${ctxStr}`
181
+ }
182
+
183
+ // ponytail: CLI startup logs (between intro/outro) should use the same
184
+ // clack tool as the banner: log.info / log.warn / log.error. Those tools
185
+ // render with a dim │ border + level-appropriate icon (● / ▲ / ■) and no
186
+ // "INFO"/"WARN" text label — matching the banner's visual language.
187
+ // HumanWritable maps pino levels to those tools so `logger.info(...)`
188
+ // calls made between intro() and outro() automatically get the border and
189
+ // icon. Outside a banner (or when stdout is not a TTY), fall back to the
190
+ // plain formatHuman line so piped output stays greppable.
191
+ class HumanWritable extends Writable {
192
+ override _write(
193
+ chunk: Buffer | string,
194
+ _encoding: BufferEncoding,
195
+ callback: (error?: Error | null) => void,
196
+ ): void {
197
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8")
198
+ const lines = text.split("\n")
199
+ const useClack = process.stdout.isTTY && !isServiceMode()
200
+ for (const line of lines) {
201
+ if (line.length === 0) continue
202
+ if (useClack) {
203
+ let entry: Record<string, unknown> | null = null
204
+ try {
205
+ entry = JSON.parse(line) as Record<string, unknown>
206
+ } catch {
207
+ // not JSON — fall back to plain
208
+ }
209
+ if (
210
+ entry !== null &&
211
+ typeof entry["level"] === "number" &&
212
+ typeof entry["msg"] === "string"
213
+ ) {
214
+ const levelNum = entry["level"] as number
215
+ const component =
216
+ typeof entry["component"] === "string"
217
+ ? ` [${entry["component"]}]`
218
+ : ""
219
+ // ponytail: keep CONTEXT_FIELDS + err inline so operators still see
220
+ // deckId / position / err details on the terminal, just without the
221
+ // "INFO"/"WARN" text label — the icon + color from log.* conveys level.
222
+ const ctxParts: string[] = []
223
+ for (const key of CONTEXT_FIELDS) {
224
+ const value = entry[key]
225
+ if (value === undefined || value === null) continue
226
+ const display =
227
+ typeof value === "string" ? value : JSON.stringify(value)
228
+ if (display.length === 0) continue
229
+ ctxParts.push(`${key}: ${display}`)
230
+ }
231
+ const err = entry["err"]
232
+ if (err !== null && typeof err === "object") {
233
+ const e = err as { type?: unknown; message?: unknown }
234
+ const errType = typeof e.type === "string" ? e.type : "Error"
235
+ const errMsg = typeof e.message === "string" ? e.message : ""
236
+ if (errMsg.length > 0) ctxParts.push(`err: ${errType}: ${errMsg}`)
237
+ }
238
+ const ctxStr = ctxParts.length > 0 ? ` (${ctxParts.join(", ")})` : ""
239
+ const msg = `${entry["msg"] as string}${component}${ctxStr}`
240
+ if (levelNum >= 50) log.error(msg)
241
+ else if (levelNum >= 40) log.warn(msg)
242
+ else log.info(msg)
243
+ continue
244
+ }
245
+ }
246
+ const formatted = formatHuman(line)
247
+ if (formatted !== null) {
248
+ process.stdout.write(`${formatted}\n`)
249
+ }
250
+ }
251
+ callback()
252
+ }
253
+ }
254
+
255
+ const errorSerializer = (
256
+ err: Error & { issues?: unknown; type?: string },
257
+ ): SerializedError => {
258
+ const out = {
259
+ type: err.name || "Error",
260
+ message: err.message,
261
+ stack: process.env["SIRENO_LOG_VERBOSE"] === "1" ? (err.stack ?? "") : "",
262
+ raw: err,
263
+ }
264
+ if (err.type !== undefined) (out as { type?: string }).type = err.type
265
+ if (err.issues !== undefined)
266
+ (out as { issues?: unknown }).issues = err.issues
267
+ return out as SerializedError
268
+ }
269
+
270
+ // ponytail: stray INVOCATION_ID + JOURNAL_STREAM in a user's shell (from a
271
+ // prior systemd context) makes human-format detection false-positive. Match
272
+ // the same heuristic as isUnderServiceManager: ppid === 1 is the only reliable
273
+ // signal that we're actually being supervised by init.
274
+ import { readFileSync } from "node:fs"
275
+
276
+ export const isOrphanedToInit = (): boolean => {
277
+ if (process.platform !== "linux") return true
278
+ try {
279
+ const stat = readFileSync("/proc/self/stat", "utf8")
280
+ const closeParen = stat.lastIndexOf(")")
281
+ const tail = stat.slice(closeParen + 2)
282
+ const ppid = Number.parseInt(tail.split(" ")[1] ?? "", 10)
283
+ if (!Number.isFinite(ppid)) return false
284
+ // ppid === 1 covers system-level services and true orphans; a systemd
285
+ // --user unit's parent is the user manager itself (not pid 1), and
286
+ // treating that as "not a service" made the daemon try `systemctl
287
+ // restart` on its own unit — a self-restart loop under Restart=.
288
+ // Interactive shells never have systemd as a direct parent, so the
289
+ // stray-env false-positive defense still holds.
290
+ if (ppid === 1) return true
291
+ return readFileSync(`/proc/${ppid}/comm`, "utf8").trim() === "systemd"
292
+ } catch {
293
+ return false
294
+ }
295
+ }
296
+
297
+ // ponytail: factory so tests can inject a mocked isOrphanedToInit without
298
+ // hitting ESM binding issues (the closure above captures the real one).
299
+ export interface ServiceModeDeps {
300
+ readonly isOrphaned: () => boolean
301
+ }
302
+
303
+ /** launchd job label, matching the plist written by service/install.ts. */
304
+ const SERVICE_LABEL = "sirenodeck"
305
+
306
+ export const createIsServiceMode = (deps: ServiceModeDeps) => (): boolean => {
307
+ if (process.env["SIRENO_DAEMON_CHILD"]) return true
308
+ if (process.env["LAUNCH_JOB_NAME"]) return deps.isOrphaned()
309
+ // Twin of createIsUnderServiceManager in cli/commands/spawn-daemon.ts —
310
+ // these must stay in lockstep. LAUNCH_JOB_NAME is the pre-10.10 variable;
311
+ // bootstrap/kickstart sets XPC_SERVICE_NAME to the job label instead.
312
+ // Matched against our own label because other launchd jobs (Terminal.app)
313
+ // export theirs into the shells they start.
314
+ const xpcName = process.env["XPC_SERVICE_NAME"]
315
+ if (xpcName !== undefined && xpcName.includes(SERVICE_LABEL)) return true
316
+ // INVOCATION_ID is systemd-scoped: user-unit children always have it, and
317
+ // it can only leak into a shell when that shell's ANCESTOR is a user unit
318
+ // (terminal-server etc). isOrphaned() requires our DIRECT parent to be
319
+ // systemd, which a shell never is — so the pair is false-positive-proof.
320
+ // JOURNAL_STREAM proved unreliable: user units don't always export it.
321
+ if (process.env["INVOCATION_ID"] && deps.isOrphaned()) return true
322
+ return false
323
+ }
324
+
325
+ export const isServiceMode = createIsServiceMode({
326
+ isOrphaned: isOrphanedToInit,
327
+ })
328
+
329
+ export const createLogger = (options: CreateLoggerOptions = {}): Logger => {
330
+ const { level, verbose = false, json = false, component } = options
331
+
332
+ if (verbose) process.env["SIRENO_LOG_VERBOSE"] = "1"
333
+ if (json) process.env["SIRENO_LOG_JSON"] = "1"
334
+
335
+ const wantRaw = json || isServiceMode() || !process.stdout.isTTY
336
+
337
+ const loggerOptions: LoggerOptions = {
338
+ name: "sirenodeck",
339
+ level: level ?? (verbose ? "debug" : "info"),
340
+ serializers: {
341
+ err: errorSerializer,
342
+ },
343
+ redact: {
344
+ paths: ["err.raw"],
345
+ censor: "[hidden]",
346
+ },
347
+ }
348
+
349
+ if (component === undefined) {
350
+ return makeLogger(loggerOptions, wantRaw)
351
+ }
352
+ return makeLogger(loggerOptions, wantRaw).child({ component })
353
+ }
354
+
355
+ const makeLogger = (loggerOptions: LoggerOptions, wantRaw: boolean): Logger => {
356
+ if (wantRaw) {
357
+ return pino(loggerOptions)
358
+ }
359
+
360
+ const dest = new HumanWritable()
361
+ const teeStream = {
362
+ write(chunk: string): void {
363
+ dest.write(chunk)
364
+ try {
365
+ const parsed = JSON.parse(chunk) as {
366
+ level?: number
367
+ time?: number
368
+ msg?: string
369
+ component?: unknown
370
+ deckId?: unknown
371
+ position?: unknown
372
+ addonName?: unknown
373
+ gesture?: unknown
374
+ keyIndex?: unknown
375
+ reason?: unknown
376
+ }
377
+ if (
378
+ typeof parsed.level === "number" &&
379
+ typeof parsed.time === "number" &&
380
+ typeof parsed.msg === "string"
381
+ ) {
382
+ const levelName = levelNameFromNumber(parsed.level)
383
+ if (levelName !== null) {
384
+ const payload: Record<string, unknown> = {
385
+ level: levelName,
386
+ msg: parsed.msg,
387
+ ts: parsed.time,
388
+ }
389
+ for (const key of FORWARDED_CONTEXT_FIELDS) {
390
+ const v = parsed[key]
391
+ if (v === undefined || v === null) continue
392
+ if (typeof v === "string" || typeof v === "number") {
393
+ payload[key] = v
394
+ }
395
+ }
396
+ ;(process.emit as unknown as (e: string, p: unknown) => void)(
397
+ "sireno:log",
398
+ payload,
399
+ )
400
+ }
401
+ }
402
+ } catch {
403
+ // ignore non-JSON log lines
404
+ }
405
+ },
406
+ }
407
+ return pino(loggerOptions, teeStream as unknown as pino.DestinationStream)
408
+ }
409
+
410
+ const levelNameFromNumber = (level: number): string | null => {
411
+ if (level >= 60) return "fatal"
412
+ if (level >= 50) return "error"
413
+ if (level >= 40) return "warn"
414
+ if (level >= 30) return "info"
415
+ if (level >= 20) return "debug"
416
+ return "trace"
417
+ }