@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,699 @@
1
+ import { execFile } from "node:child_process"
2
+ import { existsSync, readFileSync } from "node:fs"
3
+ import { promisify } from "node:util"
4
+
5
+ import type { CommandModule } from "yargs"
6
+
7
+ import type pino from "pino"
8
+
9
+ import { formatInstallInstructions, UDEV_RULES } from "@/device/linux-udev"
10
+
11
+ import {
12
+ type InstallStep,
13
+ type InstallStepResult,
14
+ type SudoRunResult,
15
+ type SystemReport,
16
+ ACCESSIBILITY_SETTINGS_URL,
17
+ DARWIN_ACCESSIBILITY_STEP_ID,
18
+ buildInstallPlan,
19
+ hasDarwinAccessibility,
20
+ openSettingsUrl,
21
+ capturePassword,
22
+ isSudoNopasswd,
23
+ needsConfigSeed,
24
+ probeAll,
25
+ runWithSudo,
26
+ seedDefaultConfig,
27
+ summarizeReport,
28
+ } from "@/system/setup-wizard"
29
+ import {
30
+ formatCapabilityPanel,
31
+ formatResultLine,
32
+ formatStepInstructions,
33
+ color,
34
+ } from "@/system/setup-wizard/format"
35
+ import {
36
+ cancel,
37
+ confirm,
38
+ intro,
39
+ isCancel,
40
+ log,
41
+ note,
42
+ outro,
43
+ spinner,
44
+ } from "@/cli/prompt"
45
+ import { collectBuiltinAddonChecks, runAddonChecks } from "@/addon/check-runner"
46
+ import { onboardCodingAgents } from "@/cli/coding-agents-onboarding"
47
+
48
+ const execFileAsync = promisify(execFile)
49
+
50
+ // ponytail: failed sudo runs return stderr in the result. The spinners
51
+ // used to swallow it ("udev write failed" with no hint why). Surface the
52
+ // last line of stderr so the user sees the real reason
53
+ // (Permission denied, command not found, "udevadm: not found", etc.).
54
+ const formatFailure = (result: SudoRunResult): string => {
55
+ const tail = (s: string): string => {
56
+ const line = s
57
+ .split("\n")
58
+ .map((l) => l.trimEnd())
59
+ .filter((l) => l.length > 0)
60
+ .at(-1)
61
+ return line ?? ""
62
+ }
63
+ const pieces = [
64
+ `exit ${result.exitCode}`,
65
+ result.neededPassword ? "sudo rejected password" : null,
66
+ tail(result.stderr) || null,
67
+ tail(result.stdout) || null,
68
+ ].filter((p): p is string => p !== null && p.length > 0)
69
+ return pieces.join(" — ")
70
+ }
71
+
72
+ const realExecutor = {
73
+ async run(command: string, args: ReadonlyArray<string>) {
74
+ try {
75
+ const result = await execFileAsync(command, [...args], {
76
+ timeout: 5_000,
77
+ })
78
+ return {
79
+ exitCode: 0,
80
+ stdout: result.stdout ?? "",
81
+ stderr: result.stderr ?? "",
82
+ }
83
+ } catch (err) {
84
+ const e = err as {
85
+ code?: number | string
86
+ stdout?: string
87
+ stderr?: string
88
+ }
89
+ return {
90
+ exitCode: typeof e.code === "number" ? e.code : 1,
91
+ stdout: e.stdout ?? "",
92
+ stderr: e.stderr ?? "",
93
+ }
94
+ }
95
+ },
96
+ }
97
+
98
+ const resolveXdgConfigHome = (options: SystemRequirementsOptions): string => {
99
+ const home = options.homeDir ?? process.env["HOME"] ?? ""
100
+ return (
101
+ options.xdgConfigHome ?? process.env["XDG_CONFIG_HOME"] ?? `${home}/.config`
102
+ )
103
+ }
104
+
105
+ const resolveHome = (options: SystemRequirementsOptions): string =>
106
+ options.homeDir ?? process.env["HOME"] ?? ""
107
+
108
+ export interface SystemRequirementsOptions {
109
+ readonly yes?: boolean
110
+ readonly nonInteractive?: boolean
111
+ readonly homeDir?: string
112
+ readonly xdgConfigHome?: string
113
+ readonly logger: pino.Logger
114
+ readonly configPath?: string
115
+ }
116
+
117
+ const SESSION_LABEL: Readonly<Record<string, string>> = {
118
+ wayland: "Wayland",
119
+ x11: "X11",
120
+ unknown: "unknown",
121
+ }
122
+
123
+ const pmLabel = (pm: string): string => {
124
+ switch (pm) {
125
+ case "apt":
126
+ return "apt"
127
+ case "dnf":
128
+ return "dnf"
129
+ case "pacman":
130
+ return "pacman (Arch)"
131
+ case "zypper":
132
+ return "zypper (openSUSE)"
133
+ case "brew":
134
+ return "Homebrew"
135
+ case "none":
136
+ return "none detected"
137
+ default:
138
+ return pm
139
+ }
140
+ }
141
+
142
+ const printProbeSummary = (report: SystemReport): void => {
143
+ // ponytail: pad labels to the longest one ("Stream Deck" → 11) plus ":" so
144
+ // every row's value starts at the same column. The icon is green/red
145
+ // depending on whether the row represents a "ready" state.
146
+ const rows: Array<{ label: string; value: string; ok: boolean }> = []
147
+ rows.push({
148
+ label: "Platform",
149
+ value: `${report.platform} · ${SESSION_LABEL[report.session] ?? "unknown"} session`,
150
+ ok: true,
151
+ })
152
+ rows.push({
153
+ label: "Packages",
154
+ value: pmLabel(report.packageManager),
155
+ ok: true,
156
+ })
157
+ if (report.platform === "linux") {
158
+ rows.push({
159
+ label: "udev",
160
+ value: report.udev.rulesInstalled
161
+ ? "installed"
162
+ : `missing at ${report.udev.rulesPath}`,
163
+ ok: report.udev.rulesInstalled,
164
+ })
165
+ rows.push({
166
+ label: "Stream Deck",
167
+ value: report.udev.streamDeckConnected
168
+ ? `connected (${report.udev.matchedProductIds.join(", ")})`
169
+ : "not detected",
170
+ ok: report.udev.streamDeckConnected,
171
+ })
172
+ }
173
+ rows.push({
174
+ label: "Config",
175
+ value: report.config.exists ? "present" : `missing — ${report.config.path}`,
176
+ ok: report.config.exists,
177
+ })
178
+
179
+ const labelWidth = Math.max(...rows.map((r) => r.label.length))
180
+ const lines = rows.map((r) => {
181
+ const padded = `${r.label}:`.padEnd(labelWidth + 2)
182
+ const icon = r.ok ? color.green("✓") : color.red("✗")
183
+ return `${icon} ${color.dim(padded)} ${r.value}`
184
+ })
185
+ note(lines.join("\n"), "Detected")
186
+ }
187
+
188
+ const reProbeCapability = async (
189
+ step: InstallStep,
190
+ deps: SystemRequirementsOptions,
191
+ ): Promise<boolean> => {
192
+ if (step.capability === "udev" || step.capability === "config") {
193
+ return false
194
+ }
195
+ const home = resolveHome(deps)
196
+ const xdg = resolveXdgConfigHome(deps)
197
+ const fresh = await probeAll({
198
+ platform: process.platform,
199
+ homeDir: home,
200
+ xdgConfigHome: xdg,
201
+ env: process.env,
202
+ executor: realExecutor,
203
+ fileExists: (p) => existsSync(p),
204
+ readFile: (p) => {
205
+ try {
206
+ return readFileSync(p, "utf8")
207
+ } catch {
208
+ return null
209
+ }
210
+ },
211
+ })
212
+ return fresh.capabilities[step.capability].available
213
+ }
214
+
215
+ // ponytail: the Accessibility grant is the one step no package manager can
216
+ // perform, so the wizard used to hand the user a System Settings breadcrumb and
217
+ // give up. macOS can deep-link straight to the pane, so offer to open it, then
218
+ // re-probe in place — the grant takes effect immediately for processes started
219
+ // afterwards, and re-probing here saves a whole extra wizard run.
220
+ const runDarwinAccessibilityStep = async (
221
+ yesBatched: boolean,
222
+ ): Promise<InstallStepResult> => {
223
+ if (await hasDarwinAccessibility(realExecutor)) return "installed"
224
+
225
+ if (!yesBatched) {
226
+ const shouldOpen = await confirm({
227
+ message: "Open System Settings at Privacy & Security → Accessibility?",
228
+ initialValue: true,
229
+ })
230
+ if (isCancel(shouldOpen) || !shouldOpen) return "manual"
231
+ }
232
+
233
+ const opened = await openSettingsUrl(realExecutor, ACCESSIBILITY_SETTINGS_URL)
234
+ if (!opened) {
235
+ log.warn("Could not open System Settings automatically.")
236
+ return "manual"
237
+ }
238
+ log.info(
239
+ `Enable ${color.green(appNeedingAccessibility())} in the list that just opened.`,
240
+ )
241
+
242
+ // Non-interactive runs cannot wait for a human to flip the switch, so report
243
+ // the step as manual rather than re-probing a grant that cannot have changed.
244
+ if (yesBatched) return "manual"
245
+
246
+ const granted = await confirm({
247
+ message: "Granted? (re-checks the permission)",
248
+ initialValue: true,
249
+ })
250
+ if (isCancel(granted) || !granted) return "manual"
251
+
252
+ if (await hasDarwinAccessibility(realExecutor)) return "installed"
253
+ log.warn(
254
+ "Still not granted for this process — macOS applies the grant to the app that launched sirenodeck, so restart your terminal and re-run.",
255
+ )
256
+ return "manual"
257
+ }
258
+
259
+ /**
260
+ * Best-effort name of the app the user must tick in the Accessibility list.
261
+ * macOS grants TCC to the bundle hosting the process, which is the terminal,
262
+ * not sirenodeck itself — naming it removes the most common misstep.
263
+ */
264
+ const appNeedingAccessibility = (): string =>
265
+ process.env["TERM_PROGRAM"] ?? "your terminal"
266
+
267
+ const runInstallStep = async (
268
+ step: InstallStep,
269
+ yesBatched: boolean,
270
+ logger: pino.Logger,
271
+ deps: SystemRequirementsOptions,
272
+ ): Promise<InstallStepResult> => {
273
+ if (step.id === DARWIN_ACCESSIBILITY_STEP_ID) {
274
+ return await runDarwinAccessibilityStep(yesBatched)
275
+ }
276
+ if (step.manualOnly) return "manual"
277
+ if (step.packages.length === 0 || step.packageManager === "none") {
278
+ return "manual"
279
+ }
280
+
281
+ // ponytail: re-probe before shell-out. If the binary is already on PATH
282
+ // (e.g. user installed manually between runs), skip the install step
283
+ // entirely — silently. The Capabilities panel already shows "Installed"
284
+ // for this capability, so any spinner or result line here would duplicate
285
+ // the same fact. The user explicitly asked for "only ask about the
286
+ // missing ones" — installed ones are silent.
287
+ const alreadyInstalled = await reProbeCapability(step, deps)
288
+ if (alreadyInstalled) {
289
+ return "installed"
290
+ }
291
+
292
+ const s = spinner()
293
+ s.start(`Checking ${step.packages.join(", ")}`)
294
+ s.stop(`${step.packages.join(", ")} not found — install needed`)
295
+
296
+ if (!yesBatched) {
297
+ const shouldRun = await confirm({
298
+ message: `Install ${step.title}?`,
299
+ initialValue: !step.sudo,
300
+ })
301
+ if (isCancel(shouldRun) || !shouldRun) return "skipped"
302
+ }
303
+
304
+ let command: string
305
+ let args: ReadonlyArray<string>
306
+ switch (step.packageManager) {
307
+ case "apt":
308
+ command = "apt-get"
309
+ args = ["install", "-y", ...step.packages]
310
+ break
311
+ case "dnf":
312
+ command = "dnf"
313
+ args = ["install", "-y", ...step.packages]
314
+ break
315
+ case "pacman":
316
+ command = "pacman"
317
+ args = ["-S", "--noconfirm", ...step.packages]
318
+ break
319
+ case "zypper":
320
+ command = "zypper"
321
+ args = ["install", "-y", ...step.packages]
322
+ break
323
+ case "brew":
324
+ command = "brew"
325
+ args = ["install", ...step.packages]
326
+ break
327
+ default:
328
+ return "manual"
329
+ }
330
+
331
+ const install = spinner()
332
+ install.start(`Installing ${step.packages.join(", ")}`)
333
+
334
+ if (!step.sudo) {
335
+ try {
336
+ await execFileAsync(command, [...args], { timeout: 120_000 })
337
+ install.stop(`${step.packages.join(", ")} installed`)
338
+ return "installed"
339
+ } catch (err) {
340
+ install.stop(`Install failed: ${(err as Error).message ?? "unknown"}`)
341
+ logger.warn({ err, step: step.id }, "non-sudo install failed")
342
+ return "failed"
343
+ }
344
+ }
345
+
346
+ const nopasswd = await isSudoNopasswd()
347
+ let password = ""
348
+ if (!nopasswd) {
349
+ if (!process.stdin.isTTY) {
350
+ install.stop("sudo password required but no TTY — skipping")
351
+ return "skipped"
352
+ }
353
+ try {
354
+ password = await capturePassword("[sudo] password: ")
355
+ } catch {
356
+ install.stop("password prompt cancelled")
357
+ return "skipped"
358
+ }
359
+ }
360
+ const result = await runWithSudo({
361
+ command,
362
+ args,
363
+ ...(password.length > 0 ? { stdinInput: password + "\n" } : {}),
364
+ timeoutMs: 120_000,
365
+ logger,
366
+ })
367
+ if (result.succeeded) {
368
+ install.stop(`${step.packages.join(", ")} installed`)
369
+ return "installed"
370
+ }
371
+ install.stop(`Install failed: ${formatFailure(result)}`)
372
+ return "failed"
373
+ }
374
+
375
+ const runUdevStep = async (
376
+ step: InstallStep,
377
+ yesBatched: boolean,
378
+ logger: pino.Logger,
379
+ ): Promise<InstallStepResult> => {
380
+ if (!yesBatched) {
381
+ const shouldRun = await confirm({
382
+ message: `Install udev rules to ${process.platform === "linux" ? "/etc/udev/rules.d/70-sirenodeck.rules" : "system location"}?`,
383
+ initialValue: false,
384
+ })
385
+ if (isCancel(shouldRun) || !shouldRun) return "skipped"
386
+ }
387
+ const nopasswd = await isSudoNopasswd()
388
+ let password = ""
389
+ if (!nopasswd) {
390
+ if (!process.stdin.isTTY) {
391
+ log.warn("sudo requires a password but no TTY — skipping udev")
392
+ return "skipped"
393
+ }
394
+ try {
395
+ password = await capturePassword("[sudo] password: ")
396
+ } catch {
397
+ return "skipped"
398
+ }
399
+ }
400
+ const write = spinner()
401
+ write.start("Installing udev rules")
402
+ // ponytail: write the rules via `sudo sh -c "cat > /path <<'EOF'..."` instead
403
+ // of `sudo -- tee`. `execFile`'s `input` option writes stdin then closes the
404
+ // pipe, so `tee` inherits a closed pipe and reads EOF (writes nothing). The
405
+ // heredoc is embedded in the command string — the child doesn't need stdin.
406
+ // `SIRENO_UDEV_EOF` is unique enough to not collide with UDEV_RULES content.
407
+ const writeCmd = `cat > /etc/udev/rules.d/70-sirenodeck.rules <<'SIRENO_UDEV_EOF'\n${UDEV_RULES}SIRENO_UDEV_EOF`
408
+ const w = await runWithSudo({
409
+ command: "sh",
410
+ args: ["-c", writeCmd],
411
+ ...(password.length > 0 ? { stdinInput: password + "\n" } : {}),
412
+ logger,
413
+ timeoutMs: 30_000,
414
+ })
415
+ if (!w.succeeded) {
416
+ write.stop(`udev write failed: ${formatFailure(w)}`)
417
+ note(formatInstallInstructions(), "You can install udev rules manually:")
418
+ return "failed"
419
+ }
420
+ // ponytail: still verify. A successful `cat` over `sudo` is the highest
421
+ // confidence signal short of running `udevadm test`, but the verify step
422
+ // below catches the rare case where sudo+cwd/permissions landed the file in
423
+ // the wrong place (e.g. EPERM on the parent dir).
424
+ const written = readFileSync("/etc/udev/rules.d/70-sirenodeck.rules", "utf8")
425
+ if (!written.includes("ATTRS{idVendor}")) {
426
+ write.stop("udev write failed: file is empty or incomplete")
427
+ logger.warn(
428
+ { writtenLen: written.length },
429
+ "udev: write succeeded but rules file does not contain expected content",
430
+ )
431
+ note(formatInstallInstructions(), "You can install udev rules manually:")
432
+ return "failed"
433
+ }
434
+ const reload = spinner()
435
+ reload.start("Reloading udev rules")
436
+ const reloadResult = await runWithSudo({
437
+ command: "sh",
438
+ args: ["-c", "udevadm control --reload-rules && udevadm trigger"],
439
+ ...(password.length > 0 ? { stdinInput: password + "\n" } : {}),
440
+ logger,
441
+ timeoutMs: 30_000,
442
+ })
443
+ if (!reloadResult.succeeded) {
444
+ reload.stop(`udev reload failed: ${formatFailure(reloadResult)}`)
445
+ note(formatInstallInstructions(), "You can install udev rules manually:")
446
+ return "failed"
447
+ }
448
+ reload.stop("udev rules installed")
449
+ return "installed"
450
+ }
451
+
452
+ const runConfigSeed = async (
453
+ report: SystemReport,
454
+ options: SystemRequirementsOptions,
455
+ logger: pino.Logger,
456
+ ): Promise<boolean> => {
457
+ if (report.config.exists) return false
458
+ if (options.nonInteractive) {
459
+ logger.warn(
460
+ { path: report.config.path },
461
+ "config missing and non-interactive mode — skipping seed",
462
+ )
463
+ return false
464
+ }
465
+ const shouldSeed = await confirm({
466
+ message: `Seed default config to ${report.config.path}?`,
467
+ initialValue: true,
468
+ })
469
+ if (isCancel(shouldSeed) || !shouldSeed) return false
470
+ const s = spinner()
471
+ s.start(`Seeding ${report.config.path}`)
472
+ try {
473
+ const result = seedDefaultConfig(report.config.path)
474
+ s.stop(`Seeded default config to ${result.targetPath}`)
475
+ logger.info({ path: result.targetPath }, "config seeded")
476
+ return true
477
+ } catch (err) {
478
+ s.stop(`Seed failed: ${(err as Error).message ?? "unknown"}`)
479
+ logger.warn({ err, path: report.config.path }, "config seed failed")
480
+ return false
481
+ }
482
+ }
483
+
484
+ export const systemRequirements = async (
485
+ options: SystemRequirementsOptions,
486
+ ): Promise<void> => {
487
+ const { logger } = options
488
+ const tty = process.stdin.isTTY
489
+ const yes = options.yes === true
490
+ const nonInteractive = options.nonInteractive || yes || !tty
491
+
492
+ const homeDir = resolveHome(options)
493
+ const xdgConfigHome = resolveXdgConfigHome(options)
494
+ const platform = process.platform
495
+
496
+ const probe = spinner()
497
+ probe.start("Probing system")
498
+
499
+ const report = await probeAll({
500
+ platform,
501
+ homeDir,
502
+ xdgConfigHome,
503
+ env: process.env,
504
+ executor: realExecutor,
505
+ fileExists: (p) => existsSync(p),
506
+ readFile: (p) => {
507
+ try {
508
+ return readFileSync(p, "utf8")
509
+ } catch {
510
+ return null
511
+ }
512
+ },
513
+ })
514
+
515
+ probe.stop("Probe complete")
516
+ intro("sirenodeck — system requirements")
517
+ printProbeSummary(report)
518
+
519
+ const summary = summarizeReport(report)
520
+
521
+ // ponytail: per-addon requirement checks (playerctl for media on Linux,
522
+ // osascript on macOS, etc.) — each addon owns its own list of OS-specific
523
+ // probes and renders pass/fail here. Run before the early-return paths so
524
+ // operators see addon status even when core system requirements are clean.
525
+ // Probe failures don't block the wizard — they're informational, matching
526
+ // the system capability check.
527
+ const addonCheckInputs = collectBuiltinAddonChecks()
528
+ if (addonCheckInputs.length > 0) {
529
+ const addonOutcomes = await runAddonChecks(addonCheckInputs)
530
+ if (addonOutcomes.length > 0) {
531
+ const lines = addonOutcomes.map((o) => {
532
+ const mark = o.available ? color.green("●") : color.red("○")
533
+ const status = o.available
534
+ ? color.green("available")
535
+ : color.red(
536
+ `missing${o.reason !== undefined ? ` — ${o.reason}` : ""}`,
537
+ )
538
+ return `${mark} ${o.addonName}/${o.checkName}: ${status}`
539
+ })
540
+ note(lines.join("\n"), "Addon checks")
541
+ }
542
+ }
543
+
544
+ await onboardCodingAgents(options.configPath ?? report.config.path, {
545
+ nonInteractive,
546
+ ...(options.yes === true ? { yes: true } : {}),
547
+ })
548
+
549
+ if (nonInteractive) {
550
+ if (summary.ok) {
551
+ log.success("All requirements present.")
552
+ return
553
+ }
554
+ log.warn("Missing pieces detected (non-interactive mode — exiting).")
555
+ log.warn(`Run \`sirenodeck system-requirements\` interactively to fix.`)
556
+ for (const line of summary.lines) {
557
+ process.stdout.write(` ${line}\n`)
558
+ }
559
+ process.exitCode = 1
560
+ return
561
+ }
562
+
563
+ const missingSteps = buildInstallPlan(report)
564
+ if (missingSteps.length === 0 && !needsConfigSeed(report)) {
565
+ outro("Everything looks good. Run `sirenodeck start` to begin.")
566
+ return
567
+ }
568
+
569
+ // ponytail: no leading " " here — `note()` already pads the content to the
570
+ // title column. Adding more shifts the list right of "Capabilities" and the
571
+ // rows wrap with the wrong indent on long lines.
572
+ note(formatCapabilityPanel(report.capabilities), "Capabilities")
573
+
574
+ const results: Record<string, InstallStepResult> = {}
575
+ let anyInstalled = false
576
+ for (const step of missingSteps) {
577
+ const result =
578
+ step.capability === "udev"
579
+ ? await runUdevStep(step, yes, logger)
580
+ : await runInstallStep(step, yes, logger, options)
581
+ results[step.id] = result
582
+ if (result === "installed") anyInstalled = true
583
+ // ponytail: the Capabilities panel already shows "Installed" once the
584
+ // install succeeds. Surface only failures/skipped/manual here — the
585
+ // user does not need a duplicate ✓ line per step.
586
+ if (result !== "installed") {
587
+ if (result === "manual" || result === "failed") {
588
+ log.warn(formatResultLine(step, result))
589
+ } else {
590
+ log.info(formatResultLine(step, result))
591
+ }
592
+ }
593
+ }
594
+
595
+ // ponytail: re-print the Capabilities panel after installs so the user can
596
+ // see what changed without scrolling up. Skipped when nothing installed —
597
+ // the original panel still reflects reality.
598
+ if (anyInstalled) {
599
+ const freshReport = await probeAll({
600
+ platform,
601
+ homeDir,
602
+ xdgConfigHome,
603
+ env: process.env,
604
+ executor: realExecutor,
605
+ fileExists: (p) => existsSync(p),
606
+ readFile: (p) => {
607
+ try {
608
+ return readFileSync(p, "utf8")
609
+ } catch {
610
+ return null
611
+ }
612
+ },
613
+ })
614
+ note(formatCapabilityPanel(freshReport.capabilities), "Capabilities")
615
+ }
616
+
617
+ if (needsConfigSeed(report)) {
618
+ await runConfigSeed(report, options, logger)
619
+ }
620
+
621
+ const anyFailed = Object.values(results).some((r) => r === "failed")
622
+ const anyManual = Object.values(results).some((r) => r === "manual")
623
+ if (anyFailed || anyManual) {
624
+ const lines: string[] = []
625
+ for (const step of missingSteps) {
626
+ if (results[step.id] === "manual" || results[step.id] === "failed") {
627
+ lines.push(`[${step.id}] ${step.title}`)
628
+ lines.push(` ${formatStepInstructions(step)}`)
629
+ }
630
+ }
631
+ if (lines.length > 0) note(lines.join("\n"), "Manual steps remaining")
632
+ cancel(
633
+ "Setup incomplete. Run `sirenodeck system-requirements` again after manual steps.",
634
+ )
635
+ process.exitCode = 1
636
+ return
637
+ }
638
+
639
+ outro("Setup complete. Run `sirenodeck start` to begin.")
640
+ }
641
+
642
+ interface SystemRequirementsArgs {
643
+ yes?: boolean
644
+ nonInteractive?: boolean
645
+ config?: string
646
+ homeDir?: string
647
+ xdgConfigHome?: string
648
+ }
649
+
650
+ export const systemRequirementsCommand: CommandModule<
651
+ object,
652
+ SystemRequirementsArgs
653
+ > = {
654
+ command: "system-requirements",
655
+ describe:
656
+ "Detect system capabilities (key macro, clipboard, notification, active-app) and optionally install missing pieces",
657
+ builder: (yargs) =>
658
+ yargs
659
+ .option("yes", {
660
+ alias: "y",
661
+ type: "boolean",
662
+ default: false,
663
+ description: "Accept all install prompts without confirmation",
664
+ })
665
+ .option("non-interactive", {
666
+ type: "boolean",
667
+ default: false,
668
+ description:
669
+ "Print a summary and exit non-zero if anything is missing (no prompts)",
670
+ })
671
+ .option("config", {
672
+ type: "string",
673
+ description: "Config file used to detect enabled builtin addons",
674
+ }),
675
+ handler: async (argv) => {
676
+ const { createLogger } = await import("@/util/logger")
677
+ const logger = createLogger({ verbose: false })
678
+ const options: SystemRequirementsOptions = {
679
+ logger,
680
+ ...(argv.yes === true ? { yes: true } : {}),
681
+ ...(argv.nonInteractive === true ? { nonInteractive: true } : {}),
682
+ ...(argv.config !== undefined ? { configPath: argv.config } : {}),
683
+ }
684
+ try {
685
+ await systemRequirements(options)
686
+ } catch (err) {
687
+ const e = err as { message?: string }
688
+ logger.error(
689
+ { err },
690
+ e && typeof e.message === "string"
691
+ ? e.message
692
+ : "system-requirements failed",
693
+ )
694
+ process.exitCode = 1
695
+ }
696
+ },
697
+ }
698
+
699
+ export default systemRequirements