@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.2

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 (373) hide show
  1. package/AGENTS.md +556 -0
  2. package/INSTALLATION.md +310 -0
  3. package/LICENSE +21 -0
  4. package/README.md +195 -2
  5. package/client.js +3 -0
  6. package/components.json +20 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/README.en.md +196 -0
  9. package/docs/screenshots/en/overview.png +0 -0
  10. package/docs/screenshots/en/question.png +0 -0
  11. package/docs/screenshots/zh/overview.png +0 -0
  12. package/docs/screenshots/zh/question.png +0 -0
  13. package/icon.svg +24 -0
  14. package/index.js +2 -0
  15. package/lib/account.js +1 -0
  16. package/lib/app-mapping.js +1 -0
  17. package/lib/i18n.js +1 -0
  18. package/lib/media-enrich.js +1 -0
  19. package/lib/notifications.js +1 -0
  20. package/lib/performance.js +1 -0
  21. package/lib/platform/index.js +1 -0
  22. package/lib/probe-adapter.js +1 -0
  23. package/lib/probe-daemon.js +1 -0
  24. package/lib/probe.js +1 -0
  25. package/lib/qr.js +1 -0
  26. package/lib/runtime.js +1 -0
  27. package/lib/service.js +1 -0
  28. package/lib/sessions.js +1 -0
  29. package/lib/settings.js +1 -0
  30. package/lib/types.js +1 -0
  31. package/lib/util.js +1 -0
  32. package/lib/weather.js +1 -0
  33. package/lib/ws.js +1 -0
  34. package/locale/en.json +6 -0
  35. package/locale/zh.json +6 -0
  36. package/package.json +118 -4
  37. package/scripts/browser-shot.mjs +217 -0
  38. package/scripts/build-dsh.mjs +228 -0
  39. package/scripts/build-rust.mjs +319 -0
  40. package/scripts/build-web.mjs +17 -0
  41. package/scripts/check-npm-package.mjs +322 -0
  42. package/scripts/mock-preview.py +574 -0
  43. package/scripts/screenshot-compose.py +89 -0
  44. package/scripts/screenshot-device.sh +75 -0
  45. package/src/dsh/client/api.ts +68 -0
  46. package/src/dsh/client/i18n.ts +573 -0
  47. package/src/dsh/client/icons.ts +56 -0
  48. package/src/dsh/client/index.ts +95 -0
  49. package/src/dsh/client/logic.ts +310 -0
  50. package/src/dsh/client/panel.ts +483 -0
  51. package/src/dsh/client/sections/apps.ts +40 -0
  52. package/src/dsh/client/sections/balance.ts +38 -0
  53. package/src/dsh/client/sections/connection.ts +97 -0
  54. package/src/dsh/client/sections/notifications.ts +43 -0
  55. package/src/dsh/client/sections/pomodoro.ts +184 -0
  56. package/src/dsh/client/sections/preferences.ts +138 -0
  57. package/src/dsh/client/sections/system.ts +120 -0
  58. package/src/dsh/client/sections/weather.ts +276 -0
  59. package/src/dsh/client/styles.ts +1119 -0
  60. package/src/dsh/client/types.ts +366 -0
  61. package/src/dsh/client/ui.ts +142 -0
  62. package/src/dsh/index.ts +108 -0
  63. package/src/dsh/lib/account.ts +520 -0
  64. package/src/dsh/lib/app-mapping.ts +366 -0
  65. package/src/dsh/lib/i18n.ts +82 -0
  66. package/src/dsh/lib/media-enrich.ts +202 -0
  67. package/src/dsh/lib/notifications.ts +322 -0
  68. package/src/dsh/lib/performance.ts +279 -0
  69. package/src/dsh/lib/platform/index.ts +43 -0
  70. package/src/dsh/lib/probe-adapter.ts +275 -0
  71. package/src/dsh/lib/probe-daemon.ts +359 -0
  72. package/src/dsh/lib/probe.ts +395 -0
  73. package/src/dsh/lib/qr.ts +42 -0
  74. package/src/dsh/lib/runtime.ts +126 -0
  75. package/src/dsh/lib/service.ts +1645 -0
  76. package/src/dsh/lib/sessions.ts +907 -0
  77. package/src/dsh/lib/settings.ts +468 -0
  78. package/src/dsh/lib/types.ts +231 -0
  79. package/src/dsh/lib/util.ts +115 -0
  80. package/src/dsh/lib/weather.ts +532 -0
  81. package/src/dsh/lib/ws.ts +328 -0
  82. package/src/dsh/types/qrcode-generator.d.ts +16 -0
  83. package/src/rust/Cargo.lock +1431 -0
  84. package/src/rust/Cargo.toml +68 -0
  85. package/src/rust/src/cli.rs +233 -0
  86. package/src/rust/src/daemon.rs +140 -0
  87. package/src/rust/src/location/linux.rs +118 -0
  88. package/src/rust/src/location/macos.rs +26 -0
  89. package/src/rust/src/location/mod.rs +170 -0
  90. package/src/rust/src/location/windows.rs +205 -0
  91. package/src/rust/src/main.rs +42 -0
  92. package/src/rust/src/media/catalog.rs +454 -0
  93. package/src/rust/src/media/linux.rs +350 -0
  94. package/src/rust/src/media/macos/accessibility.rs +59 -0
  95. package/src/rust/src/media/macos.rs +366 -0
  96. package/src/rust/src/media/mod.rs +298 -0
  97. package/src/rust/src/media/vendor/apple_music.rs +90 -0
  98. package/src/rust/src/media/vendor/common.rs +186 -0
  99. package/src/rust/src/media/vendor/kugou.rs +52 -0
  100. package/src/rust/src/media/vendor/kuwo.rs +49 -0
  101. package/src/rust/src/media/vendor/mod.rs +75 -0
  102. package/src/rust/src/media/vendor/netease.rs +1715 -0
  103. package/src/rust/src/media/vendor/qishui.rs +36 -0
  104. package/src/rust/src/media/vendor/qqmusic.rs +33 -0
  105. package/src/rust/src/media/vendor/spotify.rs +46 -0
  106. package/src/rust/src/media/vendor/wesing.rs +39 -0
  107. package/src/rust/src/media/vendor/youtube_music.rs +49 -0
  108. package/src/rust/src/media/windows/accessibility.rs +85 -0
  109. package/src/rust/src/media/windows/audio.rs +116 -0
  110. package/src/rust/src/media/windows/private.rs +291 -0
  111. package/src/rust/src/media/windows.rs +775 -0
  112. package/src/rust/src/notifications/linux.rs +276 -0
  113. package/src/rust/src/notifications/macos.rs +382 -0
  114. package/src/rust/src/notifications/mod.rs +244 -0
  115. package/src/rust/src/notifications/windows.rs +565 -0
  116. package/src/rust/src/perf/common.rs +109 -0
  117. package/src/rust/src/perf/linux.rs +282 -0
  118. package/src/rust/src/perf/macos.rs +28 -0
  119. package/src/rust/src/perf/mod.rs +157 -0
  120. package/src/rust/src/perf/windows.rs +981 -0
  121. package/src/rust/src/util.rs +360 -0
  122. package/src/rust/src/volume/linux.rs +195 -0
  123. package/src/rust/src/volume/macos.rs +54 -0
  124. package/src/rust/src/volume/mod.rs +116 -0
  125. package/src/rust/src/volume/windows.rs +75 -0
  126. package/src/rust/tests/cli.rs +79 -0
  127. package/src/rust/tests/daemon.rs +107 -0
  128. package/src/svelte/App.svelte +385 -0
  129. package/src/svelte/index.html +18 -0
  130. package/src/svelte/lib/billing.svelte.ts +228 -0
  131. package/src/svelte/lib/components/BalanceCard.svelte +173 -0
  132. package/src/svelte/lib/components/ClockCard.svelte +183 -0
  133. package/src/svelte/lib/components/CountdownCard.svelte +261 -0
  134. package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
  135. package/src/svelte/lib/components/EmptyState.svelte +34 -0
  136. package/src/svelte/lib/components/GpuCard.svelte +245 -0
  137. package/src/svelte/lib/components/HomePage.svelte +34 -0
  138. package/src/svelte/lib/components/MusicControls.svelte +256 -0
  139. package/src/svelte/lib/components/MusicCover.svelte +88 -0
  140. package/src/svelte/lib/components/MusicPage.svelte +116 -0
  141. package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
  142. package/src/svelte/lib/components/Panel.svelte +22 -0
  143. package/src/svelte/lib/components/PerformancePage.svelte +55 -0
  144. package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
  145. package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
  146. package/src/svelte/lib/components/RadialGauge.svelte +99 -0
  147. package/src/svelte/lib/components/Rail.svelte +217 -0
  148. package/src/svelte/lib/components/SectionHeader.svelte +40 -0
  149. package/src/svelte/lib/components/SeekBar.svelte +421 -0
  150. package/src/svelte/lib/components/SessionList.svelte +364 -0
  151. package/src/svelte/lib/components/SessionStats.svelte +98 -0
  152. package/src/svelte/lib/components/SessionsPage.svelte +61 -0
  153. package/src/svelte/lib/components/SpendCard.svelte +105 -0
  154. package/src/svelte/lib/components/ThermalCard.svelte +249 -0
  155. package/src/svelte/lib/components/Toast.svelte +32 -0
  156. package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
  157. package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
  158. package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
  159. package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
  160. package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
  161. package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
  162. package/src/svelte/lib/components/ui/alert/index.ts +17 -0
  163. package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
  164. package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
  165. package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
  166. package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
  167. package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
  168. package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
  169. package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
  170. package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
  171. package/src/svelte/lib/components/ui/badge/index.ts +2 -0
  172. package/src/svelte/lib/components/ui/button/button.svelte +82 -0
  173. package/src/svelte/lib/components/ui/button/index.ts +17 -0
  174. package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
  175. package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
  176. package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
  177. package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
  178. package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
  179. package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
  180. package/src/svelte/lib/components/ui/card/card.svelte +22 -0
  181. package/src/svelte/lib/components/ui/card/index.ts +25 -0
  182. package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
  183. package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
  184. package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
  185. package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
  186. package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
  187. package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
  188. package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  189. package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
  190. package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
  191. package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
  192. package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
  193. package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
  194. package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
  195. package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
  196. package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
  197. package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
  198. package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
  199. package/src/svelte/lib/components/ui/empty/index.ts +22 -0
  200. package/src/svelte/lib/components/ui/input/index.ts +7 -0
  201. package/src/svelte/lib/components/ui/input/input.svelte +48 -0
  202. package/src/svelte/lib/components/ui/progress/index.ts +7 -0
  203. package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
  204. package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
  205. package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
  206. package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
  207. package/src/svelte/lib/components/ui/separator/index.ts +7 -0
  208. package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
  209. package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
  210. package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
  211. package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
  212. package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
  213. package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  214. package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
  215. package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  216. package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  217. package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
  218. package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  219. package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
  220. package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
  221. package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
  222. package/src/svelte/lib/components/ui/slider/index.ts +7 -0
  223. package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
  224. package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
  225. package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
  226. package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  227. package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  228. package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  229. package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
  230. package/src/svelte/lib/countdown.svelte.ts +252 -0
  231. package/src/svelte/lib/format.ts +199 -0
  232. package/src/svelte/lib/i18n.svelte.ts +583 -0
  233. package/src/svelte/lib/media.ts +32 -0
  234. package/src/svelte/lib/pages.ts +18 -0
  235. package/src/svelte/lib/pomodoro.svelte.ts +223 -0
  236. package/src/svelte/lib/session-tree.ts +80 -0
  237. package/src/svelte/lib/state.svelte.ts +359 -0
  238. package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
  239. package/src/svelte/lib/types.ts +306 -0
  240. package/src/svelte/lib/utils.ts +8 -0
  241. package/src/svelte/lib/weather.ts +84 -0
  242. package/src/svelte/lib/ws.ts +104 -0
  243. package/src/svelte/main.ts +13 -0
  244. package/src/svelte/styles/global.css +315 -0
  245. package/src/svelte/vite-env.d.ts +12 -0
  246. package/static/fonts/LICENSE.txt +287 -0
  247. package/static/icon.svg +24 -0
  248. package/static/manifest.webmanifest +18 -0
  249. package/svelte.config.js +5 -0
  250. package/tsconfig.dsh-client.json +18 -0
  251. package/tsconfig.dsh.json +21 -0
  252. package/tsconfig.json +20 -0
  253. package/vite.config.js +31 -0
  254. package/web/assets/index-DLdSfnJ3.css +1 -0
  255. package/web/assets/index-YNE8X0Uy.js +6 -0
  256. package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  257. package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  258. package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  259. package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  260. package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  261. package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  262. package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  263. package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  264. package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  265. package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  266. package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  267. package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  268. package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  269. package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
  270. package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
  271. package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
  272. package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
  273. package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
  274. package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
  275. package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
  276. package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
  277. package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
  278. package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
  279. package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
  280. package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
  281. package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
  282. package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
  283. package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
  284. package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
  285. package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
  286. package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
  287. package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
  288. package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
  289. package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
  290. package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
  291. package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
  292. package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
  293. package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
  294. package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
  295. package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
  296. package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
  297. package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
  298. package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
  299. package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
  300. package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
  301. package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
  302. package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
  303. package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
  304. package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
  305. package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
  306. package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
  307. package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
  308. package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
  309. package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
  310. package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
  311. package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
  312. package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
  313. package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
  314. package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
  315. package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
  316. package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
  317. package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
  318. package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
  319. package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
  320. package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
  321. package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
  322. package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
  323. package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
  324. package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
  325. package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
  326. package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
  327. package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
  328. package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
  329. package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
  330. package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
  331. package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
  332. package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
  333. package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
  334. package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
  335. package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
  336. package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
  337. package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
  338. package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
  339. package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
  340. package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
  341. package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
  342. package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
  343. package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
  344. package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
  345. package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
  346. package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
  347. package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
  348. package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
  349. package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
  350. package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
  351. package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
  352. package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
  353. package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
  354. package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
  355. package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
  356. package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
  357. package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
  358. package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
  359. package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
  360. package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
  361. package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
  362. package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
  363. package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
  364. package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
  365. package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
  366. package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
  367. package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
  368. package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
  369. package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
  370. package/web/fonts/LICENSE.txt +287 -0
  371. package/web/icon.svg +24 -0
  372. package/web/index.html +19 -0
  373. package/web/manifest.webmanifest +18 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Rust-backed media/volume adapter.
