@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,167 @@
1
+ // 端点登记表(ADR 0019「能力登记强制化」;评审 #32 note 1286 的 W2)。
2
+ //
3
+ // 三条判据里「能力能不能用」看的是**能力声明**;而声明要成立,前提是"新增端点时必须有人
4
+ // 登记它"。此前这件事只写在 CONTRIBUTING 的文字要求里,没有机制兜底——于是必然漏。
5
+ //
6
+ // 这里把登记变成**可被测试检出**的东西:
7
+ // - 从 `lib/index.js` 抽取所有正向分派(`rest === "..."`、`new Set([...]).has(rest)`),
8
+ // 与登记表**双向**比对:漏登记、陈旧登记、**方法不符**都会失败;
9
+ // - 抽取正则容忍引号与空白差异(`rest === '/x'`、`rest==="/x"` 同样会被抓到);
10
+ // - 每条登记项都必须显式写出 `capability`(能力键或 `null`);写了键就必须真的在
11
+ // bootstrap 的 capabilities 里声明过,否则失败。
12
+ //
13
+ // 已知边界(不假装更强):`methods` 由源码按分派段落推导,是**契约快照**而非运行期保证;
14
+ // 端点自身仍靠 `requireGet`/`method` 检查在运行时兜底。
15
+
16
+ /** 抽取正向分派所用的源码文件(相对仓库根)。 */
17
+ export const ENDPOINT_DISPATCH_SOURCE = "lib/index.js";
18
+
19
+ /** 从源码抽取"正向分派"及其方法(按分派段落判定,不看固定窗口)。 */
20
+ export function extractEndpointDetails(source) {
21
+ const lines = source.split("\n");
22
+ const marks = [];
23
+ lines.forEach((line, index) => {
24
+ for (const m of line.matchAll(/rest\s*===\s*['"]([^'"]+)['"]/g)) marks.push({ index, path: m[1] });
25
+ for (const group of line.matchAll(/new Set\(\[([^\]]+)\]\)\s*\.has\(rest\)/g)) {
26
+ for (const m of group[1].matchAll(/['"]([^'"]+)['"]/g)) marks.push({ index, path: m[1] });
27
+ }
28
+ });
29
+ marks.sort((a, b) => a.index - b.index);
30
+ const markLines = [...new Set(marks.map((m) => m.index))].sort((a, b) => a - b);
31
+ const out = new Map();
32
+ for (const { index, path } of marks) {
33
+ if (out.has(path)) continue;
34
+ const next = markLines.find((l) => l > index);
35
+ const segment = lines.slice(index, next ?? Math.min(lines.length, index + 400)).join("\n");
36
+ let methods = "GET/POST";
37
+ if (/requireGet\(method,\s*res\)/.test(segment) || /method\s*!==\s*['"]GET['"]/.test(segment)) methods = "GET";
38
+ else if (/method\s*!==\s*['"]POST['"]/.test(segment)) methods = "POST";
39
+ out.set(path, methods);
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * 抽取 **bootstrap 响应**里实际声明的能力键(G2)。
46
+ * 只认 `/bootstrap` 处理器里的那个 `capabilities: {...}` 块——SSE hello 里也有
47
+ * `capabilities`,按"源码里第一个"抽取会误判(那样负向变异根本测不出来)。
48
+ */
49
+ export function extractDeclaredCapabilities(source) {
50
+ const lines = source.split("\n");
51
+ const bootstrapAt = lines.findIndex((l) => /rest\s*===\s*['"]\/bootstrap['"]/.test(l));
52
+ const from = bootstrapAt >= 0 ? bootstrapAt : 0;
53
+ const tail = lines.slice(from).join("\n");
54
+ const m = /capabilities:\s*\{/.exec(tail);
55
+ if (!m) return new Set();
56
+ // 从 `{` 起做花括号配平,取出完整块
57
+ const open = tail.indexOf("{", m.index);
58
+ let depth = 0;
59
+ let end = -1;
60
+ for (let i = open; i < tail.length; i++) {
61
+ if (tail[i] === "{") depth++;
62
+ else if (tail[i] === "}") {
63
+ depth--;
64
+ if (depth === 0) {
65
+ end = i;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ if (end < 0) return new Set();
71
+ const block = tail.slice(open + 1, end);
72
+ return new Set([...block.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:/g)].map((x) => x[1]));
73
+ }
74
+
75
+ /** 未声明能力时的显式豁免理由(登记表要求二者必居其一)。 */
76
+ export const NO_CAPABILITY_NEEDED = "端点级能力由 App 按 404/缺字段降级;能力声明机制覆盖的是帧/字段级特性";
77
+
78
+ /**
79
+ * 双向比对。返回 `missing`(源码有、表里没有)、`stale`(表里有、源码没有)、
80
+ * `methodMismatch`(方法不符)与 `undeclaredCapabilities`(登记了未声明的能力键)。
81
+ */
82
+ export function diffRegisteredEndpoints(source, registered = REGISTERED_ENDPOINTS) {
83
+ const dispatched = extractEndpointDetails(source);
84
+ const known = new Map(registered.map((e) => [e.path, e]));
85
+ const declared = extractDeclaredCapabilities(source);
86
+ const missing = [...dispatched.keys()].filter((p) => !known.has(p)).sort();
87
+ const stale = [...known.keys()].filter((p) => !dispatched.has(p)).sort();
88
+ const methodMismatch = [...dispatched.entries()]
89
+ .filter(([p, methods]) => known.has(p) && known.get(p).methods && known.get(p).methods !== methods)
90
+ .map(([p, methods]) => ({ path: p, registered: known.get(p).methods, source: methods }))
91
+ .sort((a, b) => a.path.localeCompare(b.path));
92
+ const undeclaredCapabilities = [...new Set(
93
+ registered.map((e) => e.capability).filter((c) => typeof c === "string" && c !== "" && !declared.has(c)),
94
+ )].sort();
95
+ const missingCapabilityField = registered.filter((e) => !("capability" in e)).map((e) => e.path).sort();
96
+ // 没关联能力键的必须给出**显式豁免理由**(G2:不允许"忘了写"和"确实不需要"长得一样)
97
+ const missingExemption = registered
98
+ .filter((e) => (e.capability === null || e.capability === undefined) && !(typeof e.why === "string" && e.why.trim() !== ""))
99
+ .map((e) => e.path)
100
+ .sort();
101
+ return { missing, stale, methodMismatch, undeclaredCapabilities, missingCapabilityField, missingExemption, dispatched, declared };
102
+ }
103
+
104
+ /** 已登记的 `/m/api/*` 端点(`path` 是去掉 `/m/api` 前缀后的部分)。 */
105
+ export const REGISTERED_ENDPOINTS = Object.freeze([
106
+ { path: "/account-usage", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
107
+ { path: "/actions", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
108
+ { path: "/attachment", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
109
+ { path: "/balance", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
110
+ { path: "/bootstrap", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
111
+ { path: "/catalog", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
112
+ { path: "/commands", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
113
+ { path: "/defaults", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
114
+ { path: "/dev/approval-test", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
115
+ { path: "/diagnostics", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
116
+ { path: "/directories", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
117
+ { path: "/event-detail", methods: "GET", capability: "eventTimeline" },
118
+ { path: "/events", methods: "GET", capability: "eventTimeline" },
119
+ { path: "/feedback", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
120
+ { path: "/files", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
121
+ { path: "/files/upload", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
122
+ { path: "/git/branches", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
123
+ { path: "/git/capabilities", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
124
+ { path: "/git/commit", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
125
+ { path: "/git/file-history", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
126
+ { path: "/git/files", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
127
+ { path: "/git/graph", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
128
+ { path: "/git/preview", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
129
+ { path: "/git/repository", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
130
+ { path: "/git/worktree", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
131
+ { path: "/goal", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
132
+ { path: "/history", methods: "GET", capability: "eventTimeline" },
133
+ { path: "/jobs", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
134
+ { path: "/jobs/kill", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
135
+ { path: "/llm-providers", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
136
+ { path: "/llm-providers/probe", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
137
+ { path: "/messages", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
138
+ { path: "/notifications", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
139
+ { path: "/notifications/delete", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
140
+ { path: "/notifications/read", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
141
+ { path: "/plugin-update/check", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
142
+ { path: "/plugin-update/prepare", methods: "POST", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
143
+ { path: "/plugin-update/log", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
144
+ { path: "/plugin-update", methods: "POST", capability: null, why: NO_CAPABILITY_NEEDED },
145
+ { path: "/push-test", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
146
+ { path: "/qr-config", methods: "GET", capability: null, auth: "local-privileged", why: NO_CAPABILITY_NEEDED },
147
+ { path: "/queue", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
148
+ { path: "/respond", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
149
+ { path: "/send", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
150
+ { path: "/send-receipt", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
151
+ { path: "/session-config", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
152
+ { path: "/sessions", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
153
+ { path: "/sessions/archive", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
154
+ { path: "/sessions/fork", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
155
+ { path: "/sessions/stop", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
156
+ { path: "/sessions/touch", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
157
+ { path: "/sessions/unarchive", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
158
+ { path: "/subagents", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
159
+ { path: "/subagents/interrupt", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
160
+ { path: "/todos", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
161
+ { path: "/turn-outline", methods: "GET", capability: "turnOutline" },
162
+ { path: "/update/apk", methods: "GET", capability: null, auth: "anonymous", why: NO_CAPABILITY_NEEDED },
163
+ { path: "/update/manifest", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
164
+ { path: "/update/plugin", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
165
+ { path: "/usage", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
166
+ { path: "/workspaces", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
167
+ ]);
@@ -0,0 +1,450 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { realpath, open, lstat } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+
6
+ const UNTRACKED_PREVIEW_MAX = 512 * 1024;
7
+ const SIGNATURE_SAMPLE_BYTES = 4096;
8
+ const readOnlyNoFollow = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
9
+ function sameRegularFile(expected, opened) {
10
+ return expected.isFile() && opened.isFile() && expected.dev === opened.dev && expected.ino === opened.ino;
11
+ }
12
+ async function readPrefix(path, maximum, expected) {
13
+ const handle = await open(path, readOnlyNoFollow);
14
+ try {
15
+ if (!sameRegularFile(expected, await handle.stat())) throw new Error("file changed while opening preview");
16
+ const buffer = Buffer.alloc(maximum + 1);
17
+ const { bytesRead } = await handle.read(buffer, 0, maximum + 1, 0);
18
+ return { bytes: buffer.subarray(0, bytesRead), truncated: bytesRead > maximum };
19
+ } finally { await handle.close(); }
20
+ }
21
+ async function sampledFileSignature(path, info) {
22
+ const handle = await open(path, readOnlyNoFollow);
23
+ try {
24
+ if (!sameRegularFile(info, await handle.stat())) throw new Error("file changed while sampling worktree");
25
+ const first = Buffer.alloc(Math.min(SIGNATURE_SAMPLE_BYTES, info.size));
26
+ const firstRead = await handle.read(first, 0, first.length, 0);
27
+ let last = Buffer.alloc(0);
28
+ if (info.size > SIGNATURE_SAMPLE_BYTES) {
29
+ last = Buffer.alloc(SIGNATURE_SAMPLE_BYTES);
30
+ const read = await handle.read(last, 0, last.length, Math.max(0, info.size - last.length));
31
+ last = last.subarray(0, read.bytesRead);
32
+ }
33
+ return createHash("sha256").update(`${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}:`).update(first.subarray(0, firstRead.bytesRead)).update(last).digest("hex");
34
+ } finally { await handle.close(); }
35
+ }
36
+
37
+ function gitError(code, status, message = code) {
38
+ const error = new Error(message);
39
+ error.code = code;
40
+ error.status = status;
41
+ return error;
42
+ }
43
+
44
+ const refShortName = (name) => name === "HEAD" ? "HEAD" : String(name).replace(/^refs\/(?:heads|remotes|tags)\//, "");
45
+ const HISTORY_STATUS_LETTER = { A: "A", M: "M", D: "D", R: "R", C: "C", T: "T" };
46
+
47
+ /**
48
+ * Parse `git log --format=%x00<meta> --name-status -M -z` output into file
49
+ * history entries. Records are NUL-delimited: a `\x00` start marker, the
50
+ * `\x1f`-separated metadata, then `\x00\n<status>` and its NUL-separated paths.
51
+ * A deadline-aborted command may end mid-record; incomplete trailing records
52
+ * are ignored while every fully parsed commit stays usable.
53
+ */
54
+ function parseFileHistory(stdout) {
55
+ const tokens = String(stdout ?? "").split("\0");
56
+ const entries = [];
57
+ let index = 0;
58
+ while (index < tokens.length) {
59
+ const metadata = tokens[index++];
60
+ if (!metadata || metadata.startsWith("\n")) continue;
61
+ const fields = metadata.split("\x1f");
62
+ if (fields.length < 5) continue;
63
+ const oid = fields[0];
64
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid)) continue;
65
+ let status = "M";
66
+ if (index < tokens.length) {
67
+ const marker = tokens[index];
68
+ if (typeof marker === "string" && marker.startsWith("\n")) {
69
+ index++;
70
+ const code = marker.slice(1).trim();
71
+ if (code) {
72
+ status = HISTORY_STATUS_LETTER[code[0]] ?? "M";
73
+ index += /^[RC]/.test(code) ? 2 : 1;
74
+ }
75
+ }
76
+ }
77
+ entries.push({
78
+ oid,
79
+ shortOid: fields[1] || oid.slice(0, 7),
80
+ author: fields[2] ?? "",
81
+ timestamp: Number(fields[3]) || 0,
82
+ subject: fields.slice(4).join("\x1f"),
83
+ status,
84
+ });
85
+ }
86
+ return entries;
87
+ }
88
+
89
+ /** Bundled, read-only Git provider. Every command uses the host subprocess service. */
90
+ export function createBundledGitReadProvider(ctx) {
91
+ const get = (name) => {
92
+ try { return ctx.get?.(name); } catch { return undefined; }
93
+ };
94
+ const subprocess = () => get("subprocess") ?? ctx.subprocess;
95
+ const workspaces = () => (get("workspaceRegistry")?.list?.() ?? [])
96
+ .map((entry) => entry?.path).filter((path) => typeof path === "string" && path !== "");
97
+ const inside = (child, parent) => {
98
+ const value = relative(resolve(parent), resolve(child));
99
+ return value === "" || (value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value));
100
+ };
101
+ const assertWorkspace = async (path) => {
102
+ if (typeof path !== "string" || path === "") throw gitError("workspace-not-allowed", 403);
103
+ let canonical;
104
+ try { canonical = await realpath(path); } catch { throw gitError("workspace-not-allowed", 403); }
105
+ const roots = await Promise.all(workspaces().map((root) => realpath(root).catch(() => null)));
106
+ if (!roots.some((root) => root && inside(canonical, root))) throw gitError("workspace-not-allowed", 403);
107
+ return canonical;
108
+ };
109
+ const spawnGit = async (root, args, { disableFilters = false, allowNoMatches = false, timeoutMs = 0 } = {}) => {
110
+ root = await assertWorkspace(root);
111
+ const executor = subprocess();
112
+ if (!executor?.spawn) throw gitError("git-provider-unavailable", 503);
113
+ const noHooksPath = process.platform === "win32" ? "NUL" : "/dev/null";
114
+ let filterOverrides = [];
115
+ if (disableFilters) {
116
+ const names = await run(root, ["config", "--null", "--name-only", "--get-regexp", "^filter\\..*\\.(clean|process)$"], false, true);
117
+ filterOverrides = names.split("\0").filter(Boolean).flatMap((name) => ["-c", `${name}=`]);
118
+ }
119
+ // A soft deadline aborts the command and returns the output collected so far,
120
+ // so file history can degrade to a partial, explicitly truncated page instead
121
+ // of failing the request. Hardening below is shared with the batch helper.
122
+ const controller = timeoutMs > 0 ? new AbortController() : null;
123
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
124
+ try {
125
+ const child = executor.spawn({
126
+ argv: [
127
+ process.platform === "win32" ? "git.exe" : "git",
128
+ "-c", "core.fsmonitor=false",
129
+ "-c", `core.hooksPath=${noHooksPath}`,
130
+ ...filterOverrides,
131
+ "-C", root,
132
+ "--no-optional-locks",
133
+ ...args,
134
+ ],
135
+ cwd: root,
136
+ // Prevent ambient Git repository/config overrides from redirecting a
137
+ // session-authorized read. Undefined values are seam tombstones.
138
+ env: {
139
+ GIT_TERMINAL_PROMPT: "0",
140
+ GCM_INTERACTIVE: "never",
141
+ GIT_DIR: undefined,
142
+ GIT_WORK_TREE: undefined,
143
+ GIT_INDEX_FILE: undefined,
144
+ GIT_COMMON_DIR: undefined,
145
+ GIT_OBJECT_DIRECTORY: undefined,
146
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: undefined,
147
+ GIT_CONFIG: undefined,
148
+ GIT_CONFIG_PARAMETERS: undefined,
149
+ GIT_CONFIG_COUNT: "0",
150
+ // Trace destinations open files even for otherwise read-only Git commands.
151
+ ...Object.fromEntries(Object.keys(process.env).filter((key) => /^GIT_TRACE(?:2)?(?:_|$)/.test(key)).map((key) => [key, undefined])),
152
+ GIT_TRACE: undefined,
153
+ GIT_TRACE2: undefined,
154
+ GIT_TRACE2_EVENT: undefined,
155
+ GIT_TRACE2_PERF: undefined,
156
+ },
157
+ stdio: { stdin: "ignore", stdout: { maxBytes: 8 * 1024 * 1024 }, stderr: { maxBytes: 1024 * 1024 } },
158
+ graceMs: 10_000,
159
+ ...(controller ? { signal: controller.signal } : {}),
160
+ });
161
+ let done;
162
+ try { done = await child.done; } catch (error) {
163
+ if (!controller?.signal.aborted) throw error;
164
+ done = { exitCode: null };
165
+ }
166
+ const stdoutResult = await child.collected?.stdout?.readFrom?.(0);
167
+ const stderrResult = await child.collected?.stderr?.readFrom?.(0);
168
+ if (stdoutResult?.lossy || stderrResult?.lossy) {
169
+ throw gitError("git-output-too-large", 413);
170
+ }
171
+ const stdout = stdoutResult?.text ?? "";
172
+ const stderr = stderrResult?.text ?? "";
173
+ if (controller?.signal.aborted) return { stdout, stderr, timedOut: true };
174
+ if (done?.exitCode !== 0) {
175
+ if (allowNoMatches && done?.exitCode === 1) return { stdout: "", stderr, timedOut: false };
176
+ if (/not a git repository/i.test(stderr)) throw gitError("not-git-repository", 404);
177
+ throw gitError("git-command-failed", 400);
178
+ }
179
+ return { stdout, stderr, timedOut: false };
180
+ } finally {
181
+ if (timer) clearTimeout(timer);
182
+ }
183
+ };
184
+ const run = async (root, args, disableFilters = false, allowNoMatches = false) =>
185
+ (await spawnGit(root, args, { disableFilters, allowNoMatches })).stdout;
186
+ const repositoryRoot = async (cwd) => {
187
+ const allowed = await assertWorkspace(cwd);
188
+ const root = (await run(allowed, ["rev-parse", "--show-toplevel"])).trim();
189
+ return assertWorkspace(root);
190
+ };
191
+ return {
192
+ contractVersion: "1.1.0",
193
+ capabilities() {
194
+ const available = Boolean(subprocess()?.spawn) && workspaces().length > 0;
195
+ return { available, reason: available ? null : !subprocess()?.spawn ? "git-provider-unavailable" : "workspace-registry-unavailable" };
196
+ },
197
+ repositoryRoot,
198
+ async refSignature(root) {
199
+ let refs = "";
200
+ try { refs = await run(root, ["show-ref", "--head", "--dereference"]); } catch (error) { if (error?.code !== "git-command-failed") throw error; }
201
+ return createHash("sha256").update(refs).digest("hex");
202
+ },
203
+ async repositoryInfo(root) {
204
+ let currentBranch = null, headOid = null;
205
+ try { currentBranch = (await run(root, ["symbolic-ref", "--quiet", "HEAD"])).trim() || null; } catch (error) { if (error?.code !== "git-command-failed") throw error; }
206
+ try { headOid = (await run(root, ["rev-parse", "--verify", "HEAD"])).trim() || null; } catch (error) { if (error?.code !== "git-command-failed") throw error; }
207
+ return { ...(headOid ? { headOid } : {}), ...(currentBranch ? { currentBranch } : {}), detached: Boolean(headOid && !currentBranch), empty: !headOid };
208
+ },
209
+ async resolveTip(root, ref) {
210
+ return (await run(root, ["rev-parse", "--verify", `${ref}^{commit}`])).trim();
211
+ },
212
+ async graph(root, { refs, offset, limit }) {
213
+ if (refs.length === 0) return [];
214
+ const text = await run(root, ["log", "--topo-order", "--date-order", `--skip=${offset}`, `--max-count=${limit}`, "--decorate=full", "--format=%H%x1f%P%x1f%an%x1f%at%x1f%s%x1f%D%x00", ...refs]);
215
+ return text.split("\0").map((record) => record.replace(/^[\r\n]+|[\r\n]+$/g, "")).filter(Boolean).map((record) => {
216
+ const [oid, parents, author, timestamp, subject, decorations] = record.split("\x1f");
217
+ return { oid, parents: parents ? parents.split(" ") : [], author, timestamp: Number(timestamp) || 0, subject, refs: decorations ? decorations.split(",").map((value) => value.trim()).filter(Boolean) : [] };
218
+ });
219
+ },
220
+ async commit(root, oid) {
221
+ if (typeof oid !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(oid)) throw gitError("invalid-oid", 400);
222
+ const metadata = (await run(root, ["show", "-s", "--decorate=full", "--format=%H%x1f%P%x1f%an%x1f%at%x1f%D%x1f%B%x00", oid])).split("\0")[0].split("\x1f");
223
+ const [hash, parentText, author, timestamp, decorations, ...messageParts] = metadata;
224
+ const diffBase = parentText ? [parentText.split(" ")[0], hash] : ["--root", hash];
225
+ const statusParts = (await run(root, ["diff-tree", "--no-commit-id", "-r", "-M", "--name-status", "-z", ...diffBase])).split("\0");
226
+ const files = [];
227
+ for (let index = 0; index < statusParts.length;) {
228
+ const code = statusParts[index++];
229
+ if (!code) continue;
230
+ const renamed = code.startsWith("R") || code.startsWith("C");
231
+ const oldPath = renamed ? statusParts[index++] : undefined;
232
+ const path = statusParts[index++];
233
+ if (!path) break;
234
+ const statuses = { A: "added", M: "modified", D: "deleted", R: "renamed", C: "copied", T: "type-changed" };
235
+ files.push({ path, ...(oldPath ? { oldPath } : {}), status: statuses[code[0]] ?? "modified" });
236
+ }
237
+ const statParts = (await run(root, ["diff-tree", "--no-commit-id", "-r", "-M", "--numstat", "-z", ...diffBase])).split("\0");
238
+ const stats = new Map();
239
+ for (let index = 0; index < statParts.length;) {
240
+ const record = statParts[index++];
241
+ if (!record) continue;
242
+ const firstTab = record.indexOf("\t");
243
+ const secondTab = record.indexOf("\t", firstTab + 1);
244
+ if (firstTab < 0 || secondTab < 0) continue;
245
+ const added = record.slice(0, firstTab);
246
+ const deleted = record.slice(firstTab + 1, secondTab);
247
+ let path = record.slice(secondTab + 1);
248
+ if (!path) { index++; path = statParts[index++] ?? ""; }
249
+ stats.set(path, { binary: added === "-" || deleted === "-", additions: added === "-" ? null : Number(added), deletions: deleted === "-" ? null : Number(deleted) });
250
+ }
251
+ for (const file of files) Object.assign(file, stats.get(file.path) ?? { binary: false, additions: 0, deletions: 0 });
252
+ return {
253
+ oid: hash, parents: parentText ? parentText.split(" ") : [], author, timestamp: Number(timestamp) || 0,
254
+ message: messageParts.join("\x1f").trim(),
255
+ refs: decorations ? decorations.split(",").map((value) => value.trim()).filter(Boolean) : [],
256
+ files,
257
+ };
258
+ },
259
+ /**
260
+ * All branch, remote, tag and HEAD refs with name-level facts plus
261
+ * annotated-tag metadata. Non-commit and nested tag targets carry a hint
262
+ * instead of failing the whole read.
263
+ */
264
+ async refs(root) {
265
+ const records = [];
266
+ // NUL/US-delimited fields keep tagger names and tag subjects that contain
267
+ // tabs intact; a tab-delimited line format would corrupt such records.
268
+ const text = await run(root, ["for-each-ref", "--format=%(refname)%1f%(objectname)%1f%(objecttype)%1f%(HEAD)%1f%(taggername)%1f%(taggerdate:unix)%1f%(subject)%1f%(*objectname)%1f%(*objecttype)%00", "refs/heads", "refs/remotes", "refs/tags"]);
269
+ for (const raw of text.split("\0")) {
270
+ const record = raw.replace(/^[\r\n]+|[\r\n]+$/g, "");
271
+ if (!record) continue;
272
+ const fields = record.split("\x1f");
273
+ if (fields.length < 9) continue;
274
+ // Peeled fields are fixed at the tail so a subject containing the
275
+ // unit separator cannot swallow them.
276
+ const [name, oid, objectType, headFlag, taggerName, taggerDate] = fields;
277
+ const peeledType = fields.at(-1);
278
+ const peeledOid = fields.at(-2);
279
+ const subject = fields.slice(6, -2).join("\x1f");
280
+ if (!name || !oid) continue;
281
+ if (name.startsWith("refs/remotes/") && name.endsWith("/HEAD")) continue;
282
+ if (name.startsWith("refs/heads/") || name.startsWith("refs/remotes/")) {
283
+ const commit = objectType === "commit";
284
+ records.push({
285
+ name, shortName: refShortName(name),
286
+ kind: name.startsWith("refs/heads/") ? "localBranch" : "remoteBranch",
287
+ oid, commitOid: commit ? oid : null, current: headFlag === "*", annotated: false,
288
+ ...(commit ? {} : { hint: "non-commit-target" }),
289
+ });
290
+ continue;
291
+ }
292
+ if (!name.startsWith("refs/tags/")) continue;
293
+ if (objectType !== "tag") {
294
+ // Lightweight tag: the ref itself points at the target object.
295
+ const commit = objectType === "commit";
296
+ records.push({
297
+ name, shortName: refShortName(name), kind: "tag",
298
+ oid, commitOid: commit ? oid : null, current: false, annotated: false,
299
+ ...(commit ? {} : { hint: "non-commit-target" }),
300
+ });
301
+ continue;
302
+ }
303
+ let commitOid = null, hint;
304
+ if (peeledType === "commit") commitOid = peeledOid;
305
+ else if (peeledType === "tag") {
306
+ // Nested tag: peel recursively; when the chain ends outside a
307
+ // commit we still keep the name and advertise the hint.
308
+ try { commitOid = (await run(root, ["rev-parse", "--verify", `${name}^{commit}`])).trim() || null; } catch { commitOid = null; }
309
+ hint = commitOid ? "nested-tag" : "non-commit-target";
310
+ } else hint = "non-commit-target";
311
+ const date = Number(taggerDate);
312
+ records.push({
313
+ name, shortName: refShortName(name), kind: "tag", oid, commitOid, current: false, annotated: true,
314
+ ...(taggerName ? { tagger: taggerName } : {}),
315
+ ...(Number.isFinite(date) && date > 0 ? { date } : {}),
316
+ ...(subject ? { subject } : {}),
317
+ ...(hint ? { hint } : {}),
318
+ });
319
+ }
320
+ let headOid = null;
321
+ try { headOid = (await run(root, ["rev-parse", "--verify", "HEAD"])).trim() || null; } catch (error) { if (error?.code !== "git-command-failed") throw error; }
322
+ if (headOid) records.unshift({ name: "HEAD", shortName: "HEAD", kind: "head", oid: headOid, commitOid: headOid, current: true, annotated: false });
323
+ return records;
324
+ },
325
+ /** Tracked files at HEAD, bounded and filtered by a case-insensitive path substring. */
326
+ async files(root, { q = "", limit = 200 } = {}) {
327
+ const pageSize = Math.max(1, Math.min(200, Number(limit) || 200));
328
+ // The recursive HEAD path list is bounded by the subprocess stdout cap
329
+ // (8 MiB, see spawnGit). Repositories above that cap fail closed with
330
+ // `git-output-too-large` (413) instead of returning a partial list; the
331
+ // bound is documented in docs/03-api.md §6.19.7.
332
+ let text;
333
+ try { text = await run(root, ["ls-tree", "-r", "-z", "--name-only", "HEAD"]); } catch (error) {
334
+ if (error?.code !== "git-command-failed") throw error;
335
+ // An unborn HEAD is an empty repository, not a failure: /git/files
336
+ // must answer 200 with an empty list for it.
337
+ try { await run(root, ["rev-parse", "--verify", "HEAD"]); } catch (inner) {
338
+ if (inner?.code === "git-command-failed") return { files: [], truncated: false };
339
+ throw inner;
340
+ }
341
+ throw error;
342
+ }
343
+ const query = typeof q === "string" ? q.toLowerCase() : "";
344
+ let paths = text.split("\0").filter((path) => path !== "");
345
+ if (query) paths = paths.filter((path) => path.toLowerCase().includes(query));
346
+ paths.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
347
+ const truncated = paths.length > pageSize;
348
+ return { files: paths.slice(0, pageSize), truncated };
349
+ },
350
+ /**
351
+ * Commits touching one file, newest first, with the change status that
352
+ * commit applied to the path. Bounded by `maxCommits` or the soft
353
+ * deadline; both report an explicitly truncated (partial) list.
354
+ */
355
+ async fileHistory(root, path, { maxCommits = 5000, timeoutMs = 20000 } = {}) {
356
+ if (typeof path !== "string" || path === "" || path.includes("\0")) throw gitError("git-command-failed", 400);
357
+ try { await run(root, ["rev-parse", "--verify", "HEAD"]); } catch (error) {
358
+ if (error?.code === "git-command-failed") return { entries: [], truncated: false, reason: "no-history" };
359
+ throw error;
360
+ }
361
+ let tracked = false;
362
+ try {
363
+ const tree = await run(root, ["ls-tree", "-z", "HEAD", "--", `:(literal)${path}`]);
364
+ for (const record of tree.split("\0")) {
365
+ const separator = record.indexOf("\t");
366
+ if (separator < 0) continue;
367
+ const entryPath = record.slice(separator + 1);
368
+ const entryType = record.slice(0, separator).split(" ")[1];
369
+ // Directories, gitlinks and other tree entries are not file paths;
370
+ // only blobs (regular files and symlinks) have a file history.
371
+ if (entryPath === path && entryType === "blob") tracked = true;
372
+ }
373
+ } catch (error) {
374
+ if (error?.code !== "git-command-failed") throw error;
375
+ }
376
+ if (!tracked) return { entries: [], truncated: false, reason: "not-tracked" };
377
+ const cap = Math.max(1, Math.min(5000, Number(maxCommits) || 5000));
378
+ const result = await spawnGit(root, ["--literal-pathspecs", "log", `--max-count=${cap + 1}`, "--format=%x00%H%x1f%h%x1f%an%x1f%at%x1f%s", "--name-status", "-M", "-z", "--", path], { timeoutMs });
379
+ let entries = parseFileHistory(result.stdout);
380
+ let truncated = result.timedOut === true;
381
+ if (entries.length > cap) {
382
+ entries = entries.slice(0, cap);
383
+ truncated = true;
384
+ }
385
+ return { entries, truncated };
386
+ },
387
+ async worktree(root) {
388
+ const text = await run(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], true);
389
+ const parts = text.split("\0"); const entries = [];
390
+ for (let i = 0; i < parts.length;) {
391
+ const record = parts[i++]; if (!record) continue;
392
+ const code = record.slice(0, 2); let path = record.slice(3); let oldPath;
393
+ if (/[RC]/.test(code)) { oldPath = parts[i++] ?? ""; }
394
+ const status = code.includes("U") || code === "AA" || code === "DD" ? "conflicted" : code === "??" ? "untracked" : "modified";
395
+ entries.push({ path, ...(oldPath ? { oldPath } : {}), status, conflicted: status === "conflicted", index: code[0] !== " " && code !== "??", worktree: code[1] !== " " && code !== "??" });
396
+ }
397
+ let signatureMaterial = text;
398
+ for (const entry of entries) {
399
+ if (entry.status !== "untracked" && !entry.worktree) continue;
400
+ try { const target = resolve(root, entry.path); const info = await lstat(target); signatureMaterial += `\\0${entry.path}:${info.isSymbolicLink() ? "symlink" : info.isFile() ? await sampledFileSignature(target, info) : "special"}`; } catch { signatureMaterial += `\\0${entry.path}:missing`; }
401
+ }
402
+ try { signatureMaterial += await run(root, ["--literal-pathspecs", "diff", "--raw", "-z", "--no-abbrev", "HEAD"], true); } catch (error) { if (error?.code !== "git-command-failed") throw error; }
403
+ try { signatureMaterial += await run(root, ["--literal-pathspecs", "diff", "--raw", "-z", "--no-abbrev", "--cached"], true); } catch (error) { if (error?.code !== "git-command-failed") throw error; }
404
+ const signature = createHash("sha256").update(signatureMaterial).digest("hex");
405
+ return { entries, signature };
406
+ },
407
+ async worktreePreview(root, kind, path) {
408
+ const args = ["--literal-pathspecs", "diff", "--unified=999999", "--no-ext-diff", "--no-textconv", "--no-color", "--binary"];
409
+ if (kind === "staged") args.push("--cached");
410
+ args.push("--", path);
411
+ if (kind === "untracked") {
412
+ try {
413
+ const target = resolve(root, path); const info = await lstat(target);
414
+ if (info.isSymbolicLink()) return { diff: "", binary: false, additions: null, deletions: null, notice: "symlink preview omitted" };
415
+ if (!info.isFile()) return { diff: "", binary: false, additions: null, deletions: null, notice: "special file preview omitted" };
416
+ const { bytes, truncated } = await readPrefix(target, UNTRACKED_PREVIEW_MAX, info);
417
+ const binary = bytes.includes(0);
418
+ return { diff: binary ? "" : bytes.toString("utf8"), binary, truncated, additions: null, deletions: null, ...(binary ? { notice: "binary preview omitted" } : {}) };
419
+ } catch { return { diff: "", binary: false, additions: 0, deletions: 0, notice: "unavailable" }; }
420
+ }
421
+ return { diff: await run(root, args, true), binary: false, additions: null, deletions: null };
422
+ },
423
+ async commitPreview(root, oid, path) {
424
+ const commit = await this.commit(root, oid);
425
+ if (!commit.parents.length) return { diff: await run(root, ["--literal-pathspecs", "diff-tree", "--root", "--no-commit-id", "-p", "-r", "--unified=999999", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", oid, "--", path]), binary: false };
426
+ return { diff: await run(root, ["--literal-pathspecs", "diff", "--unified=999999", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", `${oid}^1`, oid, "--", path]), binary: false };
427
+ },
428
+ async branches(root) {
429
+ const text = await run(root, ["for-each-ref", "--format=%(HEAD)%09%(refname)%09%(objectname)%09%(upstream:short)%00", "refs/heads", "refs/remotes"]);
430
+ const rows = text.split("\0").map((record) => record.replace(/^[\r\n]+|[\r\n]+$/g, "")).filter(Boolean);
431
+ const branches = [];
432
+ for (const row of rows) {
433
+ const [head, ref, oid, upstream = ""] = row.split("\t");
434
+ if (ref.endsWith("/HEAD")) continue;
435
+ const local = ref.startsWith("refs/heads/");
436
+ let ahead = 0, behind = 0;
437
+ if (local && upstream) {
438
+ const counts = (await run(root, ["rev-list", "--left-right", "--count", `${ref}...${upstream}`])).trim().split(/\s+/).map(Number);
439
+ [ahead, behind] = counts;
440
+ }
441
+ branches.push({
442
+ name: ref, displayName: ref.replace(/^refs\/(?:heads|remotes)\//, ""), oid,
443
+ kind: local ? "local" : "remote", current: local && head === "*",
444
+ tracking: local && upstream ? `refs/remotes/${upstream}` : null, ahead, behind,
445
+ });
446
+ }
447
+ return branches;
448
+ },
449
+ };
450
+ }