@marktrue/dsh-mobile-remote 3.2.4-alpha.3-app → 3.2.4-alpha.3

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 +1439 -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,601 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { lstat, realpath } from "node:fs/promises";
4
+ import { createBundledGitReadProvider } from "./git-read-bundled-provider.js";
5
+
6
+ const MAX_GRAPH_TIPS = 5;
7
+ const MAX_TRACKED_FILES = 200;
8
+ const FILE_HISTORY_DEFAULT_LIMIT = 20;
9
+ const FILE_HISTORY_MAX_LIMIT = 100;
10
+ const FILE_HISTORY_MAX_COMMITS = 5000;
11
+ const FILE_HISTORY_TIMEOUT_MS = 20_000;
12
+ const MAX_FILE_HISTORY_SNAPSHOTS = 8;
13
+ const REF_KIND_ORDER = { head: 0, localBranch: 1, remoteBranch: 2, other: 3 };
14
+ const FILE_HISTORY_REASONS = new Set(["not-tracked", "no-history", "path-invalid"]);
15
+
16
+ function failure(code, status, message = code) {
17
+ const error = new Error(message);
18
+ error.code = code;
19
+ error.status = status;
20
+ return error;
21
+ }
22
+
23
+ // The provider contract is additive: v1.1.0 providers expose the new methods,
24
+ // while legacy v1.0.0 providers stay compatible and their raw ref strings are
25
+ // projected into the structured DTO by this facade.
26
+ const providerCompatible = (provider) => /^1\.\d+\.\d+$/.test(String(provider?.contractVersion ?? ""))
27
+ && ["repositoryRoot", "branches", "resolveTip", "graph", "commit"].every((method) => typeof provider[method] === "function");
28
+
29
+ const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
30
+ const refShortName = (name) => name === "HEAD" ? "HEAD" : String(name).replace(/^refs\/(?:heads|remotes|tags)\//, "");
31
+ const trimSubject = (value) => [...String(value)].slice(0, 200).join("");
32
+ const emptyRefDetails = () => ({ refsDetail: [], tagsDetail: [] });
33
+ const sortRefDetails = (rows) => rows.sort((left, right) => (REF_KIND_ORDER[left.kind] ?? 9) - (REF_KIND_ORDER[right.kind] ?? 9) || compareText(left.name, right.name));
34
+ const sortTagDetails = (rows) => rows.sort((left, right) => compareText(left.name, right.name));
35
+
36
+ /** Project provider-owned ref records onto the commit they resolve to. */
37
+ function refDetailsFromRecords(records, commitOid, { fullTags = false } = {}) {
38
+ const target = String(commitOid ?? "").toLowerCase();
39
+ const refs = [], tags = [], seen = new Set();
40
+ for (const record of Array.isArray(records) ? records : []) {
41
+ if (!record || String(record.commitOid ?? "").toLowerCase() !== target) continue;
42
+ const name = typeof record.name === "string" ? record.name : "";
43
+ if (!name) continue;
44
+ const shortName = typeof record.shortName === "string" && record.shortName ? record.shortName : refShortName(name);
45
+ if (record.kind === "tag") {
46
+ const key = `tag\0${shortName}`;
47
+ if (seen.has(key)) continue;
48
+ seen.add(key);
49
+ const tag = { name, shortName, oid: target, annotated: record.annotated === true };
50
+ if (fullTags) {
51
+ if (typeof record.tagger === "string" && record.tagger) tag.tagger = record.tagger;
52
+ const date = Number(record.date);
53
+ if (Number.isFinite(date) && date > 0) tag.date = date;
54
+ if (typeof record.subject === "string" && record.subject) tag.subject = trimSubject(record.subject);
55
+ if (typeof record.hint === "string" && record.hint) tag.hint = record.hint;
56
+ }
57
+ tags.push(tag);
58
+ continue;
59
+ }
60
+ const kind = ["head", "localBranch", "remoteBranch"].includes(record.kind) ? record.kind : "other";
61
+ const key = `${kind}\0${shortName}`;
62
+ if (seen.has(key)) continue;
63
+ seen.add(key);
64
+ const detail = { kind, name, shortName, oid: target };
65
+ if (record.current === true) detail.current = true;
66
+ refs.push(detail);
67
+ }
68
+ return { refsDetail: sortRefDetails(refs), tagsDetail: sortTagDetails(tags) };
69
+ }
70
+
71
+ /**
72
+ * String fallback for legacy providers: parse raw decorations such as
73
+ * `HEAD -> refs/heads/main`, `refs/remotes/origin/main` and
74
+ * `tag: refs/tags/v1.0.0`. Annotated metadata is unavailable here, so tags
75
+ * are advertised as lightweight name-level entries.
76
+ */
77
+ function refDetailsFromLegacyRefs(rawRefs, commitOid, { tags = [] } = {}) {
78
+ const target = String(commitOid ?? "");
79
+ const refs = [], tagRows = [], seen = new Set();
80
+ let currentBranch = null;
81
+ const addRef = (kind, name, shortName, extra = {}) => {
82
+ const key = `${kind}\0${shortName}`;
83
+ if (seen.has(key)) return;
84
+ seen.add(key);
85
+ refs.push({ kind, name, shortName, oid: target, ...extra });
86
+ };
87
+ const addTag = (value) => {
88
+ let name = String(value ?? "").trim();
89
+ if (name.startsWith("tag: ")) name = name.slice(5).trim();
90
+ if (!name) return;
91
+ if (!name.startsWith("refs/tags/")) name = `refs/tags/${name}`;
92
+ const shortName = name.slice(10);
93
+ if (!shortName) return;
94
+ const key = `tag\0${shortName}`;
95
+ if (seen.has(key)) return;
96
+ seen.add(key);
97
+ tagRows.push({ name, shortName, oid: target, annotated: false });
98
+ };
99
+ const addRefValue = (value) => {
100
+ const raw = String(value ?? "").trim();
101
+ if (!raw) return;
102
+ if (raw === "HEAD") return addRef("head", "HEAD", "HEAD", { current: true });
103
+ if (raw.startsWith("HEAD -> ")) {
104
+ addRef("head", "HEAD", "HEAD", { current: true });
105
+ const next = raw.slice(8).trim();
106
+ if (!next) return undefined;
107
+ // Providers may decorate with shortened branch names (`HEAD -> main`)
108
+ // instead of full ref names; normalize the branch form here. The target
109
+ // of HEAD is by definition the current branch, marked after parsing.
110
+ if (next.startsWith("refs/heads/")) { currentBranch = next.slice(11); return addRef("localBranch", next, currentBranch); }
111
+ if (next.startsWith("refs/")) return addRefValue(next);
112
+ currentBranch = next;
113
+ return addRef("localBranch", `refs/heads/${next}`, next);
114
+ }
115
+ if (raw.startsWith("tag: ")) return addTag(raw.slice(5));
116
+ if (raw === "refs/stash") return;
117
+ if (raw.startsWith("refs/heads/")) return addRef("localBranch", raw, raw.slice(11));
118
+ if (raw.startsWith("refs/remotes/")) return raw.endsWith("/HEAD") ? undefined : addRef("remoteBranch", raw, raw.slice(13));
119
+ if (raw.startsWith("refs/tags/")) return addTag(raw);
120
+ return addRef("other", raw, raw);
121
+ };
122
+ for (const raw of Array.isArray(rawRefs) ? rawRefs : []) addRefValue(raw);
123
+ for (const raw of Array.isArray(tags) ? tags : []) addTag(raw);
124
+ // `current` is set once HEAD has been seen, so decoration order does not matter.
125
+ if (currentBranch !== null) {
126
+ const row = refs.find((entry) => entry.kind === "localBranch" && entry.shortName === currentBranch);
127
+ if (row) row.current = true;
128
+ }
129
+ return { refsDetail: sortRefDetails(refs), tagsDetail: sortTagDetails(tagRows) };
130
+ }
131
+
132
+ const mergeRefDetails = (primary, secondary) => {
133
+ const refs = new Map(), tags = new Map();
134
+ for (const row of [...primary.refsDetail, ...secondary.refsDetail]) {
135
+ const key = `${row.kind}\0${row.shortName}`;
136
+ if (!refs.has(key)) refs.set(key, row);
137
+ }
138
+ for (const row of [...primary.tagsDetail, ...secondary.tagsDetail]) {
139
+ if (!tags.has(row.shortName)) tags.set(row.shortName, row);
140
+ }
141
+ return { refsDetail: sortRefDetails([...refs.values()]), tagsDetail: sortTagDetails([...tags.values()]) };
142
+ };
143
+
144
+ const HISTORY_STATUS_WORDS = { added: "A", modified: "M", deleted: "D", renamed: "R", copied: "C", "type-changed": "T" };
145
+ const historyStatus = (value) => {
146
+ const text = String(value ?? "").trim();
147
+ const word = HISTORY_STATUS_WORDS[text.toLowerCase()];
148
+ if (word) return word;
149
+ const upper = text.toUpperCase();
150
+ return ["A", "M", "D", "R", "C", "T"].includes(upper[0]) ? upper[0] : "M";
151
+ };
152
+
153
+ /** Session-authorized read facade over an optional external or bundled provider. */
154
+ export function createGitReadService(ctx, { bundledProvider, now = () => Date.now(), snapshotTtlMs = 5 * 60_000, pollIntervalMs = 30_000, onChanged } = {}) {
155
+ const get = (name) => { try { return ctx.get?.(name); } catch { return undefined; } };
156
+ const external = get("gitReadProvider");
157
+ let externalAvailable = false;
158
+ try { externalAvailable = providerCompatible(external) && external.capabilities?.().available === true; } catch {}
159
+ const bundled = bundledProvider ?? createBundledGitReadProvider(ctx);
160
+ const provider = externalAvailable ? external : bundled;
161
+ const bindings = new Map();
162
+ const repositoryIdFor = (sessionId, root) => `repo_${createHash("sha256").update(`${sessionId}\0${root}`).digest("base64url").slice(0, 32)}`;
163
+ const inside = (child, parent) => {
164
+ const path = relative(resolve(parent), resolve(child));
165
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
166
+ };
167
+ const authorizedRoot = async (cwd) => {
168
+ if (typeof cwd !== "string" || !cwd) throw failure("repository-not-authorized", 403);
169
+ try {
170
+ const canonicalCwd = await realpath(cwd);
171
+ const workspacePaths = get("workspaceRegistry")?.list?.()?.map((entry) => entry?.path) ?? [];
172
+ const workspaces = await Promise.all(workspacePaths.filter((path) => typeof path === "string").map((path) => realpath(path).catch(() => null)));
173
+ if (!workspaces.some((path) => path && inside(canonicalCwd, path))) throw failure("repository-not-authorized", 403);
174
+ const root = await realpath(await provider.repositoryRoot(canonicalCwd));
175
+ if (!inside(canonicalCwd, root) || !workspaces.some((path) => path && inside(root, path))) throw failure("repository-not-authorized", 403);
176
+ return root;
177
+ } catch (error) {
178
+ if (["git-provider-unavailable", "not-git-repository"].includes(error?.code)) throw failure(error.code, error.status);
179
+ throw failure("repository-not-authorized", 403);
180
+ }
181
+ };
182
+ const session = (sessionId) => {
183
+ if (typeof sessionId !== "string" || sessionId === "") throw failure("session-not-found", 404);
184
+ const value = get("sessions")?.get?.(sessionId);
185
+ if (!value) throw failure("session-not-found", 404);
186
+ return value;
187
+ };
188
+ const binding = async (sessionId, repositoryId) => {
189
+ const value = bindings.get(repositoryId);
190
+ if (!value || value.sessionId !== sessionId) throw failure("repository-not-authorized", 403);
191
+ const current = session(sessionId);
192
+ const currentRoot = await authorizedRoot(current?.header?.cwd);
193
+ if (currentRoot !== value.root) throw failure("repository-not-authorized", 403);
194
+ return value;
195
+ };
196
+ const worktreeSnapshots = new Map();
197
+ const snapshots = new Map();
198
+ const fileCursors = new Map();
199
+ const historySnapshots = new Map();
200
+ const historyCursors = new Map();
201
+ const refSignatures = new Map();
202
+ const keepBounded = (map, maximum) => {
203
+ while (map.size > maximum) map.delete(map.keys().next().value);
204
+ };
205
+ const prune = () => {
206
+ for (const [id, item] of snapshots) if (item.expiresAt <= now()) snapshots.delete(id);
207
+ for (const [id, item] of fileCursors) if (item.expiresAt <= now()) fileCursors.delete(id);
208
+ for (const [id, item] of historySnapshots) if (item.expiresAt <= now()) historySnapshots.delete(id);
209
+ for (const [id, item] of historyCursors) if (item.expiresAt <= now()) historyCursors.delete(id);
210
+ };
211
+ let pollTimer = null;
212
+ let unsubscribeProvider = null;
213
+ const validOid = (value) => typeof value === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
214
+ const safeFilePath = (value) => typeof value === "string" && value !== "" && !isAbsolute(value) && !/^(?:[A-Za-z]:[\\/]|\\\\|[a-z][a-z\d+.-]*:\/\/)/i.test(value) && !value.split(/[\\/]/).includes("..") && !value.includes("\0");
215
+ const validRef = (value) => value === "HEAD" || (typeof value === "string" && /^refs\/(?:heads|remotes)\/[A-Za-z0-9._/-]+$/.test(value) && !value.includes("..") && !value.includes("//") && !value.includes("@{") && !/(^|\/)\.|\/$|\.lock(?:\/|$)|[/.]$/.test(value));
216
+ const stale = () => failure("graph-stale", 409);
217
+ const hasNewFileMethods = (candidate) => typeof candidate?.files === "function" && typeof candidate?.fileHistory === "function";
218
+ const bundledProviderReady = () => {
219
+ try { return bundled.capabilities?.().available === true; } catch { return false; }
220
+ };
221
+ // Truthful capability: only the ACTIVE provider may serve the new endpoints.
222
+ // A legacy external provider must not be mixed with bundled data (its graph
223
+ // and commit facts come from a different source); a bundled provider also
224
+ // needs its Git subprocess to be usable.
225
+ const fileHistoryFeature = () => {
226
+ if (!hasNewFileMethods(provider)) return false;
227
+ if (provider !== bundled) return true;
228
+ return bundledProviderReady();
229
+ };
230
+ const fileReader = () => {
231
+ if (!hasNewFileMethods(provider)) throw failure("provider-incompatible", 503);
232
+ return provider;
233
+ };
234
+ const readRefRecords = async (root) => {
235
+ if (typeof provider.refs !== "function") return null;
236
+ try {
237
+ const records = await provider.refs(root);
238
+ return Array.isArray(records) ? records : null;
239
+ } catch { return null; }
240
+ };
241
+ const capabilities = () => {
242
+ const base = provider.capabilities?.() ?? { available: true, reason: null };
243
+ return {
244
+ contractVersion: "1.1.0", available: base.available !== false,
245
+ reason: ["git-provider-unavailable", "workspace-registry-unavailable"].includes(base.reason) ? base.reason : null,
246
+ readOnly: true, provider: provider === external ? "external" : "bundled",
247
+ features: {
248
+ repository: true, branches: true, graph: true, commit: true, worktree: true, preview: true,
249
+ // Both flags are independent and additive: refsDetail can be
250
+ // synthesized from legacy strings; fileHistory is true only when the
251
+ // active provider implements the new methods (see fileHistoryFeature).
252
+ refsDetail: true, fileHistory: fileHistoryFeature(),
253
+ },
254
+ };
255
+ };
256
+ /**
257
+ * Normalize a client-supplied path to a repository-relative POSIX path.
258
+ * Returns null for paths that escape the repository (reported as
259
+ * `path-invalid`), and throws 400 for directories.
260
+ */
261
+ const normalizeHistoryPath = async (root, input) => {
262
+ if (typeof input !== "string" || input === "" || input.includes("\0")) return null;
263
+ // Windows-style absolute paths cannot be mapped on a POSIX host; report
264
+ // them as invalid instead of treating them as a relative filename.
265
+ if (/^(?:[A-Za-z]:[\\/]|\\\\)/.test(input) && !isAbsolute(input)) return null;
266
+ const candidate = isAbsolute(input) ? resolve(input) : resolve(root, input);
267
+ const rel = relative(root, candidate);
268
+ if (rel === "") throw failure("path-invalid", 400);
269
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
270
+ const normalized = sep === "/" ? rel : rel.split(sep).join("/");
271
+ try {
272
+ const info = await lstat(candidate);
273
+ if (info.isDirectory()) throw failure("path-invalid", 400);
274
+ } catch (error) {
275
+ if (error?.code === "path-invalid") throw error;
276
+ }
277
+ return normalized;
278
+ };
279
+ const service = {
280
+ capabilities,
281
+ async capabilitiesForSession(sessionId) {
282
+ const current = session(sessionId);
283
+ const base = capabilities();
284
+ if (!base.available) return base;
285
+ try {
286
+ await authorizedRoot(current?.header?.cwd);
287
+ return base;
288
+ } catch (error) {
289
+ const reason = ["not-git-repository", "git-provider-unavailable"].includes(error?.code)
290
+ ? error.code : "repository-not-authorized";
291
+ return { ...base, available: false, reason };
292
+ }
293
+ },
294
+ async repositoryForSession(sessionId) {
295
+ const current = session(sessionId);
296
+ const root = await authorizedRoot(current?.header?.cwd);
297
+ const repositoryId = repositoryIdFor(sessionId, root);
298
+ bindings.set(repositoryId, { sessionId, root });
299
+ keepBounded(bindings, 256);
300
+ const rawInfo = typeof provider.repositoryInfo === "function" ? await provider.repositoryInfo(root) : {};
301
+ if (typeof provider.refSignature === "function") refSignatures.set(repositoryId, await provider.refSignature(root));
302
+ return {
303
+ repositoryId, name: basename(root) || "repository",
304
+ ...(validOid(rawInfo.headOid) ? { headOid: rawInfo.headOid } : {}),
305
+ ...(typeof rawInfo.currentBranch === "string" && rawInfo.currentBranch.startsWith("refs/heads/") && validRef(rawInfo.currentBranch) ? { currentBranch: rawInfo.currentBranch } : {}),
306
+ detached: rawInfo.detached === true, empty: rawInfo.empty === true,
307
+ };
308
+ },
309
+ async worktree(sessionId, repositoryId) {
310
+ const authorized = await binding(sessionId, repositoryId);
311
+ let reader = typeof provider.worktree === "function" ? provider : bundled;
312
+ let raw = await reader.worktree(authorized.root);
313
+ if (typeof raw?.signature !== "string" || raw.signature === "" || !Array.isArray(raw.entries)) {
314
+ reader = bundled;
315
+ raw = await reader.worktree(authorized.root);
316
+ }
317
+ const entries = (raw.entries ?? []).filter((entry) => safeFilePath(entry.path)).slice(0, 2000);
318
+ const snapshotId = createHash("sha256").update(`${repositoryId}\0${raw.signature}\0${JSON.stringify(entries)}`).digest("base64url");
319
+ worktreeSnapshots.set(snapshotId, { sessionId, repositoryId, root: authorized.root, signature: raw.signature, entries, reader, expiresAt: now() + snapshotTtlMs });
320
+ keepBounded(worktreeSnapshots, 128);
321
+ return { repositoryId, snapshotId, staged: entries.filter((e) => e.index).map(({ path, oldPath, status, conflicted }) => ({ path, ...(oldPath ? { oldPath } : {}), status, conflicted })), unstaged: entries.filter((e) => e.worktree).map(({ path, oldPath, status, conflicted }) => ({ path, ...(oldPath ? { oldPath } : {}), status, conflicted })), untracked: entries.filter((e) => e.status === "untracked").map(({ path, status, conflicted }) => ({ path, status, conflicted })), truncated: (raw.entries ?? []).length > 2000 };
322
+ },
323
+ async preview(sessionId, repositoryId, { kind, snapshotId, path, oid } = {}) {
324
+ const authorized = await binding(sessionId, repositoryId);
325
+ if (!safeFilePath(path) || !["staged", "unstaged", "untracked", "commit"].includes(kind)) throw failure("preview-invalid", 400);
326
+ const reader = bundled;
327
+ let raw;
328
+ if (kind === "commit") {
329
+ if (!validOid(oid)) throw failure("invalid-oid", 400);
330
+ const commit = typeof provider.commit === "function"
331
+ ? await provider.commit(authorized.root, oid)
332
+ : await reader.commit(authorized.root, oid);
333
+ const commitFile = (Array.isArray(commit?.files) ? commit.files : []).find((file) => file.path === path || file.oldPath === path);
334
+ if (!commitFile) throw failure("preview-file-not-found", 404);
335
+ raw = { ...await reader.commitPreview(authorized.root, oid, path), ...(commitFile.oldPath ? { oldPath: commitFile.oldPath } : {}) };
336
+ } else {
337
+ const snap = worktreeSnapshots.get(snapshotId);
338
+ if (!snap || snap.sessionId !== sessionId || snap.repositoryId !== repositoryId || snap.root !== authorized.root || snap.expiresAt <= now()) throw stale();
339
+ const current = await snap.reader.worktree(authorized.root);
340
+ if (current.signature !== snap.signature) throw stale();
341
+ const member = snap.entries.find((entry) => entry.path === path && (kind === "untracked" ? entry.status === "untracked" : kind === "staged" ? entry.index : entry.worktree));
342
+ if (!member) throw failure("preview-file-not-found", 404);
343
+ if (member.conflicted) raw = { diff: "", binary: false, notice: "merge conflict preview omitted" };
344
+ else {
345
+ const previewReader = typeof snap.reader.worktreePreview === "function" ? snap.reader : reader;
346
+ const preview = await previewReader.worktreePreview(authorized.root, kind, path);
347
+ const afterPreview = await snap.reader.worktree(authorized.root);
348
+ if (afterPreview.signature !== snap.signature) throw stale();
349
+ raw = { ...preview, ...(member.oldPath ? { oldPath: member.oldPath } : {}) };
350
+ }
351
+ }
352
+ let diff = String(raw.diff ?? ""); const max = 512 * 1024; const truncated = raw.truncated === true || Buffer.byteLength(diff) > max;
353
+ if (Buffer.byteLength(diff) > max) diff = Buffer.from(diff).subarray(0, max).toString("utf8");
354
+ const binary = raw.binary === true || /Binary files? .* differ/.test(diff) || diff.includes("GIT binary patch");
355
+ let notice = raw.notice;
356
+ if (!notice && binary) { notice = "binary preview omitted"; diff = ""; }
357
+ else if (!notice && /(?:new file mode|old mode|new mode) 120000/.test(diff)) { notice = "symlink text preview omitted"; diff = ""; }
358
+ else if (!notice && /^\+(?:Subproject commit )|^-(?:Subproject commit )/m.test(diff)) { notice = "submodule preview omitted"; diff = ""; }
359
+ else if (!notice && !/^@@/m.test(diff) && /^(?:old mode|new mode) /m.test(diff)) { notice = "file mode changed; no text changes"; diff = ""; }
360
+ return { repositoryId, kind, path, ...(raw.oldPath ? { oldPath: raw.oldPath } : {}), diff, truncated, binary, additions: raw.additions ?? null, deletions: raw.deletions ?? null, ...(notice ? { notice } : {}) };
361
+ },
362
+ async branches(sessionId, repositoryId) {
363
+ const authorized = await binding(sessionId, repositoryId);
364
+ if (typeof provider.branches !== "function") throw failure("provider-incompatible", 503);
365
+ const rows = await provider.branches(authorized.root);
366
+ const branches = (Array.isArray(rows) ? rows : []).filter((row) => row && row.name !== "HEAD" && validRef(row.name) && validOid(row.oid)).map((row) => ({
367
+ name: row.name, displayName: row.name.replace(/^refs\/(?:heads|remotes)\//, ""), oid: row.oid,
368
+ kind: row.name.startsWith("refs/remotes/") ? "remote" : "local", current: row.current === true,
369
+ tracking: typeof row.tracking === "string" && row.tracking.startsWith("refs/remotes/") && validRef(row.tracking) ? row.tracking : null,
370
+ ahead: Math.max(0, Number(row.ahead) || 0), behind: Math.max(0, Number(row.behind) || 0),
371
+ }));
372
+ return { repositoryId, branches };
373
+ },
374
+ async commit(sessionId, repositoryId, oid, { filesCursor, filesLimit = 100 } = {}) {
375
+ const authorized = await binding(sessionId, repositoryId);
376
+ if (!validOid(oid)) throw failure("invalid-oid", 400);
377
+ if (typeof provider.commit !== "function") throw failure("provider-incompatible", 503);
378
+ const limit = Math.max(1, Math.min(200, Number(filesLimit) || 100));
379
+ prune();
380
+ let offset = 0;
381
+ if (filesCursor) {
382
+ const saved = fileCursors.get(filesCursor);
383
+ if (!saved || saved.expiresAt <= now() || saved.sessionId !== sessionId || saved.repositoryId !== repositoryId || saved.oid !== String(oid).toLowerCase()) throw failure("invalid-files-cursor", 400);
384
+ offset = saved.offset;
385
+ }
386
+ const value = await provider.commit(authorized.root, oid);
387
+ const allFiles = (Array.isArray(value.files) ? value.files : []).filter((file) => safeFilePath(file?.path) && (file.oldPath == null || safeFilePath(file.oldPath))).map((file) => ({
388
+ path: String(file.path ?? ""), ...(typeof file.oldPath === "string" ? { oldPath: file.oldPath } : {}),
389
+ status: String(file.status ?? "modified"), binary: file.binary === true,
390
+ additions: file.additions == null ? null : Math.max(0, Number(file.additions) || 0),
391
+ deletions: file.deletions == null ? null : Math.max(0, Number(file.deletions) || 0),
392
+ }));
393
+ const items = allFiles.slice(offset, offset + limit);
394
+ let nextCursor = null;
395
+ if (offset + items.length < allFiles.length) {
396
+ nextCursor = randomUUID();
397
+ fileCursors.set(nextCursor, { sessionId, repositoryId, oid: value.oid.toLowerCase(), offset: offset + items.length, expiresAt: now() + snapshotTtlMs });
398
+ keepBounded(fileCursors, 1024);
399
+ }
400
+ const stats = allFiles.reduce((sum, file) => ({ additions: sum.additions + (file.additions ?? 0), deletions: sum.deletions + (file.deletions ?? 0), files: sum.files + 1 }), { additions: 0, deletions: 0, files: 0 });
401
+ const commitOid = String(value.oid ?? oid);
402
+ const legacyRefs = Array.isArray(value.refs) ? value.refs.map(String) : [];
403
+ const legacyTags = Array.isArray(value.tags) ? value.tags.map(String) : [];
404
+ const records = await readRefRecords(authorized.root);
405
+ const refDetails = mergeRefDetails(
406
+ records ? refDetailsFromRecords(records, commitOid, { fullTags: true }) : emptyRefDetails(),
407
+ refDetailsFromLegacyRefs(legacyRefs, commitOid, { tags: legacyTags }),
408
+ );
409
+ return {
410
+ repositoryId, oid: commitOid, parents: Array.isArray(value.parents) ? value.parents.map(String) : [],
411
+ author: String(value.author ?? ""), timestamp: Number(value.timestamp) || 0, message: String(value.message ?? ""),
412
+ refs: legacyRefs, tags: legacyTags, refsDetail: refDetails.refsDetail, tagsDetail: refDetails.tagsDetail,
413
+ stats, files: { items, total: allFiles.length, nextCursor },
414
+ };
415
+ },
416
+ async graph(sessionId, repositoryId, { tips, snapshotId, cursor, limit = 100 } = {}) {
417
+ const authorized = await binding(sessionId, repositoryId);
418
+ prune();
419
+ const pageSize = Math.max(1, Math.min(200, Number(limit) || 100));
420
+ let snapshot, offset = 0;
421
+ if (snapshotId !== undefined || cursor !== undefined) {
422
+ if (!snapshotId || !cursor) throw stale();
423
+ snapshot = snapshots.get(snapshotId);
424
+ if (!snapshot || snapshot.sessionId !== sessionId || snapshot.repositoryId !== repositoryId || snapshot.root !== authorized.root || snapshot.expiresAt <= now()) throw stale();
425
+ offset = snapshot.cursors.get(cursor);
426
+ if (!Number.isInteger(offset)) throw stale();
427
+ } else {
428
+ const selected = tips === undefined ? [{ name: "HEAD", tipOid: null }] : tips;
429
+ if (!Array.isArray(selected) || selected.length > MAX_GRAPH_TIPS) throw failure("graph-too-many-tips", 400);
430
+ if (selected.length === 0) throw failure("graph-tip-invalid", 400);
431
+ const names = selected.map((tip) => tip?.name);
432
+ if (new Set(names).size !== names.length || names.some((name) => !validRef(name)) || selected.some((tip) => tip?.tipOid != null && !validOid(tip.tipOid))) throw failure("graph-tip-invalid", 400);
433
+ const resolved = [];
434
+ for (const tip of selected) {
435
+ let oid;
436
+ try { oid = await provider.resolveTip(authorized.root, tip.name); }
437
+ catch (error) {
438
+ if (tip.name === "HEAD" && error?.code === "git-command-failed") continue;
439
+ throw failure("graph-tip-invalid", 409);
440
+ }
441
+ if (!validOid(oid)) throw failure("graph-tip-invalid", 409);
442
+ if (tip.tipOid != null && tip.tipOid.toLowerCase() !== oid.toLowerCase()) throw failure("graph-tip-invalid", 409);
443
+ resolved.push({ name: tip.name, tipOid: oid });
444
+ }
445
+ snapshot = { id: randomUUID(), sessionId, repositoryId, root: authorized.root, tips: resolved, expiresAt: now() + snapshotTtlMs, cursors: new Map() };
446
+ snapshots.set(snapshot.id, snapshot);
447
+ keepBounded(snapshots, 128);
448
+ }
449
+ for (const tip of snapshot.tips) {
450
+ let current;
451
+ try { current = await provider.resolveTip(authorized.root, tip.name); } catch { throw stale(); }
452
+ if (current.toLowerCase() !== tip.tipOid.toLowerCase()) throw stale();
453
+ }
454
+ let rawCommits;
455
+ try { rawCommits = await provider.graph(authorized.root, { refs: snapshot.tips.map((tip) => tip.tipOid), offset, limit: pageSize }); }
456
+ catch (error) {
457
+ for (const tip of snapshot.tips) {
458
+ let current;
459
+ try { current = await provider.resolveTip(authorized.root, tip.name); } catch { snapshots.delete(snapshot.id); throw stale(); }
460
+ if (current.toLowerCase() !== tip.tipOid.toLowerCase()) { snapshots.delete(snapshot.id); throw stale(); }
461
+ }
462
+ throw error;
463
+ }
464
+ for (const tip of snapshot.tips) {
465
+ let current;
466
+ try { current = await provider.resolveTip(authorized.root, tip.name); } catch { snapshots.delete(snapshot.id); throw stale(); }
467
+ if (current.toLowerCase() !== tip.tipOid.toLowerCase()) { snapshots.delete(snapshot.id); throw stale(); }
468
+ }
469
+ const records = await readRefRecords(authorized.root);
470
+ const commits = (Array.isArray(rawCommits) ? rawCommits : []).map((row) => {
471
+ const oid = String(row.oid ?? "");
472
+ const legacy = Array.isArray(row.refs) ? row.refs.map(String) : [];
473
+ // Graph rows only guarantee name-level refs plus `annotated`; full
474
+ // tag metadata is reserved for commit details.
475
+ const refDetails = mergeRefDetails(
476
+ records ? refDetailsFromRecords(records, oid) : emptyRefDetails(),
477
+ refDetailsFromLegacyRefs(legacy, oid),
478
+ );
479
+ return {
480
+ oid, parents: Array.isArray(row.parents) ? row.parents.map(String) : [],
481
+ author: String(row.author ?? ""), timestamp: Number(row.timestamp) || 0, subject: String(row.subject ?? ""),
482
+ refs: legacy, refsDetail: refDetails.refsDetail, tagsDetail: refDetails.tagsDetail,
483
+ };
484
+ });
485
+ let nextCursor = null;
486
+ if (commits.length === pageSize) {
487
+ nextCursor = randomUUID();
488
+ snapshot.cursors.set(nextCursor, offset + commits.length);
489
+ keepBounded(snapshot.cursors, 128);
490
+ }
491
+ return { repositoryId, snapshotId: snapshot.id, tips: snapshot.tips, commits, nextCursor };
492
+ },
493
+ async files(sessionId, repositoryId, { q, limit } = {}) {
494
+ const authorized = await binding(sessionId, repositoryId);
495
+ const pageSize = Math.max(1, Math.min(MAX_TRACKED_FILES, Number(limit) || MAX_TRACKED_FILES));
496
+ const query = typeof q === "string" ? q : "";
497
+ const raw = await fileReader().files(authorized.root, { q: query, limit: pageSize });
498
+ const files = [...new Set((Array.isArray(raw?.files) ? raw.files : [])
499
+ .map((path) => String(path ?? ""))
500
+ .filter((path) => safeFilePath(path)))].sort(compareText).slice(0, pageSize);
501
+ return { repositoryId, files, truncated: raw?.truncated === true };
502
+ },
503
+ async fileHistory(sessionId, repositoryId, { path, cursor, limit } = {}) {
504
+ const authorized = await binding(sessionId, repositoryId);
505
+ const pageSize = Math.max(1, Math.min(FILE_HISTORY_MAX_LIMIT, Number(limit) || FILE_HISTORY_DEFAULT_LIMIT));
506
+ prune();
507
+ const normalized = await normalizeHistoryPath(authorized.root, path);
508
+ if (normalized === null) {
509
+ return { repositoryId, path: String(path ?? ""), entries: [], nextCursor: null, truncated: false, reason: "path-invalid" };
510
+ }
511
+ let snapshot = null, offset = 0;
512
+ if (cursor !== undefined && cursor !== null && cursor !== "") {
513
+ const saved = historyCursors.get(String(cursor));
514
+ if (!saved || saved.expiresAt <= now() || saved.sessionId !== sessionId || saved.repositoryId !== repositoryId || saved.path !== normalized) throw failure("invalid-history-cursor", 400);
515
+ snapshot = historySnapshots.get(saved.snapshotId);
516
+ if (!snapshot || snapshot.expiresAt <= now() || snapshot.root !== authorized.root) throw failure("invalid-history-cursor", 400);
517
+ offset = saved.offset;
518
+ }
519
+ if (!snapshot) {
520
+ const raw = await fileReader().fileHistory(authorized.root, normalized, {
521
+ maxCommits: FILE_HISTORY_MAX_COMMITS,
522
+ timeoutMs: FILE_HISTORY_TIMEOUT_MS,
523
+ });
524
+ const reason = FILE_HISTORY_REASONS.has(raw?.reason) ? raw.reason : null;
525
+ const entries = (Array.isArray(raw?.entries) ? raw.entries : []).map((entry) => ({
526
+ oid: String(entry?.oid ?? ""),
527
+ shortOid: String(entry?.shortOid ?? String(entry?.oid ?? "").slice(0, 7)),
528
+ author: String(entry?.author ?? ""),
529
+ timestamp: Number(entry?.timestamp) || 0,
530
+ subject: String(entry?.subject ?? ""),
531
+ status: historyStatus(entry?.status),
532
+ })).filter((entry) => validOid(entry.oid));
533
+ if (reason && entries.length === 0) {
534
+ return { repositoryId, path: normalized, entries: [], nextCursor: null, truncated: false, reason };
535
+ }
536
+ snapshot = {
537
+ id: randomUUID(), sessionId, repositoryId, root: authorized.root, path: normalized,
538
+ entries, truncated: raw?.truncated === true, expiresAt: now() + snapshotTtlMs,
539
+ };
540
+ historySnapshots.set(snapshot.id, snapshot);
541
+ keepBounded(historySnapshots, MAX_FILE_HISTORY_SNAPSHOTS);
542
+ }
543
+ const items = snapshot.entries.slice(offset, offset + pageSize);
544
+ let nextCursor = null;
545
+ if (offset + items.length < snapshot.entries.length) {
546
+ nextCursor = randomUUID();
547
+ historyCursors.set(nextCursor, {
548
+ sessionId, repositoryId, path: normalized, snapshotId: snapshot.id,
549
+ offset: offset + items.length, expiresAt: now() + snapshotTtlMs,
550
+ });
551
+ keepBounded(historyCursors, 1024);
552
+ }
553
+ return { repositoryId, path: normalized, entries: items, nextCursor, truncated: snapshot.truncated };
554
+ },
555
+ async _pollOnce() {
556
+ for (const [repositoryId, authorized] of bindings) {
557
+ try {
558
+ await binding(authorized.sessionId, repositoryId);
559
+ if (typeof provider.refSignature === "function") {
560
+ const signature = await provider.refSignature(authorized.root);
561
+ const previous = refSignatures.get(repositoryId); refSignatures.set(repositoryId, signature);
562
+ if (previous !== undefined && previous !== signature) onChanged?.(repositoryId, "refs");
563
+ }
564
+ } catch { bindings.delete(repositoryId); refSignatures.delete(repositoryId); }
565
+ }
566
+ },
567
+ start() {
568
+ if (unsubscribeProvider || pollTimer) return;
569
+ if (typeof provider.subscribeChanges === "function") {
570
+ try { unsubscribeProvider = provider.subscribeChanges((change) => {
571
+ void (async () => {
572
+ for (const [repositoryId, authorized] of bindings) {
573
+ if (change?.root !== authorized.root) continue;
574
+ try {
575
+ await binding(authorized.sessionId, repositoryId);
576
+ if (typeof provider.refSignature === "function") {
577
+ const signature = await provider.refSignature(authorized.root);
578
+ const previous = refSignatures.get(repositoryId);
579
+ refSignatures.set(repositoryId, signature);
580
+ if (previous === signature) continue;
581
+ }
582
+ onChanged?.(repositoryId);
583
+ } catch { bindings.delete(repositoryId); refSignatures.delete(repositoryId); }
584
+ }
585
+ })();
586
+ }); } catch { unsubscribeProvider = null; }
587
+ if (typeof unsubscribeProvider !== "function") unsubscribeProvider = null;
588
+ }
589
+ // Ref polling is server-side; worktree polling stays scoped to an open mobile page.
590
+ pollTimer = setInterval(() => { void service._pollOnce(); }, pollIntervalMs);
591
+ pollTimer.unref?.();
592
+ },
593
+ stop() {
594
+ if (pollTimer) clearInterval(pollTimer);
595
+ pollTimer = null;
596
+ try { unsubscribeProvider?.(); } catch { /* optional provider teardown must not break host shutdown */ }
597
+ unsubscribeProvider = null;
598
+ },
599
+ };
600
+ return service;
601
+ }