3
+ *
4
+ * `dsh-probe` is the only OS probe layer: there is no PowerShell, playerctl,
5
+ * wpctl or osascript fallback. When the binary is missing every read returns a
6
+ * clear error state and every control action throws that error.
7
+ *
8
+ * Media progress smoothing / seek eligibility lives in `media-enrich.js`.
9
+ */
10
+
11
+ import type { JsonRecord, ProbeMediaResult, PublicMedia } from './types.js';
12
+ import { probeInfo, materializeThumbnail, runProbe } from './probe.js';
13
+ import { createMediaEnrichment, mediaKey } from './media-enrich.js';
14
+ import { clamp, finiteNumber, serializeError } from './util.js';
15
+
16
+ const MEDIA_TTL_MS = 1400;
17
+ const VOLUME_TTL_MS = 3000;
18
+ const ERROR_TTL_MS = 5000;
19
+ const MEDIA_ACTIONS = ['playpause', 'next', 'previous', 'seek', 'like', 'volume', 'mute'];
20
+
21
+ /** Cached `media read` / media action result, plus the cache-hit marker. */
22
+ export interface MediaCache {
23
+ ok: boolean;
24
+ available: boolean;
25
+ media: PublicMedia | null;
26
+ error: string | null;
27
+ backend: 'rust';
28
+ updatedAt: number;
29
+ cached?: boolean;
30
+ }
31
+
32
+ /** Cached `volume read` / volume action result, plus the cache-hit marker. */
33
+ export interface VolumeCache {
34
+ ok: boolean;
35
+ available: boolean;
36
+ volume: number;
37
+ muted: boolean;
38
+ error: string | null;
39
+ backend: 'rust';
40
+ updatedAt: number;
41
+ cached?: boolean;
42
+ }
43
+
44
+ /** One channel projected for diagnostics; every field is null before the first read. */
45
+ interface ChannelStatus {
46
+ ok: boolean | null;
47
+ available: boolean | null;
48
+ error: string | null;
49
+ backend: 'rust' | null;
50
+ updatedAt: number | null;
51
+ }
52
+
53
+ export interface MediaAdapterStatus {
54
+ media: ChannelStatus;
55
+ volume: ChannelStatus;
56
+ }
57
+
58
+ /** Media plus volume snapshot handed to the browser. */
59
+ export interface MediaStatePayload {
60
+ ok: boolean;
61
+ media: MediaCache;
62
+ volume: VolumeCache;
63
+ label: string;
64
+ sampledAt: number;
65
+ }
66
+
67
+ /** Result of `control('volume' | 'mute', ...)`. */
68
+ interface VolumeControlResult {
69
+ ok: true;
70
+ action: string;
71
+ volume: VolumeCache;
72
+ }
73
+
74
+ /** Result of the remaining media actions. */
75
+ interface MediaControlResult {
76
+ ok: true;
77
+ action: string;
78
+ media: MediaCache;
79
+ }
80
+
81
+ /** Error raised by a probe action, carrying the raw payload for diagnostics. */
82
+ interface ProbeActionError extends Error {
83
+ probeResult?: JsonRecord;
84
+ }
85
+
86
+ /** The adapter surface consumed by the platform wiring and the DSH service. */
87
+ interface ProbeMediaAdapter {
88
+ getState(): Promise<MediaStatePayload>;
89
+ getMedia(force?: boolean): Promise<MediaCache>;
90
+ getVolume(force?: boolean): Promise<VolumeCache>;
91
+ control(action: string, value?: number | boolean | null): Promise<VolumeControlResult | MediaControlResult>;
92
+ status(): MediaAdapterStatus;
93
+ }
94
+
95
+ function probeUnavailable(): Error {
96
+ const info = probeInfo();
97
+ return new Error(info.error || 'native probe unavailable');
98
+ }
99
+
100
+ export function createProbeMediaAdapter({ label = '平台' }: { label?: string } = {}): ProbeMediaAdapter {
101
+ let mediaCache: MediaCache | null = null;
102
+ let mediaAt = 0;
103
+ let volumeCache: VolumeCache | null = null;
104
+ let volumeAt = 0;
105
+ const enrichment = createMediaEnrichment();
106
+
107
+ function activeAppId(): string {
108
+ return mediaCache?.media?.appId || '';
109
+ }
110
+
111
+ async function callProbe(...args: string[]): Promise<ProbeMediaResult> {
112
+ const info = probeInfo();
113
+ if (!info.available) throw probeUnavailable();
114
+ const result: ProbeMediaResult = await materializeThumbnail(await runProbe(args));
115
+ if (result?.ok === false) {
116
+ const error: ProbeActionError = new Error(
117
+ String(result.actionError || result.error || `dsh-probe ${args.join(' ')} failed`),
118
+ );
119
+ error.probeResult = result;
120
+ throw error;
121
+ }
122
+ return result;
123
+ }
124
+
125
+ async function getMedia(force: boolean = false): Promise<MediaCache> {
126
+ const now = Date.now();
127
+ const ttl = mediaCache?.ok ? MEDIA_TTL_MS : ERROR_TTL_MS;
128
+ if (!force && mediaCache && now - mediaAt < ttl) return { ...mediaCache, cached: true };
129
+
130
+ try {
131
+ const args = ['media', 'read'];
132
+ const appId = activeAppId();
133
+ if (appId) args.push('--app-id', appId);
134
+ const result = await callProbe(...args);
135
+ mediaCache = {
136
+ ok: true,
137
+ available: result.available !== false,
138
+ media: await enrichment.enrich(result),
139
+ error: null,
140
+ backend: 'rust',
141
+ updatedAt: now,
142
+ };
143
+ } catch (error) {
144
+ mediaCache = {
145
+ ok: false,
146
+ available: false,
147
+ media: null,
148
+ error: serializeError(error),
149
+ backend: 'rust',
150
+ updatedAt: now,
151
+ };
152
+ }
153
+ mediaAt = Date.now();
154
+ return mediaCache;
155
+ }
156
+
157
+ async function getVolume(force: boolean = false): Promise<VolumeCache> {
158
+ const now = Date.now();
159
+ const ttl = volumeCache?.ok ? VOLUME_TTL_MS : ERROR_TTL_MS;
160
+ if (!force && volumeCache && now - volumeAt < ttl) return { ...volumeCache, cached: true };
161
+
162
+ try {
163
+ const result = await callProbe('volume', 'read');
164
+ volumeCache = {
165
+ ok: true,
166
+ available: true,
167
+ volume: clamp(finiteNumber(result.volume, 0.5) ?? 0.5, 0, 1),
168
+ muted: Boolean(result.muted),
169
+ error: null,
170
+ backend: 'rust',
171
+ updatedAt: now,
172
+ };
173
+ } catch (error) {
174
+ volumeCache = {
175
+ ok: false,
176
+ available: false,
177
+ volume: 0.5,
178
+ muted: false,
179
+ error: serializeError(error),
180
+ backend: 'rust',
181
+ updatedAt: now,
182
+ };
183
+ }
184
+ volumeAt = Date.now();
185
+ return volumeCache;
186
+ }
187
+
188
+ async function controlVolume(normalized: string, value: number | null): Promise<VolumeControlResult> {
189
+ const level = clamp(finiteNumber(value, 0.5) ?? 0.5, 0, 1);
190
+ const result = normalized === 'mute'
191
+ ? await callProbe('volume', 'toggle')
192
+ : await callProbe('volume', 'set', '--value', String(level));
193
+ volumeCache = {
194
+ ok: true,
195
+ available: true,
196
+ volume: clamp(finiteNumber(result.volume, level) ?? level, 0, 1),
197
+ muted: Boolean(result.muted),
198
+ error: null,
199
+ backend: 'rust',
200
+ updatedAt: Date.now(),
201
+ };
202
+ volumeAt = Date.now();
203
+ return { ok: true, action: normalized, volume: volumeCache };
204
+ }
205
+
206
+ async function control(
207
+ action: string,
208
+ value: number | boolean | null = null,
209
+ ): Promise<VolumeControlResult | MediaControlResult> {
210
+ const normalized = String(action || '').toLowerCase();
211
+ if (!MEDIA_ACTIONS.includes(normalized)) throw new Error('Unsupported media action');
212
+ if (normalized === 'volume' || normalized === 'mute') {
213
+ return controlVolume(normalized, Number(value));
214
+ }
215
+ if (normalized === 'like' && !mediaCache?.media?.canSetLiked) {
216
+ throw new Error('当前播放器不支持修改红心');
217
+ }
218
+
219
+ let position = 0;
220
+ if (normalized === 'seek') {
221
+ if (!mediaCache?.media?.canSeek) throw new Error('当前播放器只上报进度,不支持拖动');
222
+ position = clamp(finiteNumber(value, 0) ?? 0, 0, 24 * 60 * 60);
223
+ }
224
+
225
+ if (normalized === 'seek') enrichment.markSeek(mediaKey(mediaCache?.media), position);
226
+ const args = ['media', normalized];
227
+ if (normalized === 'seek') args.push('--position', String(position));
228
+ if (normalized === 'like' && typeof value === 'boolean') {
229
+ args.push('--value', value ? 'true' : 'false');
230
+ }
231
+ const appId = activeAppId();
232
+ if (appId) args.push('--app-id', appId);
233
+ const result = await callProbe(...args);
234
+ mediaCache = {
235
+ ok: true,
236
+ available: result.available !== false,
237
+ media: await enrichment.enrich(result),
238
+ error: null,
239
+ backend: 'rust',
240
+ updatedAt: Date.now(),
241
+ };
242
+ mediaAt = Date.now();
243
+ return { ok: true, action: normalized, media: mediaCache };
244
+ }
245
+
246
+ async function getState(): Promise<MediaStatePayload> {
247
+ const [media, volume] = await Promise.all([getMedia(), getVolume()]);
248
+ return { ok: media.ok || volume.ok, media, volume, label, sampledAt: Date.now() };
249
+ }
250
+
251
+ function status(): MediaAdapterStatus {
252
+ return {
253
+ media: mediaCache
254
+ ? {
255
+ ok: mediaCache.ok,
256
+ available: mediaCache.available,
257
+ error: mediaCache.error,
258
+ backend: mediaCache.backend,
259
+ updatedAt: mediaCache.updatedAt,
260
+ }
261
+ : { ok: null, available: null, error: null, backend: null, updatedAt: null },
262
+ volume: volumeCache
263
+ ? {
264
+ ok: volumeCache.ok,
265
+ available: volumeCache.available,
266
+ error: volumeCache.error,
267
+ backend: volumeCache.backend,
268
+ updatedAt: volumeCache.updatedAt,
269
+ }
270
+ : { ok: null, available: null, error: null, backend: null, updatedAt: null },
271
+ };
272
+ }
273
+
274
+ return { getState, getMedia, getVolume, control, status };
275
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Persistent `dsh-probe daemon` client.
3
+ *
4
+ * Starting the native probe for every poll costs process creation plus a full
5
+ * platform sampler warm-up (sysinfo's CPU window, PDH query setup, D-Bus
6
+ * connect, WinRT initialization). This client keeps one daemon process alive
7
+ * and multiplexes requests over newline-delimited JSON on stdin/stdout.
8
+ *
9
+ * The daemon is demand-driven: it only samples when a request arrives, so an
10
+ * idle console costs one sleeping process and no periodic wake-ups. It exits
11
+ * after `IDLE_TIMEOUT_MS` without requests, and the next request starts a fresh
12
+ * one. If the binary predates daemon support, `probe.ts` falls back to one-shot
13
+ * invocations for a while instead of failing the feature.
14
+ */
15
+
16
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
17
+ import { createInterface, type Interface } from 'node:readline';
18
+ import type { JsonRecord } from './types.js';
19
+ import { isRecord } from './types.js';
20
+
21
+ /** Must match `daemon::PROTOCOL_VERSION` in the Rust probe. */
22
+ export const DAEMON_PROTOCOL_VERSION = 1;
23
+
24
+ const READY_TIMEOUT_MS = 10_000;
25
+ const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
26
+ const MAX_STDERR = 4_000;
27
+
28
+ /** The daemon could not be started; the caller may retry with one-shot mode. */
29
+ export class ProbeDaemonStartError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.name = 'ProbeDaemonStartError';
33
+ }
34
+ }
35
+
36
+ interface PendingRequest {
37
+ resolve: (value: JsonRecord) => void;
38
+ reject: (error: Error) => void;
39
+ timer: NodeJS.Timeout;
40
+ }
41
+
42
+ interface ReadyGate {
43
+ resolve: () => void;
44
+ reject: (error: Error) => void;
45
+ timer: NodeJS.Timeout;
46
+ }
47
+
48
+ function idleTimeoutMs(): number {
49
+ const raw = Number(process.env.DSH_RC_PROBE_DAEMON_IDLE_MS);
50
+ if (!Number.isFinite(raw) || raw < 5_000) return DEFAULT_IDLE_TIMEOUT_MS;
51
+ return Math.min(raw, 30 * 60_000);
52
+ }
53
+
54
+ export class ProbeDaemonClient {
55
+ private readonly binary: string;
56
+ private readonly env: NodeJS.ProcessEnv;
57
+ private child: ChildProcessWithoutNullStreams | null = null;
58
+ private reader: Interface | null = null;
59
+ private readyGate: ReadyGate | null = null;
60
+ private ready = false;
61
+ private starting: Promise<void> | null = null;
62
+ private pending = new Map<number, PendingRequest>();
63
+ private nextId = 1;
64
+ private stderr = '';
65
+ private idleTimer: NodeJS.Timeout | null = null;
66
+ private stopped = false;
67
+
68
+ constructor(binary: string, env: NodeJS.ProcessEnv = process.env) {
69
+ this.binary = binary;
70
+ this.env = env;
71
+ }
72
+
73
+ isRunning(): boolean {
74
+ return this.ready && this.child !== null;
75
+ }
76
+
77
+ /** Run one probe command through the daemon, starting it on demand. */
78
+ async request(argv: string[], timeoutMs: number): Promise<JsonRecord> {
79
+ if (!this.ready || !this.child) await this.ensureStarted();
80
+ const child = this.child;
81
+ if (!child || !this.ready) throw new ProbeDaemonStartError('probe daemon is not running');
82
+ if (child.stdin.destroyed) throw new ProbeDaemonStartError('probe daemon stdin is closed');
83
+
84
+ const id = this.nextId++;
85
+ const payload = `${JSON.stringify({ id, argv })}\n`;
86
+ const timeout = Math.max(1_000, Math.min(Number(timeoutMs) || 15_000, 60_000));
87
+
88
+ return new Promise<JsonRecord>((resolve, reject) => {
89
+ const timer = setTimeout(() => {
90
+ this.pending.delete(id);
91
+ this.fail(new Error(`dsh-probe daemon ${argv.join(' ')} timed out after ${timeout} ms`));
92
+ }, timeout);
93
+ timer.unref?.();
94
+ this.pending.set(id, { resolve, reject, timer });
95
+ this.armIdle();
96
+
97
+ child.stdin.write(payload, (error) => {
98
+ if (!error) return;
99
+ const pending = this.pending.get(id);
100
+ if (!pending) return;
101
+ this.pending.delete(id);
102
+ clearTimeout(pending.timer);
103
+ pending.reject(error);
104
+ });
105
+ });
106
+ }
107
+
108
+ /** Stop the daemon and reject anything still in flight. */
109
+ shutdown(): void {
110
+ this.stopped = true;
111
+ this.clearIdle();
112
+ const error = new Error('probe daemon shut down');
113
+ this.rejectAll(error);
114
+ const gate = this.readyGate;
115
+ this.readyGate = null;
116
+ if (gate) {
117
+ clearTimeout(gate.timer);
118
+ gate.reject(new ProbeDaemonStartError(error.message));
119
+ }
120
+ const child = this.child;
121
+ this.child = null;
122
+ this.ready = false;
123
+ this.reader?.close();
124
+ this.reader = null;
125
+ if (child) {
126
+ try {
127
+ child.stdin.end();
128
+ } catch {
129
+ /* already closed */
130
+ }
131
+ try {
132
+ child.kill();
133
+ } catch {
134
+ /* already exited */
135
+ }
136
+ }
137
+ }
138
+
139
+ /** Synchronous best-effort kill for a process-exit hook. */
140
+ kill(): void {
141
+ const child = this.child;
142
+ this.child = null;
143
+ this.ready = false;
144
+ if (!child) return;
145
+ try {
146
+ child.kill();
147
+ } catch {
148
+ /* already exited */
149
+ }
150
+ }
151
+
152
+ private ensureStarted(): Promise<void> {
153
+ if (this.ready && this.child) return Promise.resolve();
154
+ if (this.starting) return this.starting;
155
+ this.starting = this.start().finally(() => {
156
+ this.starting = null;
157
+ });
158
+ return this.starting;
159
+ }
160
+
161
+ private start(): Promise<void> {
162
+ return new Promise<void>((resolve, reject) => {
163
+ let child: ChildProcessWithoutNullStreams;
164
+ try {
165
+ child = spawn(this.binary, ['daemon'], {
166
+ stdio: ['pipe', 'pipe', 'pipe'],
167
+ windowsHide: true,
168
+ env: this.env,
169
+ });
170
+ } catch (error) {
171
+ reject(new ProbeDaemonStartError(`failed to start dsh-probe daemon: ${String(error)}`));
172
+ return;
173
+ }
174
+
175
+ this.child = child;
176
+ this.ready = false;
177
+ this.stderr = '';
178
+ this.stopped = false;
179
+
180
+ const timer = setTimeout(() => {
181
+ this.readyGate = null;
182
+ this.stopChild(child);
183
+ reject(new ProbeDaemonStartError(`dsh-probe daemon did not become ready within ${READY_TIMEOUT_MS} ms`));
184
+ }, READY_TIMEOUT_MS);
185
+ timer.unref?.();
186
+ this.readyGate = { resolve, reject, timer };
187
+
188
+ child.stderr.setEncoding('utf8');
189
+ child.stderr.on('data', (chunk: string) => {
190
+ this.stderr = (this.stderr + chunk).slice(-MAX_STDERR);
191
+ });
192
+ child.once('error', (error) => {
193
+ if (this.child !== child) return;
194
+ this.failStart(child, new ProbeDaemonStartError(`failed to start dsh-probe daemon: ${error.message}`));
195
+ });
196
+ child.once('exit', (code, signal) => {
197
+ this.handleExit(child, code, signal);
198
+ });
199
+
200
+ const reader = createInterface({ input: child.stdout, crlfDelay: Infinity });
201
+ this.reader = reader;
202
+ reader.on('line', (line) => {
203
+ this.handleLine(line);
204
+ });
205
+ reader.on('close', () => {
206
+ /* the exit handler owns cleanup */
207
+ });
208
+ });
209
+ }
210
+
211
+ private handleLine(line: string): void {
212
+ const text = line.trim();
213
+ if (!text) return;
214
+ let message: unknown;
215
+ try {
216
+ message = JSON.parse(text);
217
+ } catch {
218
+ return;
219
+ }
220
+ if (!isRecord(message)) return;
221
+
222
+ if (message.event === 'ready') {
223
+ if (this.ready) return;
224
+ const protocol = Number(message.protocol);
225
+ if (protocol !== DAEMON_PROTOCOL_VERSION) {
226
+ this.failStart(
227
+ this.child,
228
+ new ProbeDaemonStartError(
229
+ `dsh-probe daemon protocol ${String(message.protocol)} is not supported (expected ${DAEMON_PROTOCOL_VERSION})`,
230
+ ),
231
+ );
232
+ return;
233
+ }
234
+ this.ready = true;
235
+ const gate = this.readyGate;
236
+ this.readyGate = null;
237
+ if (gate) {
238
+ clearTimeout(gate.timer);
239
+ gate.resolve();
240
+ }
241
+ this.armIdle();
242
+ return;
243
+ }
244
+
245
+ const id = typeof message.id === 'number' ? message.id : null;
246
+ if (id === null) return;
247
+ const pending = this.pending.get(id);
248
+ if (!pending) return;
249
+ this.pending.delete(id);
250
+ clearTimeout(pending.timer);
251
+ this.armIdle();
252
+
253
+ if (message.ok !== true) {
254
+ pending.reject(new Error(typeof message.error === 'string' ? message.error : 'dsh-probe daemon request failed'));
255
+ return;
256
+ }
257
+ if (!isRecord(message.result)) {
258
+ pending.reject(new Error('dsh-probe daemon returned no result payload'));
259
+ return;
260
+ }
261
+ pending.resolve(message.result);
262
+ }
263
+
264
+ private handleExit(
265
+ child: ChildProcessWithoutNullStreams,
266
+ code: number | null,
267
+ signal: NodeJS.Signals | null,
268
+ ): void {
269
+ if (this.child !== child) return;
270
+ this.child = null;
271
+ this.ready = false;
272
+ this.reader?.close();
273
+ this.reader = null;
274
+ this.clearIdle();
275
+
276
+ const detail = [
277
+ code === null ? '' : `code ${code}`,
278
+ signal ? `signal ${signal}` : '',
279
+ this.stderr.trim(),
280
+ ]
281
+ .filter(Boolean)
282
+ .join(', ');
283
+ const message = `dsh-probe daemon exited${detail ? ` (${detail})` : ''}`;
284
+
285
+ const gate = this.readyGate;
286
+ this.readyGate = null;
287
+ if (gate) {
288
+ clearTimeout(gate.timer);
289
+ gate.reject(new ProbeDaemonStartError(message));
290
+ }
291
+ this.rejectAll(new Error(message));
292
+ }
293
+
294
+ private failStart(child: ChildProcessWithoutNullStreams | null, error: ProbeDaemonStartError): void {
295
+ const gate = this.readyGate;
296
+ this.readyGate = null;
297
+ if (gate) {
298
+ clearTimeout(gate.timer);
299
+ gate.reject(error);
300
+ }
301
+ this.rejectAll(error);
302
+ if (child) this.stopChild(child);
303
+ }
304
+
305
+ private fail(error: Error): void {
306
+ this.rejectAll(error);
307
+ const child = this.child;
308
+ this.child = null;
309
+ this.ready = false;
310
+ if (child) this.stopChild(child);
311
+ }
312
+
313
+ private stopChild(child: ChildProcessWithoutNullStreams): void {
314
+ if (this.child === child) {
315
+ this.child = null;
316
+ this.ready = false;
317
+ }
318
+ this.reader?.close();
319
+ this.reader = null;
320
+ try {
321
+ child.stdin.end();
322
+ } catch {
323
+ /* already closed */
324
+ }
325
+ try {
326
+ child.kill();
327
+ } catch {
328
+ /* already exited */
329
+ }
330
+ }
331
+
332
+ private rejectAll(error: Error): void {
333
+ for (const pending of this.pending.values()) {
334
+ clearTimeout(pending.timer);
335
+ pending.reject(error);
336
+ }
337
+ this.pending.clear();
338
+ }
339
+
340
+ private armIdle(): void {
341
+ this.clearIdle();
342
+ if (this.stopped || !this.ready) return;
343
+ this.idleTimer = setTimeout(() => {
344
+ this.idleTimer = null;
345
+ if (this.pending.size > 0) {
346
+ this.armIdle();
347
+ return;
348
+ }
349
+ this.shutdown();
350
+ }, idleTimeoutMs());
351
+ this.idleTimer.unref?.();
352
+ }
353
+
354
+ private clearIdle(): void {
355
+ if (!this.idleTimer) return;
356
+ clearTimeout(this.idleTimer);
357
+ this.idleTimer = null;
358
+ }
359
+ }