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

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/DSH-Remote-v3.2.4.apk +4 -0
  2. package/README.md +6 -254
  3. package/manifest.json +21 -0
  4. package/package.json +12 -65
  5. package/CHANGELOG.md +0 -1410
  6. package/CONTRIBUTING.md +0 -59
  7. package/FAQ.md +0 -111
  8. package/cordis.patch.yml +0 -14
  9. package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +0 -125
  10. package/docs/01-PRD.md +0 -148
  11. package/docs/02-architecture.md +0 -249
  12. package/docs/03-api.md +0 -1154
  13. package/docs/04-security.md +0 -100
  14. package/docs/05-test-cases.md +0 -405
  15. package/docs/06-install-run.md +0 -372
  16. package/docs/07-user-manual.md +0 -70
  17. package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +0 -86
  18. package/docs/09-compatibility.md +0 -227
  19. package/docs/10-release.md +0 -1067
  20. package/docs/screenshots/01-home.png +0 -0
  21. package/docs/screenshots/02-notifications.png +0 -0
  22. package/docs/screenshots/03-sessions.png +0 -0
  23. package/docs/screenshots/04-chat.png +0 -0
  24. package/docs/screenshots/05-settings.png +0 -0
  25. package/docs/screenshots/06-new-session.png +0 -0
  26. package/docs/screenshots/pgy-appstore.jpg +0 -0
  27. package/docs/screenshots/pgy-mobile-app.jpg +0 -0
  28. package/docs/screenshots/pgy-pc-client.jpg +0 -0
  29. package/lib/account-usage.js +0 -145
  30. package/lib/app-alignment.js +0 -112
  31. package/lib/app-carrier-store.js +0 -1972
  32. package/lib/app-carrier.js +0 -609
  33. package/lib/client.js +0 -1174
  34. package/lib/desktop-form.js +0 -738
  35. package/lib/endpoint-registry.js +0 -167
  36. package/lib/git-read-bundled-provider.js +0 -450
  37. package/lib/git-read-service.js +0 -601
  38. package/lib/index.js +0 -9622
  39. package/lib/npm-update-source.js +0 -226
  40. package/lib/plugin-content-digest.js +0 -80
  41. package/lib/plugin-self-update.js +0 -997
  42. package/lib/plugin-update-controller.js +0 -274
  43. package/lib/plugin-update-log.js +0 -164
  44. package/lib/plugin-update-transaction.js +0 -322
  45. package/lib/plugin-update.js +0 -309
  46. package/lib/runtime-dependency-snapshot.js +0 -345
  47. package/lib/session-title-cache.js +0 -123
  48. package/lib/session-title-refresh.js +0 -261
  49. package/lib/tool-process.js +0 -20
  50. package/lib/update-fetch.js +0 -24
  51. package/lib/update-lock-arbiter.js +0 -170
  52. package/lib/update-supervisor.js +0 -346
  53. package/node_modules/ansi-regex/index.d.ts +0 -37
  54. package/node_modules/ansi-regex/index.js +0 -10
  55. package/node_modules/ansi-regex/license +0 -9
  56. package/node_modules/ansi-regex/package.json +0 -55
  57. package/node_modules/ansi-regex/readme.md +0 -78
  58. package/node_modules/ansi-styles/index.d.ts +0 -345
  59. package/node_modules/ansi-styles/index.js +0 -163
  60. package/node_modules/ansi-styles/license +0 -9
  61. package/node_modules/ansi-styles/package.json +0 -56
  62. package/node_modules/ansi-styles/readme.md +0 -152
  63. package/node_modules/camelcase/index.d.ts +0 -63
  64. package/node_modules/camelcase/index.js +0 -76
  65. package/node_modules/camelcase/license +0 -9
  66. package/node_modules/camelcase/package.json +0 -43
  67. package/node_modules/camelcase/readme.md +0 -99
  68. package/node_modules/cliui/CHANGELOG.md +0 -76
  69. package/node_modules/cliui/LICENSE.txt +0 -14
  70. package/node_modules/cliui/README.md +0 -115
  71. package/node_modules/cliui/index.js +0 -354
  72. package/node_modules/cliui/package.json +0 -65
  73. package/node_modules/color-convert/CHANGELOG.md +0 -54
  74. package/node_modules/color-convert/LICENSE +0 -21
  75. package/node_modules/color-convert/README.md +0 -68
  76. package/node_modules/color-convert/conversions.js +0 -839
  77. package/node_modules/color-convert/index.js +0 -81
  78. package/node_modules/color-convert/package.json +0 -48
  79. package/node_modules/color-convert/route.js +0 -97
  80. package/node_modules/color-name/LICENSE +0 -8
  81. package/node_modules/color-name/README.md +0 -11
  82. package/node_modules/color-name/index.js +0 -152
  83. package/node_modules/color-name/package.json +0 -28
  84. package/node_modules/decamelize/index.js +0 -13
  85. package/node_modules/decamelize/license +0 -21
  86. package/node_modules/decamelize/package.json +0 -38
  87. package/node_modules/decamelize/readme.md +0 -48
  88. package/node_modules/dijkstrajs/.travis.yml +0 -4
  89. package/node_modules/dijkstrajs/CONTRIBUTING.md +0 -8
  90. package/node_modules/dijkstrajs/LICENSE.md +0 -19
  91. package/node_modules/dijkstrajs/README.md +0 -18
  92. package/node_modules/dijkstrajs/dijkstra.js +0 -165
  93. package/node_modules/dijkstrajs/package.json +0 -60
  94. package/node_modules/dijkstrajs/test/dijkstra.test.js +0 -96
  95. package/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  96. package/node_modules/emoji-regex/README.md +0 -73
  97. package/node_modules/emoji-regex/es2015/index.js +0 -6
  98. package/node_modules/emoji-regex/es2015/text.js +0 -6
  99. package/node_modules/emoji-regex/index.d.ts +0 -23
  100. package/node_modules/emoji-regex/index.js +0 -6
  101. package/node_modules/emoji-regex/package.json +0 -50
  102. package/node_modules/emoji-regex/text.js +0 -6
  103. package/node_modules/find-up/index.d.ts +0 -137
  104. package/node_modules/find-up/index.js +0 -89
  105. package/node_modules/find-up/license +0 -9
  106. package/node_modules/find-up/package.json +0 -53
  107. package/node_modules/find-up/readme.md +0 -156
  108. package/node_modules/get-caller-file/LICENSE.md +0 -6
  109. package/node_modules/get-caller-file/README.md +0 -41
  110. package/node_modules/get-caller-file/index.d.ts +0 -2
  111. package/node_modules/get-caller-file/index.js +0 -22
  112. package/node_modules/get-caller-file/index.js.map +0 -1
  113. package/node_modules/get-caller-file/package.json +0 -42
  114. package/node_modules/is-fullwidth-code-point/index.d.ts +0 -17
  115. package/node_modules/is-fullwidth-code-point/index.js +0 -50
  116. package/node_modules/is-fullwidth-code-point/license +0 -9
  117. package/node_modules/is-fullwidth-code-point/package.json +0 -42
  118. package/node_modules/is-fullwidth-code-point/readme.md +0 -39
  119. package/node_modules/locate-path/index.d.ts +0 -83
  120. package/node_modules/locate-path/index.js +0 -65
  121. package/node_modules/locate-path/license +0 -9
  122. package/node_modules/locate-path/package.json +0 -45
  123. package/node_modules/locate-path/readme.md +0 -122
  124. package/node_modules/p-limit/index.d.ts +0 -38
  125. package/node_modules/p-limit/index.js +0 -57
  126. package/node_modules/p-limit/license +0 -9
  127. package/node_modules/p-limit/package.json +0 -52
  128. package/node_modules/p-limit/readme.md +0 -101
  129. package/node_modules/p-locate/index.d.ts +0 -64
  130. package/node_modules/p-locate/index.js +0 -52
  131. package/node_modules/p-locate/license +0 -9
  132. package/node_modules/p-locate/package.json +0 -53
  133. package/node_modules/p-locate/readme.md +0 -90
  134. package/node_modules/p-try/index.d.ts +0 -39
  135. package/node_modules/p-try/index.js +0 -9
  136. package/node_modules/p-try/license +0 -9
  137. package/node_modules/p-try/package.json +0 -42
  138. package/node_modules/p-try/readme.md +0 -58
  139. package/node_modules/path-exists/index.d.ts +0 -28
  140. package/node_modules/path-exists/index.js +0 -23
  141. package/node_modules/path-exists/license +0 -9
  142. package/node_modules/path-exists/package.json +0 -39
  143. package/node_modules/path-exists/readme.md +0 -52
  144. package/node_modules/pngjs/.eslintignore +0 -2
  145. package/node_modules/pngjs/.eslintrc.json +0 -19
  146. package/node_modules/pngjs/.prettierignore +0 -3
  147. package/node_modules/pngjs/LICENSE +0 -20
  148. package/node_modules/pngjs/README.md +0 -429
  149. package/node_modules/pngjs/browser.js +0 -16096
  150. package/node_modules/pngjs/coverage/lcov-report/base.css +0 -354
  151. package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +0 -881
  152. package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +0 -554
  153. package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +0 -77
  154. package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +0 -647
  155. package/node_modules/pngjs/coverage/lcov-report/constants.js.html +0 -176
  156. package/node_modules/pngjs/coverage/lcov-report/crc.js.html +0 -200
  157. package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
  158. package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +0 -593
  159. package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +0 -152
  160. package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +0 -143
  161. package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +0 -611
  162. package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +0 -359
  163. package/node_modules/pngjs/coverage/lcov-report/index.html +0 -426
  164. package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +0 -365
  165. package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +0 -230
  166. package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +0 -248
  167. package/node_modules/pngjs/coverage/lcov-report/packer.js.html +0 -467
  168. package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +0 -128
  169. package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +0 -575
  170. package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +0 -404
  171. package/node_modules/pngjs/coverage/lcov-report/parser.js.html +0 -950
  172. package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +0 -116
  173. package/node_modules/pngjs/coverage/lcov-report/png.js.html +0 -662
  174. package/node_modules/pngjs/coverage/lcov-report/prettify.css +0 -101
  175. package/node_modules/pngjs/coverage/lcov-report/prettify.js +0 -1007
  176. package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  177. package/node_modules/pngjs/coverage/lcov-report/sorter.js +0 -169
  178. package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +0 -584
  179. package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +0 -215
  180. package/node_modules/pngjs/coverage/lcov.info +0 -2177
  181. package/node_modules/pngjs/lib/bitmapper.js +0 -267
  182. package/node_modules/pngjs/lib/bitpacker.js +0 -158
  183. package/node_modules/pngjs/lib/chunkstream.js +0 -189
  184. package/node_modules/pngjs/lib/constants.js +0 -32
  185. package/node_modules/pngjs/lib/crc.js +0 -40
  186. package/node_modules/pngjs/lib/filter-pack.js +0 -171
  187. package/node_modules/pngjs/lib/filter-parse-async.js +0 -24
  188. package/node_modules/pngjs/lib/filter-parse-sync.js +0 -21
  189. package/node_modules/pngjs/lib/filter-parse.js +0 -177
  190. package/node_modules/pngjs/lib/format-normaliser.js +0 -93
  191. package/node_modules/pngjs/lib/interlace.js +0 -95
  192. package/node_modules/pngjs/lib/packer-async.js +0 -50
  193. package/node_modules/pngjs/lib/packer-sync.js +0 -56
  194. package/node_modules/pngjs/lib/packer.js +0 -129
  195. package/node_modules/pngjs/lib/paeth-predictor.js +0 -16
  196. package/node_modules/pngjs/lib/parser-async.js +0 -165
  197. package/node_modules/pngjs/lib/parser-sync.js +0 -108
  198. package/node_modules/pngjs/lib/parser.js +0 -290
  199. package/node_modules/pngjs/lib/png-sync.js +0 -12
  200. package/node_modules/pngjs/lib/png.js +0 -194
  201. package/node_modules/pngjs/lib/sync-inflate.js +0 -168
  202. package/node_modules/pngjs/lib/sync-reader.js +0 -45
  203. package/node_modules/pngjs/package.json +0 -73
  204. package/node_modules/qrcode/README.md +0 -770
  205. package/node_modules/qrcode/bin/qrcode +0 -159
  206. package/node_modules/qrcode/helper/to-sjis-browser.js +0 -2
  207. package/node_modules/qrcode/helper/to-sjis.js +0 -105
  208. package/node_modules/qrcode/lib/browser.js +0 -76
  209. package/node_modules/qrcode/lib/can-promise.js +0 -7
  210. package/node_modules/qrcode/lib/core/alignment-pattern.js +0 -83
  211. package/node_modules/qrcode/lib/core/alphanumeric-data.js +0 -59
  212. package/node_modules/qrcode/lib/core/bit-buffer.js +0 -37
  213. package/node_modules/qrcode/lib/core/bit-matrix.js +0 -65
  214. package/node_modules/qrcode/lib/core/byte-data.js +0 -30
  215. package/node_modules/qrcode/lib/core/error-correction-code.js +0 -135
  216. package/node_modules/qrcode/lib/core/error-correction-level.js +0 -50
  217. package/node_modules/qrcode/lib/core/finder-pattern.js +0 -22
  218. package/node_modules/qrcode/lib/core/format-info.js +0 -29
  219. package/node_modules/qrcode/lib/core/galois-field.js +0 -69
  220. package/node_modules/qrcode/lib/core/kanji-data.js +0 -54
  221. package/node_modules/qrcode/lib/core/mask-pattern.js +0 -234
  222. package/node_modules/qrcode/lib/core/mode.js +0 -167
  223. package/node_modules/qrcode/lib/core/numeric-data.js +0 -43
  224. package/node_modules/qrcode/lib/core/polynomial.js +0 -62
  225. package/node_modules/qrcode/lib/core/qrcode.js +0 -495
  226. package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +0 -56
  227. package/node_modules/qrcode/lib/core/regex.js +0 -31
  228. package/node_modules/qrcode/lib/core/segments.js +0 -330
  229. package/node_modules/qrcode/lib/core/utils.js +0 -63
  230. package/node_modules/qrcode/lib/core/version-check.js +0 -9
  231. package/node_modules/qrcode/lib/core/version.js +0 -163
  232. package/node_modules/qrcode/lib/index.js +0 -12
  233. package/node_modules/qrcode/lib/renderer/canvas.js +0 -63
  234. package/node_modules/qrcode/lib/renderer/png.js +0 -78
  235. package/node_modules/qrcode/lib/renderer/svg-tag.js +0 -81
  236. package/node_modules/qrcode/lib/renderer/svg.js +0 -19
  237. package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +0 -85
  238. package/node_modules/qrcode/lib/renderer/terminal/terminal.js +0 -49
  239. package/node_modules/qrcode/lib/renderer/terminal.js +0 -9
  240. package/node_modules/qrcode/lib/renderer/utf8.js +0 -71
  241. package/node_modules/qrcode/lib/renderer/utils.js +0 -99
  242. package/node_modules/qrcode/lib/server.js +0 -138
  243. package/node_modules/qrcode/license +0 -10
  244. package/node_modules/qrcode/package.json +0 -77
  245. package/node_modules/require-directory/.jshintrc +0 -67
  246. package/node_modules/require-directory/.npmignore +0 -1
  247. package/node_modules/require-directory/.travis.yml +0 -3
  248. package/node_modules/require-directory/LICENSE +0 -22
  249. package/node_modules/require-directory/README.markdown +0 -184
  250. package/node_modules/require-directory/index.js +0 -86
  251. package/node_modules/require-directory/package.json +0 -40
  252. package/node_modules/require-main-filename/CHANGELOG.md +0 -26
  253. package/node_modules/require-main-filename/LICENSE.txt +0 -14
  254. package/node_modules/require-main-filename/README.md +0 -26
  255. package/node_modules/require-main-filename/index.js +0 -18
  256. package/node_modules/require-main-filename/package.json +0 -35
  257. package/node_modules/set-blocking/CHANGELOG.md +0 -26
  258. package/node_modules/set-blocking/LICENSE.txt +0 -14
  259. package/node_modules/set-blocking/README.md +0 -31
  260. package/node_modules/set-blocking/index.js +0 -7
  261. package/node_modules/set-blocking/package.json +0 -42
  262. package/node_modules/string-width/index.d.ts +0 -29
  263. package/node_modules/string-width/index.js +0 -47
  264. package/node_modules/string-width/license +0 -9
  265. package/node_modules/string-width/package.json +0 -56
  266. package/node_modules/string-width/readme.md +0 -50
  267. package/node_modules/strip-ansi/index.d.ts +0 -17
  268. package/node_modules/strip-ansi/index.js +0 -4
  269. package/node_modules/strip-ansi/license +0 -9
  270. package/node_modules/strip-ansi/package.json +0 -54
  271. package/node_modules/strip-ansi/readme.md +0 -46
  272. package/node_modules/which-module/LICENSE +0 -13
  273. package/node_modules/which-module/README.md +0 -58
  274. package/node_modules/which-module/index.js +0 -9
  275. package/node_modules/which-module/package.json +0 -41
  276. package/node_modules/wrap-ansi/index.js +0 -186
  277. package/node_modules/wrap-ansi/license +0 -9
  278. package/node_modules/wrap-ansi/package.json +0 -61
  279. package/node_modules/wrap-ansi/readme.md +0 -97
  280. package/node_modules/y18n/CHANGELOG.md +0 -35
  281. package/node_modules/y18n/LICENSE +0 -13
  282. package/node_modules/y18n/README.md +0 -109
  283. package/node_modules/y18n/index.js +0 -188
  284. package/node_modules/y18n/package.json +0 -39
  285. package/node_modules/yargs/CHANGELOG.md +0 -420
  286. package/node_modules/yargs/LICENSE +0 -21
  287. package/node_modules/yargs/README.md +0 -140
  288. package/node_modules/yargs/build/lib/apply-extends.d.ts +0 -2
  289. package/node_modules/yargs/build/lib/apply-extends.js +0 -65
  290. package/node_modules/yargs/build/lib/argsert.d.ts +0 -2
  291. package/node_modules/yargs/build/lib/argsert.js +0 -65
  292. package/node_modules/yargs/build/lib/command.d.ts +0 -64
  293. package/node_modules/yargs/build/lib/command.js +0 -416
  294. package/node_modules/yargs/build/lib/common-types.d.ts +0 -36
  295. package/node_modules/yargs/build/lib/common-types.js +0 -25
  296. package/node_modules/yargs/build/lib/completion-templates.d.ts +0 -2
  297. package/node_modules/yargs/build/lib/completion-templates.js +0 -50
  298. package/node_modules/yargs/build/lib/completion.d.ts +0 -21
  299. package/node_modules/yargs/build/lib/completion.js +0 -135
  300. package/node_modules/yargs/build/lib/is-promise.d.ts +0 -1
  301. package/node_modules/yargs/build/lib/is-promise.js +0 -9
  302. package/node_modules/yargs/build/lib/levenshtein.d.ts +0 -1
  303. package/node_modules/yargs/build/lib/levenshtein.js +0 -58
  304. package/node_modules/yargs/build/lib/middleware.d.ts +0 -10
  305. package/node_modules/yargs/build/lib/middleware.js +0 -57
  306. package/node_modules/yargs/build/lib/obj-filter.d.ts +0 -1
  307. package/node_modules/yargs/build/lib/obj-filter.js +0 -14
  308. package/node_modules/yargs/build/lib/parse-command.d.ts +0 -11
  309. package/node_modules/yargs/build/lib/parse-command.js +0 -36
  310. package/node_modules/yargs/build/lib/process-argv.d.ts +0 -2
  311. package/node_modules/yargs/build/lib/process-argv.js +0 -31
  312. package/node_modules/yargs/build/lib/usage.d.ts +0 -49
  313. package/node_modules/yargs/build/lib/usage.js +0 -540
  314. package/node_modules/yargs/build/lib/validation.d.ts +0 -34
  315. package/node_modules/yargs/build/lib/validation.js +0 -330
  316. package/node_modules/yargs/build/lib/yargs.d.ts +0 -274
  317. package/node_modules/yargs/build/lib/yargs.js +0 -1190
  318. package/node_modules/yargs/build/lib/yerror.d.ts +0 -4
  319. package/node_modules/yargs/build/lib/yerror.js +0 -11
  320. package/node_modules/yargs/index.js +0 -40
  321. package/node_modules/yargs/locales/be.json +0 -46
  322. package/node_modules/yargs/locales/de.json +0 -46
  323. package/node_modules/yargs/locales/en.json +0 -51
  324. package/node_modules/yargs/locales/es.json +0 -46
  325. package/node_modules/yargs/locales/fi.json +0 -49
  326. package/node_modules/yargs/locales/fr.json +0 -53
  327. package/node_modules/yargs/locales/hi.json +0 -49
  328. package/node_modules/yargs/locales/hu.json +0 -46
  329. package/node_modules/yargs/locales/id.json +0 -50
  330. package/node_modules/yargs/locales/it.json +0 -46
  331. package/node_modules/yargs/locales/ja.json +0 -51
  332. package/node_modules/yargs/locales/ko.json +0 -49
  333. package/node_modules/yargs/locales/nb.json +0 -44
  334. package/node_modules/yargs/locales/nl.json +0 -49
  335. package/node_modules/yargs/locales/nn.json +0 -44
  336. package/node_modules/yargs/locales/pirate.json +0 -13
  337. package/node_modules/yargs/locales/pl.json +0 -49
  338. package/node_modules/yargs/locales/pt.json +0 -45
  339. package/node_modules/yargs/locales/pt_BR.json +0 -48
  340. package/node_modules/yargs/locales/ru.json +0 -46
  341. package/node_modules/yargs/locales/th.json +0 -46
  342. package/node_modules/yargs/locales/tr.json +0 -48
  343. package/node_modules/yargs/locales/zh_CN.json +0 -48
  344. package/node_modules/yargs/locales/zh_TW.json +0 -47
  345. package/node_modules/yargs/package.json +0 -92
  346. package/node_modules/yargs/yargs.js +0 -14
  347. package/node_modules/yargs-parser/CHANGELOG.md +0 -601
  348. package/node_modules/yargs-parser/LICENSE.txt +0 -14
  349. package/node_modules/yargs-parser/README.md +0 -449
  350. package/node_modules/yargs-parser/index.js +0 -1032
  351. package/node_modules/yargs-parser/lib/tokenize-arg-string.js +0 -40
  352. package/node_modules/yargs-parser/package.json +0 -46
@@ -1,601 +0,0 @@
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
- }