@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,57 @@
1
+ import { homedir } from "node:os"
2
+
3
+ export const defaultResolveHome = (p: string): string => {
4
+ if (p === "~") return homedir()
5
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
6
+ return homedir() + p.slice(1)
7
+ }
8
+ return p
9
+ }
10
+
11
+ export const BUILTIN_CLI_ICONS: ReadonlySet<string> = new Set<string>([
12
+ "play",
13
+ "pause",
14
+ "next",
15
+ "previous",
16
+ "stop",
17
+ "settings",
18
+ "back",
19
+ "home",
20
+ "lock",
21
+ "unlock",
22
+ "wifi",
23
+ "wifi-off",
24
+ "battery-full",
25
+ "battery-half",
26
+ "battery-low",
27
+ "battery-charging",
28
+ "volume-high",
29
+ "volume-mid",
30
+ "volume-low",
31
+ "volume-mute",
32
+ "cpu",
33
+ "memory",
34
+ "clock",
35
+ "calendar",
36
+ "sun",
37
+ "moon",
38
+ "spotify",
39
+ "chrome",
40
+ "firefox",
41
+ "discord",
42
+ "slack",
43
+ "vscode",
44
+ "terminal",
45
+ "folder",
46
+ "file",
47
+ "power",
48
+ "refresh",
49
+ "search",
50
+ "star",
51
+ "heart",
52
+ "check",
53
+ "close",
54
+ "add",
55
+ "remove",
56
+ "menu",
57
+ ])
@@ -0,0 +1,54 @@
1
+ import type { RawButtonEntry, RawConfig, RawDeckDef } from "./schemas"
2
+
3
+ const buttonKey = (entry: RawButtonEntry, idx: number): string => {
4
+ if (typeof entry === "string") return `s:${idx}:${entry}`
5
+ return `o:${idx}:${JSON.stringify(entry)}`
6
+ }
7
+
8
+ const stableValue = (value: unknown): unknown => {
9
+ if (Array.isArray(value)) return value.map(stableValue)
10
+ if (typeof value !== "object" || value === null) return value
11
+ return Object.fromEntries(
12
+ Object.entries(value)
13
+ .sort(([a], [b]) => a.localeCompare(b))
14
+ .map(([key, entry]) => [key, stableValue(entry)]),
15
+ )
16
+ }
17
+
18
+ const sameButtons = (a: RawDeckDef, b: RawDeckDef): boolean => {
19
+ if (a.buttons.length !== b.buttons.length) return false
20
+ for (let i = 0; i < a.buttons.length; i += 1) {
21
+ if (buttonKey(a.buttons[i]!, i) !== buttonKey(b.buttons[i]!, i)) {
22
+ return false
23
+ }
24
+ }
25
+ return true
26
+ }
27
+
28
+ export const decksChanged = (prev: RawConfig, next: RawConfig): boolean => {
29
+ const prevKeys = Object.keys(prev.decks)
30
+ const nextKeys = Object.keys(next.decks)
31
+ if (prevKeys.length !== nextKeys.length) return true
32
+ for (const id of prevKeys) {
33
+ if (!Object.prototype.hasOwnProperty.call(next.decks, id)) return true
34
+ }
35
+ for (const id of nextKeys) {
36
+ const prevDeck = prev.decks[id]
37
+ const nextDeck = next.decks[id]
38
+ if (prevDeck === undefined || nextDeck === undefined) return true
39
+ if (
40
+ JSON.stringify(stableValue(prevDeck)) !==
41
+ JSON.stringify(stableValue(nextDeck))
42
+ )
43
+ return true
44
+ if (!sameButtons(prevDeck, nextDeck)) return true
45
+ }
46
+ return false
47
+ }
48
+
49
+ /** Returns true for every config change that can affect runtime/editor state. */
50
+ export const configChanged = (prev: RawConfig, next: RawConfig): boolean =>
51
+ JSON.stringify(stableValue(prev)) !== JSON.stringify(stableValue(next))
52
+
53
+ // Kept as the explicit old name used by the initial corrective tests.
54
+ export const onlyDecksChanged = decksChanged
@@ -0,0 +1,64 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, isAbsolute, resolve as resolvePath } from "node:path"
3
+
4
+ import { getOriginalCwd } from "@/cli/cwd"
5
+
6
+ export interface FindConfigOptions {
7
+ cwd?: string
8
+ explicitPath?: string
9
+ envVar?: string
10
+ homeDir: string
11
+ xdgConfigHome?: string
12
+ }
13
+
14
+ export const DEFAULT_CONFIG_FILENAME = "config.yml"
15
+ export const XDG_CONFIG_SUBDIR = "sirenodeck"
16
+
17
+ const resolvePath_ = (p: string, cwd: string): string =>
18
+ isAbsolute(p) ? p : resolvePath(cwd, p)
19
+
20
+ // ponytail: precedence is cli arg → ~/.config/sirenodeck/config.yml →
21
+ // ./config.yml (run folder / cwd). Parent-directory walk removed: the run
22
+ // folder is the canonical place for a project config, and silently walking
23
+ // up makes a "missing config" failure look like a "found some other config"
24
+ // one. The previous walk also kept selectors scripts out of the canoe.
25
+ export const findConfigPath = (options: FindConfigOptions): string | null => {
26
+ const cwd = options.cwd ?? getOriginalCwd()
27
+ if (options.explicitPath) {
28
+ const abs = resolvePath_(options.explicitPath, cwd)
29
+ return existsSync(abs) ? abs : null
30
+ }
31
+ if (options.envVar) {
32
+ const abs = resolvePath_(options.envVar, cwd)
33
+ if (existsSync(abs)) return abs
34
+ }
35
+ const xdgRoot =
36
+ options.xdgConfigHome ?? resolvePath(options.homeDir, ".config")
37
+ const xdgConfig = resolvePath(
38
+ xdgRoot,
39
+ XDG_CONFIG_SUBDIR,
40
+ DEFAULT_CONFIG_FILENAME,
41
+ )
42
+ if (existsSync(xdgConfig)) return xdgConfig
43
+ const cwdConfig = resolvePath(cwd, DEFAULT_CONFIG_FILENAME)
44
+ if (existsSync(cwdConfig)) return cwdConfig
45
+ return null
46
+ }
47
+
48
+ // ponytail: kept for the parent-walk test fixture — call sites should use
49
+ // findConfigPath. Removing the export would break the existing test, so
50
+ // the helper stays here as an explicit escape hatch with no callers.
51
+ export const walkUpForConfig = (
52
+ startDir: string,
53
+ maxDepth: number,
54
+ ): string | null => {
55
+ let current = startDir
56
+ for (let depth = 0; depth <= maxDepth; depth += 1) {
57
+ const candidate = resolvePath(current, DEFAULT_CONFIG_FILENAME)
58
+ if (existsSync(candidate)) return candidate
59
+ const parent = dirname(current)
60
+ if (parent === current) return null
61
+ current = parent
62
+ }
63
+ return null
64
+ }
@@ -0,0 +1,90 @@
1
+ import { isAbsolute, resolve as resolvePath } from "node:path"
2
+
3
+ export type IconSource =
4
+ | { kind: "path"; absolutePath: string }
5
+ | { kind: "cli-builtin"; id: string }
6
+ | { kind: "builtin-addon"; addonName: string; subPath: string }
7
+ | { kind: "addon"; addonName: string; subPath: string }
8
+
9
+ export interface ResolveIconRefContext {
10
+ configDir: string
11
+ resolveHome(p: string): string
12
+ builtinIconIds: ReadonlySet<string>
13
+ }
14
+
15
+ const CLI_BUILTIN_PREFIX = "icon://"
16
+ const BUILTIN_ADDON_PREFIX = "builtin://"
17
+ const ADDON_PREFIX = "addon://"
18
+
19
+ const startsWith = (s: string, prefix: string): boolean => s.startsWith(prefix)
20
+
21
+ export const isLocalIconPath = (ref: string): boolean => {
22
+ if (startsWith(ref, "./") || startsWith(ref, "../") || startsWith(ref, "/"))
23
+ return true
24
+ if (startsWith(ref, "~/") || startsWith(ref, "~\\")) return true
25
+ if (/[\\/]/.test(ref)) return true
26
+ return false
27
+ }
28
+
29
+ export const resolveIconRef = (
30
+ ref: string,
31
+ ctx: ResolveIconRefContext,
32
+ ): IconSource => {
33
+ if (startsWith(ref, CLI_BUILTIN_PREFIX)) {
34
+ const id = ref.slice(CLI_BUILTIN_PREFIX.length)
35
+ if (id.length === 0) {
36
+ throw new Error(`Invalid CLI-builtin icon ref: ${ref} (missing id)`)
37
+ }
38
+ if (!ctx.builtinIconIds.has(id)) {
39
+ throw new Error(`Unknown CLI-builtin icon: ${id}`)
40
+ }
41
+ return { kind: "cli-builtin", id }
42
+ }
43
+ if (startsWith(ref, BUILTIN_ADDON_PREFIX)) {
44
+ const rest = ref.slice(BUILTIN_ADDON_PREFIX.length)
45
+ const slash = rest.indexOf("/")
46
+ if (slash <= 0 || slash === rest.length - 1) {
47
+ throw new Error(`Invalid builtin:// addon ref: ${ref}`)
48
+ }
49
+ return {
50
+ kind: "builtin-addon",
51
+ addonName: rest.slice(0, slash),
52
+ subPath: rest.slice(slash + 1),
53
+ }
54
+ }
55
+ if (startsWith(ref, ADDON_PREFIX)) {
56
+ const rest = ref.slice(ADDON_PREFIX.length)
57
+ const slash = rest.indexOf("/")
58
+ if (slash <= 0 || slash === rest.length - 1) {
59
+ throw new Error(`Invalid addon:// ref: ${ref}`)
60
+ }
61
+ return {
62
+ kind: "addon",
63
+ addonName: rest.slice(0, slash),
64
+ subPath: rest.slice(slash + 1),
65
+ }
66
+ }
67
+ if (isLocalIconPath(ref)) {
68
+ const expanded = startsWith(ref, "~") ? ctx.resolveHome(ref) : ref
69
+ const absolutePath = isAbsolute(expanded)
70
+ ? expanded
71
+ : resolvePath(ctx.configDir, expanded)
72
+ return { kind: "path", absolutePath }
73
+ }
74
+ throw new Error(
75
+ `Unrecognized icon ref: ${ref} — must be a relative path, icon://<id>, builtin://<addon>/<path>, or addon://<addon>/<path>`,
76
+ )
77
+ }
78
+
79
+ export const iconSourceToString = (src: IconSource): string => {
80
+ switch (src.kind) {
81
+ case "path":
82
+ return src.absolutePath
83
+ case "cli-builtin":
84
+ return `icon://${src.id}`
85
+ case "builtin-addon":
86
+ return `builtin://${src.addonName}/${src.subPath}`
87
+ case "addon":
88
+ return `addon://${src.addonName}/${src.subPath}`
89
+ }
90
+ }
@@ -0,0 +1,202 @@
1
+ import { readFileSync, realpathSync } from "node:fs"
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ resolve as resolvePath,
7
+ } from "node:path"
8
+
9
+ export class IncludeResolutionError extends Error {
10
+ readonly issues: { message: string; path?: string }[]
11
+
12
+ constructor(
13
+ message: string,
14
+ issues: { message: string; path?: string }[] = [],
15
+ ) {
16
+ super(message)
17
+ this.name = "IncludeResolutionError"
18
+ this.issues = issues
19
+ }
20
+ }
21
+
22
+ const INCLUDE_RE = /^(\s*)(.*?)\s*!include\s+(\S+)(.*)$/
23
+
24
+ /**
25
+ * Resolves symlinks so two spellings of the same directory compare equal.
26
+ *
27
+ * ponytail: the containment check below is a plain string prefix, so a config
28
+ * reached through a symlinked parent failed it — the defining file had already
29
+ * been canonicalised while an absolute `!include` had not, and the two never
30
+ * shared a prefix. On macOS that is every config under a temp dir, since /var
31
+ * is a symlink to /private/var, but it applies to any symlinked config
32
+ * directory. Falls back to the literal path when the target does not exist yet
33
+ * (a missing include must be reported as missing, not as an escape attempt).
34
+ */
35
+ const canonical = (p: string): string => {
36
+ try {
37
+ return realpathSync.native(p)
38
+ } catch {
39
+ try {
40
+ return resolvePath(realpathSync.native(dirname(p)), basename(p))
41
+ } catch {
42
+ return p
43
+ }
44
+ }
45
+ }
46
+
47
+ const isUnder = (candidate: string, root: string): boolean =>
48
+ candidate.startsWith(root.endsWith("/") ? root : `${root}/`)
49
+
50
+ const resolveIncludePath = (
51
+ pathStr: string,
52
+ definingFilePath: string,
53
+ ): string => {
54
+ const lexicalRoot = resolvePath(dirname(definingFilePath))
55
+ const includePath = isAbsolute(pathStr)
56
+ ? pathStr
57
+ : resolvePath(lexicalRoot, pathStr)
58
+
59
+ // ponytail: containment is judged LEXICALLY, and that is the whole point.
60
+ // This check exists to stop `../../etc/passwd`, not to police where the
61
+ // user's own files live. Resolving symlinks first broke a perfectly ordinary
62
+ // setup — a `demos -> ../../repo/demos` symlink placed inside the config
63
+ // directory on purpose — by reporting the user's deliberate choice as a path
64
+ // traversal attempt and refusing to start the daemon.
65
+ //
66
+ // The canonical forms are still accepted, because the defining file may
67
+ // arrive already realpath'd while an absolute !include has not (on macOS
68
+ // /var is a symlink to /private/var, so this is every config under a temp
69
+ // dir). Any of the spellings matching is enough; `../` escapes match none.
70
+ const canonicalRoot = canonical(lexicalRoot)
71
+ const contained =
72
+ isUnder(includePath, lexicalRoot) ||
73
+ isUnder(includePath, canonicalRoot) ||
74
+ isUnder(canonical(includePath), canonicalRoot)
75
+
76
+ if (!contained) {
77
+ throw new IncludeResolutionError(
78
+ `!include path escapes config directory: ${includePath} (from ${definingFilePath})`,
79
+ [
80
+ {
81
+ message: `path traversal blocked: ${pathStr}`,
82
+ path: definingFilePath,
83
+ },
84
+ ],
85
+ )
86
+ }
87
+ return includePath
88
+ }
89
+
90
+ const processLine = (
91
+ line: string,
92
+ definingFilePath: string,
93
+ visited: Set<string>,
94
+ replacements: ReadonlyMap<string, string>,
95
+ ): string[] => {
96
+ const match = line.match(INCLUDE_RE)
97
+ if (match === null) return [line]
98
+ const indent = match[1] ?? ""
99
+ const before = match[2] ?? ""
100
+ const pathStr = match[3] ?? ""
101
+ if (pathStr.length === 0) {
102
+ throw new IncludeResolutionError(
103
+ `Empty !include path at ${definingFilePath}`,
104
+ [{ message: "empty path after !include", path: definingFilePath }],
105
+ )
106
+ }
107
+ // ponytail: !include must stay within the defining file's directory.
108
+ const includePath = resolveIncludePath(pathStr, definingFilePath)
109
+ if (visited.has(includePath)) {
110
+ const cycle = [...visited, includePath].join(" -> ")
111
+ throw new IncludeResolutionError(`Circular include detected: ${cycle}`, [
112
+ { message: `cycle: ${cycle}`, path: definingFilePath },
113
+ ])
114
+ }
115
+ let raw = replacements.get(includePath)
116
+ if (raw === undefined) {
117
+ try {
118
+ raw = readFileSync(includePath, "utf8")
119
+ } catch (err) {
120
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
121
+ throw new IncludeResolutionError(
122
+ `Included file not found: ${includePath} (from ${definingFilePath})`,
123
+ [{ message: `not found: ${includePath}`, path: definingFilePath }],
124
+ )
125
+ }
126
+ throw err
127
+ }
128
+ }
129
+ const next = new Set(visited)
130
+ next.add(includePath)
131
+ const inlined = inlineIncludes(raw, includePath, next, replacements)
132
+ if (before.trim().length === 0) {
133
+ return inlined.split("\n")
134
+ }
135
+ if (before.trim().endsWith(":")) {
136
+ const childIndent = `${indent} `
137
+ const childContent = inlined
138
+ .split("\n")
139
+ .map((l) => (l.length === 0 ? l : childIndent + l))
140
+ .join("\n")
141
+ return [`${indent}${before}`, childContent]
142
+ }
143
+ throw new IncludeResolutionError(
144
+ `Unsupported !include position in line "${line}" (from ${definingFilePath})`,
145
+ [{ message: `unsupported !include position`, path: definingFilePath }],
146
+ )
147
+ }
148
+
149
+ const inlineIncludes = (
150
+ text: string,
151
+ definingFilePath: string,
152
+ visited: Set<string>,
153
+ replacements: ReadonlyMap<string, string>,
154
+ ): string => {
155
+ const lines = text.split("\n")
156
+ const out: string[] = []
157
+ for (const line of lines) {
158
+ out.push(...processLine(line, definingFilePath, visited, replacements))
159
+ }
160
+ return out.join("\n")
161
+ }
162
+
163
+ export const resolveIncludes = (
164
+ text: string,
165
+ definingFilePath: string,
166
+ replacements: ReadonlyMap<string, string> = new Map(),
167
+ ): string =>
168
+ inlineIncludes(
169
+ text,
170
+ definingFilePath,
171
+ new Set([definingFilePath]),
172
+ replacements,
173
+ )
174
+
175
+ const discoverFromFile = (filePath: string, visited: Set<string>): string[] => {
176
+ const canonicalPath = realpathSync(filePath)
177
+ if (visited.has(canonicalPath)) {
178
+ throw new IncludeResolutionError(
179
+ `Circular include detected: ${canonicalPath}`,
180
+ )
181
+ }
182
+ const next = new Set(visited)
183
+ next.add(canonicalPath)
184
+ const sources = [canonicalPath]
185
+ for (const line of readFileSync(canonicalPath, "utf8").split("\n")) {
186
+ const match = line.match(INCLUDE_RE)
187
+ if (match === null) continue
188
+ const pathStr = match[3]
189
+ if (pathStr === undefined || pathStr.length === 0) {
190
+ throw new IncludeResolutionError(
191
+ `Empty !include path at ${canonicalPath}`,
192
+ )
193
+ }
194
+ const includePath = resolveIncludePath(pathStr, canonicalPath)
195
+ sources.push(...discoverFromFile(includePath, next))
196
+ }
197
+ return [...new Set(sources)]
198
+ }
199
+
200
+ /** Returns the canonical files reachable through !include, including root. */
201
+ export const discoverIncludeGraph = (rootPath: string): string[] =>
202
+ discoverFromFile(rootPath, new Set())
@@ -0,0 +1,9 @@
1
+ export * from "./builtin-icons"
2
+ export * from "./discovery"
3
+ export * from "./icon-resolver"
4
+ export * from "./include-resolver"
5
+ export * from "./loader"
6
+ export * from "./mutation"
7
+ export * from "./reference-expander"
8
+ export * from "./schemas"
9
+ export * from "./validation"
@@ -0,0 +1,187 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { dirname, isAbsolute, resolve as resolvePath } from "node:path"
3
+
4
+ import { parseDocument, YAMLParseError } from "yaml"
5
+
6
+ import { getOriginalCwd } from "@/cli/cwd"
7
+
8
+ import { IncludeResolutionError, resolveIncludes } from "./include-resolver"
9
+ import { RawConfigSchema, type RawConfig } from "./schemas"
10
+ import { expandButtonReferences } from "./reference-expander"
11
+
12
+ export interface LoadConfigOptions {
13
+ configPath: string
14
+ }
15
+
16
+ export interface LoadConfigResult {
17
+ config: RawConfig
18
+ configDir: string
19
+ }
20
+
21
+ export interface LineLocation {
22
+ line: number
23
+ col: number
24
+ }
25
+
26
+ export interface ConfigError {
27
+ message: string
28
+ location?: LineLocation
29
+ path?: string
30
+ }
31
+
32
+ export class ConfigLoadError extends Error {
33
+ readonly issues: ConfigError[]
34
+
35
+ constructor(message: string, issues: ConfigError[] = []) {
36
+ super(message)
37
+ this.name = "ConfigLoadError"
38
+ this.issues = issues
39
+ }
40
+ }
41
+
42
+ const formatLineCol = (loc: LineLocation): string =>
43
+ `line ${loc.line + 1}, col ${loc.col + 1}`
44
+
45
+ const convertYamlErrors = (err: unknown): ConfigError[] => {
46
+ if (err instanceof YAMLParseError) {
47
+ const linePos = err.linePos?.[0]
48
+ return [
49
+ {
50
+ message: err.message,
51
+ ...(linePos
52
+ ? { location: { line: linePos.line, col: linePos.col } }
53
+ : {}),
54
+ },
55
+ ]
56
+ }
57
+ if (Array.isArray(err)) {
58
+ return err.map((e) => {
59
+ const linePos = e.linePos?.[0]
60
+ return {
61
+ message: e.message ?? String(e),
62
+ ...(linePos
63
+ ? { location: { line: linePos.line, col: linePos.col } }
64
+ : {}),
65
+ } satisfies ConfigError
66
+ })
67
+ }
68
+ return [{ message: err instanceof Error ? err.message : String(err) }]
69
+ }
70
+
71
+ export const loadConfigFile = (configPath: string): unknown => {
72
+ const absolutePath = isAbsolute(configPath)
73
+ ? configPath
74
+ : resolvePath(getOriginalCwd(), configPath)
75
+ let raw: string
76
+ try {
77
+ raw = readFileSync(absolutePath, "utf8")
78
+ } catch (err) {
79
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
80
+ throw new ConfigLoadError(`Config file not found: ${absolutePath}`)
81
+ }
82
+ throw err
83
+ }
84
+ const doc = parseDocument(raw, { keepSourceTokens: true })
85
+ const errors = doc.errors
86
+ // ponytail: textual include inlining — pre-process before yaml parses so
87
+ // included content participates in normal yaml merge semantics. Cycle
88
+ // detection and missing-file errors are raised here, before schema validation.
89
+ let inlinedRaw: string
90
+ try {
91
+ inlinedRaw = resolveIncludes(raw, absolutePath)
92
+ } catch (err) {
93
+ if (err instanceof IncludeResolutionError) {
94
+ throw new ConfigLoadError(err.message, err.issues)
95
+ }
96
+ throw err
97
+ }
98
+ if (inlinedRaw !== raw) {
99
+ const inlinedDoc = parseDocument(inlinedRaw, { keepSourceTokens: true })
100
+ const inlinedErrors = inlinedDoc.errors
101
+ if (inlinedErrors.length > 0) {
102
+ throw new ConfigLoadError(
103
+ `YAML parse errors in ${absolutePath} (after inlining includes):\n${inlinedErrors
104
+ .map((e) => ` - ${e.message}`)
105
+ .join("\n")}`,
106
+ convertYamlErrors(inlinedErrors),
107
+ )
108
+ }
109
+ return inlinedDoc.toJSON()
110
+ }
111
+ if (errors.length > 0) {
112
+ throw new ConfigLoadError(
113
+ `YAML parse errors in ${absolutePath}:\n${errors.map((e) => ` - ${e.message}`).join("\n")}`,
114
+ convertYamlErrors(errors),
115
+ )
116
+ }
117
+ return doc.toJSON()
118
+ }
119
+
120
+ // ponytail: zod 4's z.union() surfaces a top-level "Invalid input"
121
+ // with the per-variant reasons buried under `errors: Array<Array<issue>>`.
122
+ // We flatten those variant arrays into the top-level issue list so users
123
+ // see the actual cause (e.g. "Unrecognized key: \"src\"") instead of a
124
+ // useless top-level generic.
125
+ type RawZodIssue = {
126
+ message: string
127
+ path?: Array<string | number>
128
+ errors?: ReadonlyArray<ReadonlyArray<RawZodIssue>>
129
+ }
130
+
131
+ const flattenZodIssues = (
132
+ rawIssues: ReadonlyArray<RawZodIssue>,
133
+ basePath: ReadonlyArray<string | number> = [],
134
+ ): ConfigError[] => {
135
+ const out: ConfigError[] = []
136
+ for (const issue of rawIssues) {
137
+ const path = [...basePath, ...(issue.path ?? [])]
138
+ if (issue.errors !== undefined && issue.errors.length > 0) {
139
+ // ponytail: union variant issues — flatten ALL variants' issues so
140
+ // the user sees every possible mismatch. Filter out empty arrays
141
+ // (variants that matched nothing).
142
+ for (const variant of issue.errors) {
143
+ if (variant.length > 0) {
144
+ out.push(...flattenZodIssues(variant, path))
145
+ }
146
+ }
147
+ continue
148
+ }
149
+ out.push({
150
+ message: issue.message,
151
+ ...(path.length > 0 ? { path: path.join(".") } : {}),
152
+ })
153
+ }
154
+ return out
155
+ }
156
+
157
+ const reportZodIssues = (issues: ConfigError[]): string =>
158
+ issues
159
+ .map((issue) => {
160
+ const parts = [issue.message]
161
+ if (issue.location) parts.push(`@ ${formatLineCol(issue.location)}`)
162
+ if (issue.path) parts.push(`(at ${issue.path})`)
163
+ return ` - ${parts.join(" ")}`
164
+ })
165
+ .join("\n")
166
+
167
+ export const loadConfig = ({
168
+ configPath,
169
+ }: LoadConfigOptions): LoadConfigResult => {
170
+ const configDir = dirname(configPath)
171
+ const raw = loadConfigFile(configPath)
172
+ const expanded = expandButtonReferences(raw, configDir)
173
+ const result = RawConfigSchema.safeParse(expanded)
174
+ if (!result.success) {
175
+ // ponytail: recurse into unionErrors so the user sees the actual
176
+ // discriminator failure (e.g. "Unrecognized key 'src'") instead of
177
+ // a useless top-level "Invalid input".
178
+ const issues = flattenZodIssues(
179
+ result.error.issues as ReadonlyArray<RawZodIssue>,
180
+ )
181
+ throw new ConfigLoadError(
182
+ `Invalid config at ${configPath}:\n${reportZodIssues(issues)}`,
183
+ issues,
184
+ )
185
+ }
186
+ return { config: result.data, configDir }
187
+ }