@marktrue/dsh-mobile-remote 3.2.4-alpha.2-app → 3.2.4-alpha.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 (352) hide show
  1. package/CHANGELOG.md +1427 -0
  2. package/CONTRIBUTING.md +59 -0
  3. package/FAQ.md +111 -0
  4. package/README.md +254 -6
  5. package/cordis.patch.yml +14 -0
  6. package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +125 -0
  7. package/docs/01-PRD.md +148 -0
  8. package/docs/02-architecture.md +249 -0
  9. package/docs/03-api.md +1154 -0
  10. package/docs/04-security.md +100 -0
  11. package/docs/05-test-cases.md +405 -0
  12. package/docs/06-install-run.md +372 -0
  13. package/docs/07-user-manual.md +70 -0
  14. package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +86 -0
  15. package/docs/09-compatibility.md +227 -0
  16. package/docs/10-release.md +1067 -0
  17. package/docs/screenshots/01-home.png +0 -0
  18. package/docs/screenshots/02-notifications.png +0 -0
  19. package/docs/screenshots/03-sessions.png +0 -0
  20. package/docs/screenshots/04-chat.png +0 -0
  21. package/docs/screenshots/05-settings.png +0 -0
  22. package/docs/screenshots/06-new-session.png +0 -0
  23. package/docs/screenshots/pgy-appstore.jpg +0 -0
  24. package/docs/screenshots/pgy-mobile-app.jpg +0 -0
  25. package/docs/screenshots/pgy-pc-client.jpg +0 -0
  26. package/lib/account-usage.js +145 -0
  27. package/lib/app-alignment.js +112 -0
  28. package/lib/app-carrier-store.js +1972 -0
  29. package/lib/app-carrier.js +609 -0
  30. package/lib/client.js +1174 -0
  31. package/lib/desktop-form.js +738 -0
  32. package/lib/endpoint-registry.js +167 -0
  33. package/lib/git-read-bundled-provider.js +450 -0
  34. package/lib/git-read-service.js +601 -0
  35. package/lib/index.js +9622 -0
  36. package/lib/npm-update-source.js +226 -0
  37. package/lib/plugin-content-digest.js +80 -0
  38. package/lib/plugin-self-update.js +997 -0
  39. package/lib/plugin-update-controller.js +274 -0
  40. package/lib/plugin-update-log.js +164 -0
  41. package/lib/plugin-update-transaction.js +322 -0
  42. package/lib/plugin-update.js +309 -0
  43. package/lib/runtime-dependency-snapshot.js +345 -0
  44. package/lib/session-title-cache.js +123 -0
  45. package/lib/session-title-refresh.js +261 -0
  46. package/lib/tool-process.js +20 -0
  47. package/lib/update-fetch.js +24 -0
  48. package/lib/update-lock-arbiter.js +170 -0
  49. package/lib/update-supervisor.js +346 -0
  50. package/node_modules/ansi-regex/index.d.ts +37 -0
  51. package/node_modules/ansi-regex/index.js +10 -0
  52. package/node_modules/ansi-regex/license +9 -0
  53. package/node_modules/ansi-regex/package.json +55 -0
  54. package/node_modules/ansi-regex/readme.md +78 -0
  55. package/node_modules/ansi-styles/index.d.ts +345 -0
  56. package/node_modules/ansi-styles/index.js +163 -0
  57. package/node_modules/ansi-styles/license +9 -0
  58. package/node_modules/ansi-styles/package.json +56 -0
  59. package/node_modules/ansi-styles/readme.md +152 -0
  60. package/node_modules/camelcase/index.d.ts +63 -0
  61. package/node_modules/camelcase/index.js +76 -0
  62. package/node_modules/camelcase/license +9 -0
  63. package/node_modules/camelcase/package.json +43 -0
  64. package/node_modules/camelcase/readme.md +99 -0
  65. package/node_modules/cliui/CHANGELOG.md +76 -0
  66. package/node_modules/cliui/LICENSE.txt +14 -0
  67. package/node_modules/cliui/README.md +115 -0
  68. package/node_modules/cliui/index.js +354 -0
  69. package/node_modules/cliui/package.json +65 -0
  70. package/node_modules/color-convert/CHANGELOG.md +54 -0
  71. package/node_modules/color-convert/LICENSE +21 -0
  72. package/node_modules/color-convert/README.md +68 -0
  73. package/node_modules/color-convert/conversions.js +839 -0
  74. package/node_modules/color-convert/index.js +81 -0
  75. package/node_modules/color-convert/package.json +48 -0
  76. package/node_modules/color-convert/route.js +97 -0
  77. package/node_modules/color-name/LICENSE +8 -0
  78. package/node_modules/color-name/README.md +11 -0
  79. package/node_modules/color-name/index.js +152 -0
  80. package/node_modules/color-name/package.json +28 -0
  81. package/node_modules/decamelize/index.js +13 -0
  82. package/node_modules/decamelize/license +21 -0
  83. package/node_modules/decamelize/package.json +38 -0
  84. package/node_modules/decamelize/readme.md +48 -0
  85. package/node_modules/dijkstrajs/.travis.yml +4 -0
  86. package/node_modules/dijkstrajs/CONTRIBUTING.md +8 -0
  87. package/node_modules/dijkstrajs/LICENSE.md +19 -0
  88. package/node_modules/dijkstrajs/README.md +18 -0
  89. package/node_modules/dijkstrajs/dijkstra.js +165 -0
  90. package/node_modules/dijkstrajs/package.json +60 -0
  91. package/node_modules/dijkstrajs/test/dijkstra.test.js +96 -0
  92. package/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
  93. package/node_modules/emoji-regex/README.md +73 -0
  94. package/node_modules/emoji-regex/es2015/index.js +6 -0
  95. package/node_modules/emoji-regex/es2015/text.js +6 -0
  96. package/node_modules/emoji-regex/index.d.ts +23 -0
  97. package/node_modules/emoji-regex/index.js +6 -0
  98. package/node_modules/emoji-regex/package.json +50 -0
  99. package/node_modules/emoji-regex/text.js +6 -0
  100. package/node_modules/find-up/index.d.ts +137 -0
  101. package/node_modules/find-up/index.js +89 -0
  102. package/node_modules/find-up/license +9 -0
  103. package/node_modules/find-up/package.json +53 -0
  104. package/node_modules/find-up/readme.md +156 -0
  105. package/node_modules/get-caller-file/LICENSE.md +6 -0
  106. package/node_modules/get-caller-file/README.md +41 -0
  107. package/node_modules/get-caller-file/index.d.ts +2 -0
  108. package/node_modules/get-caller-file/index.js +22 -0
  109. package/node_modules/get-caller-file/index.js.map +1 -0
  110. package/node_modules/get-caller-file/package.json +42 -0
  111. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  112. package/node_modules/is-fullwidth-code-point/index.js +50 -0
  113. package/node_modules/is-fullwidth-code-point/license +9 -0
  114. package/node_modules/is-fullwidth-code-point/package.json +42 -0
  115. package/node_modules/is-fullwidth-code-point/readme.md +39 -0
  116. package/node_modules/locate-path/index.d.ts +83 -0
  117. package/node_modules/locate-path/index.js +65 -0
  118. package/node_modules/locate-path/license +9 -0
  119. package/node_modules/locate-path/package.json +45 -0
  120. package/node_modules/locate-path/readme.md +122 -0
  121. package/node_modules/p-limit/index.d.ts +38 -0
  122. package/node_modules/p-limit/index.js +57 -0
  123. package/node_modules/p-limit/license +9 -0
  124. package/node_modules/p-limit/package.json +52 -0
  125. package/node_modules/p-limit/readme.md +101 -0
  126. package/node_modules/p-locate/index.d.ts +64 -0
  127. package/node_modules/p-locate/index.js +52 -0
  128. package/node_modules/p-locate/license +9 -0
  129. package/node_modules/p-locate/package.json +53 -0
  130. package/node_modules/p-locate/readme.md +90 -0
  131. package/node_modules/p-try/index.d.ts +39 -0
  132. package/node_modules/p-try/index.js +9 -0
  133. package/node_modules/p-try/license +9 -0
  134. package/node_modules/p-try/package.json +42 -0
  135. package/node_modules/p-try/readme.md +58 -0
  136. package/node_modules/path-exists/index.d.ts +28 -0
  137. package/node_modules/path-exists/index.js +23 -0
  138. package/node_modules/path-exists/license +9 -0
  139. package/node_modules/path-exists/package.json +39 -0
  140. package/node_modules/path-exists/readme.md +52 -0
  141. package/node_modules/pngjs/.eslintignore +2 -0
  142. package/node_modules/pngjs/.eslintrc.json +19 -0
  143. package/node_modules/pngjs/.prettierignore +3 -0
  144. package/node_modules/pngjs/LICENSE +20 -0
  145. package/node_modules/pngjs/README.md +429 -0
  146. package/node_modules/pngjs/browser.js +16096 -0
  147. package/node_modules/pngjs/coverage/lcov-report/base.css +354 -0
  148. package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +881 -0
  149. package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +554 -0
  150. package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +77 -0
  151. package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +647 -0
  152. package/node_modules/pngjs/coverage/lcov-report/constants.js.html +176 -0
  153. package/node_modules/pngjs/coverage/lcov-report/crc.js.html +200 -0
  154. package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
  155. package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +593 -0
  156. package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +152 -0
  157. package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +143 -0
  158. package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +611 -0
  159. package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +359 -0
  160. package/node_modules/pngjs/coverage/lcov-report/index.html +426 -0
  161. package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +365 -0
  162. package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +230 -0
  163. package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +248 -0
  164. package/node_modules/pngjs/coverage/lcov-report/packer.js.html +467 -0
  165. package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +128 -0
  166. package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +575 -0
  167. package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +404 -0
  168. package/node_modules/pngjs/coverage/lcov-report/parser.js.html +950 -0
  169. package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +116 -0
  170. package/node_modules/pngjs/coverage/lcov-report/png.js.html +662 -0
  171. package/node_modules/pngjs/coverage/lcov-report/prettify.css +101 -0
  172. package/node_modules/pngjs/coverage/lcov-report/prettify.js +1007 -0
  173. package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  174. package/node_modules/pngjs/coverage/lcov-report/sorter.js +169 -0
  175. package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +584 -0
  176. package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +215 -0
  177. package/node_modules/pngjs/coverage/lcov.info +2177 -0
  178. package/node_modules/pngjs/lib/bitmapper.js +267 -0
  179. package/node_modules/pngjs/lib/bitpacker.js +158 -0
  180. package/node_modules/pngjs/lib/chunkstream.js +189 -0
  181. package/node_modules/pngjs/lib/constants.js +32 -0
  182. package/node_modules/pngjs/lib/crc.js +40 -0
  183. package/node_modules/pngjs/lib/filter-pack.js +171 -0
  184. package/node_modules/pngjs/lib/filter-parse-async.js +24 -0
  185. package/node_modules/pngjs/lib/filter-parse-sync.js +21 -0
  186. package/node_modules/pngjs/lib/filter-parse.js +177 -0
  187. package/node_modules/pngjs/lib/format-normaliser.js +93 -0
  188. package/node_modules/pngjs/lib/interlace.js +95 -0
  189. package/node_modules/pngjs/lib/packer-async.js +50 -0
  190. package/node_modules/pngjs/lib/packer-sync.js +56 -0
  191. package/node_modules/pngjs/lib/packer.js +129 -0
  192. package/node_modules/pngjs/lib/paeth-predictor.js +16 -0
  193. package/node_modules/pngjs/lib/parser-async.js +165 -0
  194. package/node_modules/pngjs/lib/parser-sync.js +108 -0
  195. package/node_modules/pngjs/lib/parser.js +290 -0
  196. package/node_modules/pngjs/lib/png-sync.js +12 -0
  197. package/node_modules/pngjs/lib/png.js +194 -0
  198. package/node_modules/pngjs/lib/sync-inflate.js +168 -0
  199. package/node_modules/pngjs/lib/sync-reader.js +45 -0
  200. package/node_modules/pngjs/package.json +73 -0
  201. package/node_modules/qrcode/README.md +770 -0
  202. package/node_modules/qrcode/bin/qrcode +159 -0
  203. package/node_modules/qrcode/helper/to-sjis-browser.js +2 -0
  204. package/node_modules/qrcode/helper/to-sjis.js +105 -0
  205. package/node_modules/qrcode/lib/browser.js +76 -0
  206. package/node_modules/qrcode/lib/can-promise.js +7 -0
  207. package/node_modules/qrcode/lib/core/alignment-pattern.js +83 -0
  208. package/node_modules/qrcode/lib/core/alphanumeric-data.js +59 -0
  209. package/node_modules/qrcode/lib/core/bit-buffer.js +37 -0
  210. package/node_modules/qrcode/lib/core/bit-matrix.js +65 -0
  211. package/node_modules/qrcode/lib/core/byte-data.js +30 -0
  212. package/node_modules/qrcode/lib/core/error-correction-code.js +135 -0
  213. package/node_modules/qrcode/lib/core/error-correction-level.js +50 -0
  214. package/node_modules/qrcode/lib/core/finder-pattern.js +22 -0
  215. package/node_modules/qrcode/lib/core/format-info.js +29 -0
  216. package/node_modules/qrcode/lib/core/galois-field.js +69 -0
  217. package/node_modules/qrcode/lib/core/kanji-data.js +54 -0
  218. package/node_modules/qrcode/lib/core/mask-pattern.js +234 -0
  219. package/node_modules/qrcode/lib/core/mode.js +167 -0
  220. package/node_modules/qrcode/lib/core/numeric-data.js +43 -0
  221. package/node_modules/qrcode/lib/core/polynomial.js +62 -0
  222. package/node_modules/qrcode/lib/core/qrcode.js +495 -0
  223. package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +56 -0
  224. package/node_modules/qrcode/lib/core/regex.js +31 -0
  225. package/node_modules/qrcode/lib/core/segments.js +330 -0
  226. package/node_modules/qrcode/lib/core/utils.js +63 -0
  227. package/node_modules/qrcode/lib/core/version-check.js +9 -0
  228. package/node_modules/qrcode/lib/core/version.js +163 -0
  229. package/node_modules/qrcode/lib/index.js +12 -0
  230. package/node_modules/qrcode/lib/renderer/canvas.js +63 -0
  231. package/node_modules/qrcode/lib/renderer/png.js +78 -0
  232. package/node_modules/qrcode/lib/renderer/svg-tag.js +81 -0
  233. package/node_modules/qrcode/lib/renderer/svg.js +19 -0
  234. package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +85 -0
  235. package/node_modules/qrcode/lib/renderer/terminal/terminal.js +49 -0
  236. package/node_modules/qrcode/lib/renderer/terminal.js +9 -0
  237. package/node_modules/qrcode/lib/renderer/utf8.js +71 -0
  238. package/node_modules/qrcode/lib/renderer/utils.js +99 -0
  239. package/node_modules/qrcode/lib/server.js +138 -0
  240. package/node_modules/qrcode/license +10 -0
  241. package/node_modules/qrcode/package.json +77 -0
  242. package/node_modules/require-directory/.jshintrc +67 -0
  243. package/node_modules/require-directory/.npmignore +1 -0
  244. package/node_modules/require-directory/.travis.yml +3 -0
  245. package/node_modules/require-directory/LICENSE +22 -0
  246. package/node_modules/require-directory/README.markdown +184 -0
  247. package/node_modules/require-directory/index.js +86 -0
  248. package/node_modules/require-directory/package.json +40 -0
  249. package/node_modules/require-main-filename/CHANGELOG.md +26 -0
  250. package/node_modules/require-main-filename/LICENSE.txt +14 -0
  251. package/node_modules/require-main-filename/README.md +26 -0
  252. package/node_modules/require-main-filename/index.js +18 -0
  253. package/node_modules/require-main-filename/package.json +35 -0
  254. package/node_modules/set-blocking/CHANGELOG.md +26 -0
  255. package/node_modules/set-blocking/LICENSE.txt +14 -0
  256. package/node_modules/set-blocking/README.md +31 -0
  257. package/node_modules/set-blocking/index.js +7 -0
  258. package/node_modules/set-blocking/package.json +42 -0
  259. package/node_modules/string-width/index.d.ts +29 -0
  260. package/node_modules/string-width/index.js +47 -0
  261. package/node_modules/string-width/license +9 -0
  262. package/node_modules/string-width/package.json +56 -0
  263. package/node_modules/string-width/readme.md +50 -0
  264. package/node_modules/strip-ansi/index.d.ts +17 -0
  265. package/node_modules/strip-ansi/index.js +4 -0
  266. package/node_modules/strip-ansi/license +9 -0
  267. package/node_modules/strip-ansi/package.json +54 -0
  268. package/node_modules/strip-ansi/readme.md +46 -0
  269. package/node_modules/which-module/LICENSE +13 -0
  270. package/node_modules/which-module/README.md +58 -0
  271. package/node_modules/which-module/index.js +9 -0
  272. package/node_modules/which-module/package.json +41 -0
  273. package/node_modules/wrap-ansi/index.js +186 -0
  274. package/node_modules/wrap-ansi/license +9 -0
  275. package/node_modules/wrap-ansi/package.json +61 -0
  276. package/node_modules/wrap-ansi/readme.md +97 -0
  277. package/node_modules/y18n/CHANGELOG.md +35 -0
  278. package/node_modules/y18n/LICENSE +13 -0
  279. package/node_modules/y18n/README.md +109 -0
  280. package/node_modules/y18n/index.js +188 -0
  281. package/node_modules/y18n/package.json +39 -0
  282. package/node_modules/yargs/CHANGELOG.md +420 -0
  283. package/node_modules/yargs/LICENSE +21 -0
  284. package/node_modules/yargs/README.md +140 -0
  285. package/node_modules/yargs/build/lib/apply-extends.d.ts +2 -0
  286. package/node_modules/yargs/build/lib/apply-extends.js +65 -0
  287. package/node_modules/yargs/build/lib/argsert.d.ts +2 -0
  288. package/node_modules/yargs/build/lib/argsert.js +65 -0
  289. package/node_modules/yargs/build/lib/command.d.ts +64 -0
  290. package/node_modules/yargs/build/lib/command.js +416 -0
  291. package/node_modules/yargs/build/lib/common-types.d.ts +36 -0
  292. package/node_modules/yargs/build/lib/common-types.js +25 -0
  293. package/node_modules/yargs/build/lib/completion-templates.d.ts +2 -0
  294. package/node_modules/yargs/build/lib/completion-templates.js +50 -0
  295. package/node_modules/yargs/build/lib/completion.d.ts +21 -0
  296. package/node_modules/yargs/build/lib/completion.js +135 -0
  297. package/node_modules/yargs/build/lib/is-promise.d.ts +1 -0
  298. package/node_modules/yargs/build/lib/is-promise.js +9 -0
  299. package/node_modules/yargs/build/lib/levenshtein.d.ts +1 -0
  300. package/node_modules/yargs/build/lib/levenshtein.js +58 -0
  301. package/node_modules/yargs/build/lib/middleware.d.ts +10 -0
  302. package/node_modules/yargs/build/lib/middleware.js +57 -0
  303. package/node_modules/yargs/build/lib/obj-filter.d.ts +1 -0
  304. package/node_modules/yargs/build/lib/obj-filter.js +14 -0
  305. package/node_modules/yargs/build/lib/parse-command.d.ts +11 -0
  306. package/node_modules/yargs/build/lib/parse-command.js +36 -0
  307. package/node_modules/yargs/build/lib/process-argv.d.ts +2 -0
  308. package/node_modules/yargs/build/lib/process-argv.js +31 -0
  309. package/node_modules/yargs/build/lib/usage.d.ts +49 -0
  310. package/node_modules/yargs/build/lib/usage.js +540 -0
  311. package/node_modules/yargs/build/lib/validation.d.ts +34 -0
  312. package/node_modules/yargs/build/lib/validation.js +330 -0
  313. package/node_modules/yargs/build/lib/yargs.d.ts +274 -0
  314. package/node_modules/yargs/build/lib/yargs.js +1190 -0
  315. package/node_modules/yargs/build/lib/yerror.d.ts +4 -0
  316. package/node_modules/yargs/build/lib/yerror.js +11 -0
  317. package/node_modules/yargs/index.js +40 -0
  318. package/node_modules/yargs/locales/be.json +46 -0
  319. package/node_modules/yargs/locales/de.json +46 -0
  320. package/node_modules/yargs/locales/en.json +51 -0
  321. package/node_modules/yargs/locales/es.json +46 -0
  322. package/node_modules/yargs/locales/fi.json +49 -0
  323. package/node_modules/yargs/locales/fr.json +53 -0
  324. package/node_modules/yargs/locales/hi.json +49 -0
  325. package/node_modules/yargs/locales/hu.json +46 -0
  326. package/node_modules/yargs/locales/id.json +50 -0
  327. package/node_modules/yargs/locales/it.json +46 -0
  328. package/node_modules/yargs/locales/ja.json +51 -0
  329. package/node_modules/yargs/locales/ko.json +49 -0
  330. package/node_modules/yargs/locales/nb.json +44 -0
  331. package/node_modules/yargs/locales/nl.json +49 -0
  332. package/node_modules/yargs/locales/nn.json +44 -0
  333. package/node_modules/yargs/locales/pirate.json +13 -0
  334. package/node_modules/yargs/locales/pl.json +49 -0
  335. package/node_modules/yargs/locales/pt.json +45 -0
  336. package/node_modules/yargs/locales/pt_BR.json +48 -0
  337. package/node_modules/yargs/locales/ru.json +46 -0
  338. package/node_modules/yargs/locales/th.json +46 -0
  339. package/node_modules/yargs/locales/tr.json +48 -0
  340. package/node_modules/yargs/locales/zh_CN.json +48 -0
  341. package/node_modules/yargs/locales/zh_TW.json +47 -0
  342. package/node_modules/yargs/package.json +92 -0
  343. package/node_modules/yargs/yargs.js +14 -0
  344. package/node_modules/yargs-parser/CHANGELOG.md +601 -0
  345. package/node_modules/yargs-parser/LICENSE.txt +14 -0
  346. package/node_modules/yargs-parser/README.md +449 -0
  347. package/node_modules/yargs-parser/index.js +1032 -0
  348. package/node_modules/yargs-parser/lib/tokenize-arg-string.js +40 -0
  349. package/node_modules/yargs-parser/package.json +46 -0
  350. package/package.json +65 -12
  351. package/DSH-Remote-v3.2.4.apk +0 -4
  352. package/manifest.json +0 -21
@@ -0,0 +1,261 @@
1
+ export const DEFAULT_TITLE_REFRESH_BUDGET_MS = 1500;
2
+ export const DEFAULT_TITLE_REFRESH_CONCURRENCY = 4;
3
+
4
+ /**
5
+ * Coalesce title refreshes for one corpus pass.
6
+ *
7
+ * Why a pass folds in **one** host call (issue #20 review, 2026-10-07):
8
+ * `sessionQuery.readTitleSnapshots(ids)` is not a point read. The installed
9
+ * `0.2.0-rc.2` chain is
10
+ *
11
+ * readTitleSnapshots → SessionCorpus.projectMany → listPersisted → persistence.list()
12
+ *
13
+ * and `persistence.list()` walks the whole corpus (per-file header + stat, plus
14
+ * a corpus-wide revision hash while a historical generation is present). Batch
15
+ * size therefore multiplies corpus enumerations: folding four ids at a time
16
+ * cost ceil(N/4) full enumerations on top of the pass. The pass now hands every
17
+ * pending id to one call, so the enumeration count stays constant as N grows;
18
+ * read width inside that call is the host's own `persistedReadConcurrency`
19
+ * (default 4, `SESSION_QUERY_DEFAULT_PERSISTED_INSPECT_CONCURRENCY`, bounded by
20
+ * `projectMany`'s worker pool), and this module adds no per-id fan-out on top of
21
+ * it. Only the plugin's own revision scan is bounded here — it reads no logs.
22
+ *
23
+ * Waiters may leave independently; the shared pass is cancelled only after its
24
+ * last waiter leaves, unless a timed-out response promoted it to deliberate
25
+ * background warm-up. A cancelled pass is retired immediately so the next
26
+ * request starts a live successor instead of feeding the dead queue, while the
27
+ * fold gate keeps a retired pass and its successor from overlapping — an
28
+ * overlap would double the host's read width during cancellation cleanup.
29
+ */
30
+ export const createSessionTitleRefresher = ({
31
+ readRevision,
32
+ readTitles,
33
+ getCachedTitle,
34
+ setCachedTitle,
35
+ onCacheChange = () => {},
36
+ concurrency = DEFAULT_TITLE_REFRESH_CONCURRENCY,
37
+ } = {}) => {
38
+ if (typeof readTitles !== "function") throw new TypeError("readTitles must be a function");
39
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new RangeError("concurrency must be a positive safe integer");
40
+ let currentRun = null;
41
+ let disposed = false;
42
+ // Serializes fold calls across runs: at most one host title call is in flight
43
+ // per refresher, so cancellation cleanup cannot overlap fresh read work.
44
+ let foldTail = Promise.resolve();
45
+
46
+ const foldAll = (ids, signal) => {
47
+ const previous = foldTail;
48
+ let release;
49
+ foldTail = new Promise((resolve) => { release = resolve; });
50
+ return (async () => {
51
+ try {
52
+ if (signal.aborted) throw signal.reason ?? new Error("session title refresh aborted");
53
+ await previous.catch(() => {});
54
+ if (signal.aborted) throw signal.reason ?? new Error("session title refresh aborted");
55
+ return await readTitles(ids, signal);
56
+ } finally {
57
+ release();
58
+ }
59
+ })();
60
+ };
61
+
62
+ const finishRun = (run) => {
63
+ run.done = true;
64
+ if (run.deadlineTimer) {
65
+ clearTimeout(run.deadlineTimer);
66
+ run.deadlineTimer = null;
67
+ }
68
+ if (currentRun === run) currentRun = null;
69
+ run.resolve();
70
+ };
71
+
72
+ // issue #28 复核 2:**run 级**有限寿命(源级政策)。
73
+ // 调用者级的 `backgroundOnBudget: false` 只约束「谁不提升」——挡不住后来加入的 HTTP 调用者
74
+ // 在自己的响应预算到点时把 run 提升为**永久后台**;坏源一旦永生,之后健康的请求会一直 join 它
75
+ // (只拿到短码),失败隔离就没有闭环。故把截止放在 run 上:到点即中止并退休,**不看 background**。
76
+ const armRunDeadline = (run) => {
77
+ if (!Number.isFinite(run.deadlineAt) || run.deadlineTimer) return;
78
+ const remaining = Math.max(0, run.deadlineAt - performance.now());
79
+ run.deadlineTimer = setTimeout(() => {
80
+ run.deadlineTimer = null;
81
+ if (run.done || run.controller.signal.aborted) return;
82
+ run.controller.abort(new Error("session-title refresh run exceeded its deadline"));
83
+ if (currentRun === run) currentRun = null;
84
+ }, remaining);
85
+ run.deadlineTimer.unref?.();
86
+ };
87
+
88
+ const processRun = async (run) => {
89
+ const { signal } = run.controller;
90
+ try {
91
+ while (!disposed && !signal.aborted && run.queue.length > 0) {
92
+ // Drain everything queued so far into this round: revisions first
93
+ // (bounded workers, no log reads), then a single fold call.
94
+ const pending = run.queue.splice(0, run.queue.length);
95
+ const misses = [];
96
+ let cursor = 0;
97
+ const scanRevisions = async () => {
98
+ for (;;) {
99
+ if (disposed || signal.aborted) return;
100
+ const index = cursor;
101
+ cursor += 1;
102
+ if (index >= pending.length) return;
103
+ const id = pending[index];
104
+ let revision = null;
105
+ if (typeof readRevision === "function") {
106
+ try {
107
+ const observed = await readRevision(id, signal);
108
+ revision = typeof observed === "string" && observed !== "" ? observed : null;
109
+ } catch (error) {
110
+ if (signal.aborted) throw error;
111
+ }
112
+ }
113
+ if (disposed || signal.aborted) return;
114
+ const cached = getCachedTitle?.(id, revision);
115
+ if (cached?.hit) run.results.set(id, cached.title);
116
+ else misses.push({ index, id, revision });
117
+ }
118
+ };
119
+ await Promise.all(Array.from(
120
+ { length: Math.min(concurrency, pending.length) },
121
+ () => scanRevisions(),
122
+ ));
123
+ if (disposed || signal.aborted) break;
124
+ // Worker completion order is not the request order; keep the fold
125
+ // deterministic (and stable for the host's first-occurrence ordering).
126
+ misses.sort((a, b) => a.index - b.index);
127
+
128
+ if (misses.length > 0) {
129
+ let folded;
130
+ try {
131
+ folded = await foldAll(misses.map(({ id }) => id), signal);
132
+ } catch (error) {
133
+ if (signal.aborted) throw error;
134
+ folded = new Map();
135
+ }
136
+ if (disposed || signal.aborted) break;
137
+ for (const { id, revision } of misses) {
138
+ const result = folded?.get?.(id);
139
+ if (!result?.ok) continue;
140
+ const title = typeof result.title === "string" && result.title !== "" ? result.title : null;
141
+ setCachedTitle?.(id, revision, title);
142
+ run.results.set(id, title);
143
+ }
144
+ }
145
+ onCacheChange();
146
+ }
147
+ } catch {
148
+ // An aborted or faulty refresh is a cache miss, never an endpoint failure.
149
+ } finally {
150
+ finishRun(run);
151
+ }
152
+ };
153
+
154
+ const addIds = (run, ids) => {
155
+ for (const id of ids) {
156
+ if (typeof id !== "string" || id === "" || run.seen.has(id)) continue;
157
+ run.seen.add(id);
158
+ run.queue.push(id);
159
+ }
160
+ };
161
+
162
+ const createRun = (ids) => {
163
+ let resolve;
164
+ const promise = new Promise((done) => { resolve = done; });
165
+ const run = {
166
+ controller: new AbortController(),
167
+ queue: [],
168
+ seen: new Set(),
169
+ results: new Map(),
170
+ waiters: 0,
171
+ background: false,
172
+ deadlineAt: Infinity,
173
+ deadlineTimer: null,
174
+ done: false,
175
+ promise,
176
+ resolve,
177
+ };
178
+ addIds(run, ids);
179
+ currentRun = run;
180
+ void processRun(run);
181
+ return run;
182
+ };
183
+
184
+ const refresh = async (ids, { signal, budgetMs = DEFAULT_TITLE_REFRESH_BUDGET_MS, backgroundOnBudget = true, runDeadlineMs } = {}) => {
185
+ if (disposed || signal?.aborted) return new Map();
186
+ const uniqueIds = [...new Set(ids)].filter((id) => typeof id === "string" && id !== "");
187
+ if (uniqueIds.length === 0) return new Map();
188
+ if (!Number.isFinite(budgetMs) || budgetMs < 0) throw new RangeError("budgetMs must be a non-negative finite number");
189
+
190
+ // A run whose upstream was already aborted can no longer consume its queue:
191
+ // joining it would park this request's ids in a dead queue and hand back a
192
+ // partial result. Retire it and start a live successor instead; the fold
193
+ // gate keeps the successor's work behind the retiring pass's cleanup.
194
+ let run = currentRun;
195
+ if (!run || run.done || run.controller.signal.aborted) run = createRun(uniqueIds);
196
+ else addIds(run, uniqueIds);
197
+ // issue #28 复核 2:run 级截止(取所有声明中**最早**者——只收紧、不放松)。
198
+ // 它不因后来调用者的 background promotion 而失效:到点由 armRunDeadline 退休该 run。
199
+ if (Number.isFinite(runDeadlineMs) && runDeadlineMs >= 0) {
200
+ const nextAt = performance.now() + runDeadlineMs;
201
+ // 只在**收紧**时重设:后来者更晚的声明不得延长期限(issue #28 复核 3)。
202
+ if (nextAt < run.deadlineAt) {
203
+ run.deadlineAt = nextAt;
204
+ // 旧 timer 必须先清掉再重设——armRunDeadline 见到已有 deadlineTimer 会直接返回,
205
+ // 那样「收紧」就静默失效(评审实测:先声明 300ms、后声明 20ms,源仍按 300ms 才取消)。
206
+ if (run.deadlineTimer) {
207
+ clearTimeout(run.deadlineTimer);
208
+ run.deadlineTimer = null;
209
+ }
210
+ armRunDeadline(run);
211
+ }
212
+ }
213
+ run.waiters += 1;
214
+ let released = false;
215
+ const release = () => {
216
+ if (released) return;
217
+ released = true;
218
+ run.waiters -= 1;
219
+ if (!run.done && !run.background && run.waiters === 0) {
220
+ run.controller.abort(new Error("all session-title refresh waiters left"));
221
+ }
222
+ };
223
+
224
+ let timer;
225
+ let abortHandler;
226
+ const interrupted = new Promise((resolve) => {
227
+ if (signal) {
228
+ abortHandler = () => resolve("aborted");
229
+ signal.addEventListener("abort", abortHandler, { once: true });
230
+ }
231
+ timer = setTimeout(() => resolve("budget"), budgetMs);
232
+ });
233
+ const outcome = await Promise.race([
234
+ run.promise.then(() => "done"),
235
+ interrupted,
236
+ ]);
237
+ clearTimeout(timer);
238
+ if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
239
+
240
+ // issue #28 复核 BLOCKING 2:调用方可以要求**有限生命期**。默认(响应路径)预算到点会把
241
+ // run 提升为「后台预热」并长期存活;但启动预热**不能**这样——否则一次挂住的预热会让之后
242
+ // 健康的请求一直 join 这个坏 run、只拿到短码。传 `backgroundOnBudget: false` 时不做提升,
243
+ // 于是下面的 release() 在没有其它等待者时会中止该 run(退休),健康请求随后可重新开始。
244
+ if (outcome === "budget" && backgroundOnBudget && !signal?.aborted && !run.done) run.background = true;
245
+ const aborted = signal?.aborted === true;
246
+ release();
247
+ return aborted ? new Map() : new Map(run.results);
248
+ };
249
+
250
+ return {
251
+ refresh,
252
+ dispose() {
253
+ disposed = true;
254
+ currentRun?.controller.abort(new Error("session-title refresher disposed"));
255
+ },
256
+ /** Test seam: resolve when the current single-flight pass settles. */
257
+ whenIdle() {
258
+ return currentRun?.promise ?? Promise.resolve();
259
+ },
260
+ };
261
+ };
@@ -0,0 +1,20 @@
1
+ // 跨平台子进程调用辅助(issue #39 Windows 修复)。
2
+ //
3
+ // Windows 上 npm/pnpm 是 `npm.cmd`/`pnpm.cmd`:Node 在 `shell: false` 下用 CreateProcess
4
+ // 无法解析 `.cmd`(只会自动补 `.exe`),直接 spawn 裸命令名会 ENOENT。因此在 win32 上
5
+ // 必须经 shell 调用,并对含空白的参数(典型是 profile/临时目录路径)做 cmd 引用。
6
+ //
7
+ // 本模块保持零依赖,避免 npm-update-source ↔ plugin-self-update 的循环引用。
8
+
9
+ /** 仅 Windows 需要 `shell: true`;POSIX 保持直接 spawn(更可控、无 shell 注入面)。 */
10
+ export function shellOptions(platform = process.platform) {
11
+ return platform === "win32" ? { shell: true } : {};
12
+ }
13
+
14
+ /** Windows shell 下给含空白/元字符的参数加引号;POSIX 原样返回。 */
15
+ export function quoteShellArgument(value, platform = process.platform) {
16
+ const text = String(value ?? "");
17
+ if (platform !== "win32" || text === "") return text;
18
+ if (!/[\s"&|<>^]/.test(text)) return text;
19
+ return `"${text.replace(/"/g, '""')}"`;
20
+ }
@@ -0,0 +1,24 @@
1
+ // Host credentials are scoped to the explicit loopback origin and update routes, not to arbitrary release URLs.
2
+ export async function fetchUpdateResource(url, {
3
+ fetchImpl = fetch, token = '', trustedOrigin = '', basePath = '/m', headers = {}, signal, maxRedirects = 5,
4
+ } = {}) {
5
+ // 精确路径集合:同 origin 的其它处理器(如 /other-plugin/api/update/manifest)不得拿到口令。
6
+ const mount = basePath === '/' || basePath === '' ? '/m' : basePath.replace(/\/$/, '');
7
+ const allowedPaths = new Set([`${mount}/api/update/manifest`, `${mount}/api/update/plugin`, `${mount}/api/update/apk`]);
8
+ let target = new URL(url);
9
+ for (let hops = 0; hops <= maxRedirects; hops++) {
10
+ if (target.username || target.password || !(['https:'].includes(target.protocol) ||
11
+ (target.protocol === 'http:' && ['127.0.0.1','localhost','[::1]'].includes(target.hostname)))) throw new Error('update-url-not-allowed');
12
+ const authAllowed = Boolean(token) && trustedOrigin !== '' && target.origin === trustedOrigin &&
13
+ allowedPaths.has(target.pathname);
14
+ const response = await fetchImpl(target.toString(), {
15
+ headers: { ...headers, ...(authAllowed ? { 'x-mobile-token': token } : {}) }, redirect: 'manual', signal,
16
+ });
17
+ if (![301,302,303,307,308].includes(response.status)) return { response, finalUrl: target.toString() };
18
+ const location = response.headers.get('location');
19
+ if (!location) throw new Error('update-redirect-missing');
20
+ await response.body?.cancel?.();
21
+ target = new URL(location, target);
22
+ }
23
+ throw new Error('update-redirect-limit');
24
+ }
@@ -0,0 +1,170 @@
1
+ // Runs only under an OS flock on a persistent inode. No lock-directory unlink/rename races.
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, rmSync, symlinkSync, realpathSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ function read(file) { try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; } }
7
+ function atomic(file, value) {
8
+ mkdirSync(dirname(file), { recursive: true });
9
+ const tmp = `${file}.${randomUUID()}.tmp`;
10
+ try { writeFileSync(tmp, JSON.stringify(value) + '\n', { mode: 0o600 }); renameSync(tmp, file); }
11
+ finally { if (existsSync(tmp)) rmSync(tmp); }
12
+ }
13
+ function identity(pid) {
14
+ try { const text = readFileSync(`/proc/${pid}/stat`, 'utf8'); return { pid, start: text.slice(text.lastIndexOf(')') + 2).split(' ')[19] }; }
15
+ catch { return { pid, start: null }; }
16
+ }
17
+ function alive(person) {
18
+ if (!person?.pid || !Number.isInteger(person.pid)) return false;
19
+ try { process.kill(person.pid, 0); } catch(e) { return e.code === 'EPERM'; }
20
+ return !person.start || identity(person.pid).start === person.start;
21
+ }
22
+ function owns(holder, lease) { return !!lease?.generation && holder?.generation === lease.generation && holder.opId === lease.opId; }
23
+ function fence(holder, lease) { if (!owns(holder, lease)) throw new Error('lease-lost'); }
24
+ function terminal(phase) { return ['settled', 'rolled-back', 'failed'].includes(phase); }
25
+
26
+ function execute(input) {
27
+ const { lockPath, operation, lease, stateFile } = input;
28
+ const holderFile = join(lockPath, 'holder.json');
29
+ let holder = read(holderFile);
30
+ const available = !holder || ![holder.worker, holder.supervisor].some(alive);
31
+ if (operation === 'acquire') {
32
+ if (holder && owns(holder, lease)) return { ok: true, lease, holder, reentrant: true };
33
+ // Missing publication is not evidence of owner death. Legacy half-locks require manual repair.
34
+ if (existsSync(lockPath) && !existsSync(holderFile)) return { ok: false, error: 'lock-unpublished' };
35
+ if (!available) return { ok: false, error: 'update-busy', holder };
36
+ if (input.stateFile) {
37
+ const tx = read(input.stateFile);
38
+ // 安装/待生效/回滚未完成时,**任何**新操作都不得开始——即使锁已被误释放(holder 为空)。
39
+ if (tx && ['installing','restart-pending','rolling-back'].includes(tx.phase) &&
40
+ (!lease || tx.lease?.generation !== lease.generation)) {
41
+ return { ok: false, error: 'recovery-required', holder, transaction: tx };
42
+ }
43
+ }
44
+ holder = { opId: input.opId, generation: randomUUID(), worker: identity(input.pid), pid: input.pid, at: input.now };
45
+ mkdirSync(lockPath, { recursive: true }); atomic(holderFile, holder);
46
+ return { ok: true, holder, lease: { opId: holder.opId, generation: holder.generation } };
47
+ }
48
+ if (operation === 'release') {
49
+ if (!owns(holder, lease)) return { ok: false, error: 'lease-lost' };
50
+ const tx = input.stateFile ? read(input.stateFile) : null;
51
+ if (tx && tx.lease?.generation === lease.generation && ['installing','restart-pending','rolling-back'].includes(tx.phase)) {
52
+ return { ok: false, error: 'recovery-required' };
53
+ }
54
+ atomic(holderFile, null); return { ok: true };
55
+ }
56
+ if (operation === 'handover') {
57
+ fence(holder, lease);
58
+ holder = { ...holder, supervisor: identity(input.toPid), pid: input.toPid };
59
+ atomic(holderFile, holder); return { ok: true, holder };
60
+ }
61
+ if (operation === 'assert') { fence(holder, lease); return { ok: true, holder }; }
62
+ if (operation === 'state-write') {
63
+ fence(holder, lease);
64
+ if (input.tx.opId !== lease.opId) throw new Error('operation-mismatch');
65
+ const old = read(stateFile);
66
+ if (old?.lease && old.lease.generation !== lease.generation && !terminal(old.phase)) throw new Error('transaction-generation-mismatch');
67
+ // 阶段 CAS:旧 worker 不能把"已被别人推进的阶段"改回去(如 failed → installing)。
68
+ if (old && old.opId === input.tx.opId) {
69
+ if (input.expectedPhases && !input.expectedPhases.includes(old.phase)) throw new Error('phase-cas-failed');
70
+ if (terminal(old.phase) && !terminal(input.tx.phase)) throw new Error('terminal-regression');
71
+ }
72
+ const next = { ...input.tx, lease, id: lease.generation, updatedAt: new Date().toISOString() };
73
+ atomic(stateFile, next);
74
+ if (input.currentFile && input.currentFile !== stateFile) atomic(input.currentFile, next);
75
+ return { ok: true, transaction: next };
76
+ }
77
+ if (operation === 'switch-profile') {
78
+ fence(holder, lease);
79
+ const file = join(input.profileDir, 'package.json'), doc = read(file);
80
+ const key = input.key || 'dsh-mobile-remote';
81
+ const previous = doc?.dependencies?.[key];
82
+ if (typeof previous !== 'string') throw new Error('profile-dependency-missing');
83
+ doc.dependencies[key] = input.specifier; atomic(file, doc);
84
+ return { ok: true, previous, next: input.specifier };
85
+ }
86
+ if (operation === 'restore-profile') {
87
+ fence(holder, lease);
88
+ const file = join(input.profileDir, 'package.json');
89
+ const doc = input.snapshot?.packageJson ? JSON.parse(input.snapshot.packageJson) : read(file);
90
+ if (!doc?.dependencies) throw new Error('profile-dependencies-missing');
91
+ const key = input.key || 'dsh-mobile-remote';
92
+ if (input.preserveSpecifier === true) {
93
+ if (typeof doc.dependencies[key] !== 'string' || doc.dependencies[key] === '') throw new Error('profile-dependency-missing');
94
+ } else {
95
+ doc.dependencies[key] = `file:${input.snapshotDir}`;
96
+ }
97
+ atomic(file, doc);
98
+ const lockfile = join(input.profileDir, 'pnpm-lock.yaml');
99
+ if (typeof input.snapshot?.pnpmLock === 'string') writeFileSync(lockfile, input.snapshot.pnpmLock, { mode: 0o600 });
100
+ else if (input.snapshot?.pnpmLock === null && existsSync(lockfile)) rmSync(lockfile);
101
+ return { ok: true };
102
+ }
103
+ if (operation === 'publish-snapshot-link') {
104
+ fence(holder, lease);
105
+ const modules = join(input.profileDir, 'node_modules'); mkdirSync(modules, { recursive: true });
106
+ // Never traverse an unexpected node_modules symlink into another profile.
107
+ if (realpathSync(modules) !== join(realpathSync(input.profileDir), 'node_modules')) throw new Error('profile-modules-outside-root');
108
+ const key = input.key || 'dsh-mobile-remote';
109
+ if (!/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/.test(key)) throw new Error('dependency-key-invalid');
110
+ const segments = key.split('/');
111
+ const parent = join(modules, ...segments.slice(0, -1));
112
+ mkdirSync(parent, { recursive: true });
113
+ const relativeParent = relative(realpathSync(modules), realpathSync(parent));
114
+ if (relativeParent === '..' || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) throw new Error('profile-package-scope-outside-root');
115
+ const target = join(parent, segments.at(-1));
116
+ const staged = `${target}.restore-${randomUUID()}`, previous = `${target}.previous-${randomUUID()}`;
117
+ symlinkSync(realpathSync(input.snapshotDir), staged, 'dir');
118
+ let moved = false;
119
+ try {
120
+ try { renameSync(target, previous); moved = true; } catch(e) { if (e.code !== 'ENOENT') throw e; }
121
+ renameSync(staged, target);
122
+ } catch(e) { if (moved && !existsSync(target)) renameSync(previous, target); throw e; }
123
+ finally { if (existsSync(staged)) rmSync(staged); }
124
+ // Retain the quarantined old link/tree; never delete active hardlinked module files in place.
125
+ return { ok: true, installedRoot: target };
126
+ }
127
+ if (operation === 'claim-recovery') {
128
+ fence(holder, lease);
129
+ const tx = read(stateFile);
130
+ if (!tx || tx.opId !== lease.opId || tx.lease?.generation !== lease.generation) throw new Error('operation-mismatch');
131
+ if (tx.phase !== 'rolling-back') throw new Error('not-recovering');
132
+ // 同代的两个监督器只能有一个执行恢复:先到者写 claim,后到者拿到 claimed。
133
+ if (tx.recoveryClaim && tx.recoveryClaim.owner !== input.owner) return { ok: false, error: 'recovery-claimed', transaction: tx };
134
+ const next = { ...tx, recoveryClaim: { owner: input.owner, at: input.now } };
135
+ atomic(stateFile, next);
136
+ if (input.currentFile && input.currentFile !== stateFile) atomic(input.currentFile, next);
137
+ return { ok: true, transaction: next };
138
+ }
139
+ if (operation === 'finish-terminal') {
140
+ fence(holder, lease);
141
+ const tx = read(stateFile);
142
+ if (!tx || tx.opId !== lease.opId || tx.lease?.generation !== lease.generation) throw new Error('operation-mismatch');
143
+ if (input.expectedPhases && !input.expectedPhases.includes(tx.phase)) throw new Error('phase-cas-failed');
144
+ if (!terminal(input.phase)) throw new Error('not-a-terminal-phase');
145
+ const next = { ...tx, phase: input.phase, result: input.result, lease, id: lease.generation, updatedAt: new Date().toISOString() };
146
+ atomic(stateFile, next);
147
+ if (input.currentFile && input.currentFile !== stateFile) atomic(input.currentFile, next);
148
+ // 发布终态与释放锁是一次仲裁:中间不存在"已终态但锁仍被旧代持有/已被释放"的空窗。
149
+ atomic(holderFile, null);
150
+ return { ok: true, transaction: next };
151
+ }
152
+ if (operation === 'converge') {
153
+ const tx = read(stateFile);
154
+ if (!tx) return { ok: true, action: 'none', transaction: null };
155
+ if (terminal(tx.phase)) return { ok: true, action: 'terminal', transaction: tx };
156
+ if (input.expectedLease && !owns(holder, input.expectedLease)) return { ok: true, action: 'superseded', transaction: tx };
157
+ if (holder && [holder.worker, holder.supervisor].some(alive)) return { ok: true, action: 'active', transaction: tx };
158
+ if (existsSync(lockPath) && !existsSync(holderFile)) return { ok: true, action: 'recent', transaction: tx };
159
+ // Never relabel a possibly damaged install as an ordinary terminal failure.
160
+ if (['installing','restart-pending','rolling-back'].includes(tx.phase)) return { ok: true, action: 'recovery-required', transaction: tx };
161
+ if (input.now - Date.parse(tx.startedAt) < input.staleMs) return { ok: true, action: 'recent', transaction: tx };
162
+ if (tx.lease && holder && !owns(holder, tx.lease)) return { ok: true, action: 'superseded', transaction: tx };
163
+ const next = { ...tx, phase: 'failed', result: { ok: false, reason: 'holder-gone' } };
164
+ atomic(stateFile, next); if (holder) atomic(holderFile, null);
165
+ return { ok: true, action: 'converged', transaction: next };
166
+ }
167
+ throw new Error('unknown-arbitration-operation');
168
+ }
169
+ try { console.log(JSON.stringify(execute(JSON.parse(readFileSync(0, 'utf8'))))); }
170
+ catch(e) { console.log(JSON.stringify({ ok: false, error: e.message })); }