@edspencer/paddock 0.0.1 → 0.57.0

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 (296) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +538 -0
  33. package/packages/server/dist/config.js +625 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,446 @@
1
+ /**
2
+ * GitService — the git-aware capability layer over the projects directory
3
+ * (backing-store design, Phase 1: read surface).
4
+ *
5
+ * The base backing store is just a filesystem directory (`projectsRoot`). When
6
+ * that directory is a git working tree, paddock "lights up" git features. This
7
+ * module is the read half: detect whether the store is a repo, and report a
8
+ * single project's uncommitted changes + diff. Commit/push/auth come in Phase 2.
9
+ *
10
+ * It shells out to the `git` binary via `execFile` (no shell, arg arrays — no
11
+ * injection surface) so there's no new dependency. Every method degrades safely:
12
+ * if `git` is missing or the directory isn't a repo, it reports "not a repo"
13
+ * rather than throwing, so the rest of paddock is unaffected.
14
+ */
15
+ import { execFile } from "node:child_process";
16
+ import { promises as fs } from "node:fs";
17
+ import path from "node:path";
18
+ import { promisify } from "node:util";
19
+ const run = promisify(execFile);
20
+ /** Cap for reading an untracked file to count its added lines (bigger ⇒ skip). */
21
+ const UNTRACKED_STAT_CAP = 2 * 1024 * 1024;
22
+ /**
23
+ * Clone an external git repo into `dest` — the checkout that becomes a
24
+ * repo-backed project's working directory (issue #187). Shells out to the `git`
25
+ * binary via execFile (arg array, no shell → no injection surface), the same
26
+ * discipline as {@link GitService}.
27
+ *
28
+ * A FULL clone (not `--depth 1`): a repo-backed project is where you *do
29
+ * engineering*, so the keeper wants real history — `git log`, blame, bisect, and
30
+ * a non-shallow base for branches/PRs — from the start. Credentials are the
31
+ * ambient git environment's job (a public URL needs none; a private repo needs a
32
+ * box-level credential helper / token — per-project scoped credentials are a
33
+ * documented #187 follow-up). Throws with git's stderr on failure so the caller
34
+ * can surface a clean error and roll back the half-created project.
35
+ */
36
+ export async function cloneRepo(url, dest) {
37
+ try {
38
+ await run("git", ["clone", "--", url, dest], {
39
+ maxBuffer: MAX_BUFFER,
40
+ // Never let git prompt for credentials on a private URL — fail fast so the
41
+ // create request returns an error instead of hanging the server.
42
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
43
+ });
44
+ }
45
+ catch (err) {
46
+ throw new Error(`git clone failed: ${errText(err)}`);
47
+ }
48
+ }
49
+ const MAX_BUFFER = 16 * 1024 * 1024; // generous cap for status/diff output
50
+ export class GitService {
51
+ projectsRoot;
52
+ author;
53
+ /** Cached repo-detection result for `projectsRoot` (null = not yet checked). */
54
+ repoFlag = null;
55
+ /**
56
+ * @param projectsRoot Absolute path to the projects root the service operates on.
57
+ * @param author Commit identity used when Paddock commits on a project's behalf.
58
+ * Folded into PaddockConfig from `PADDOCK_GIT_AUTHOR_*` (issue #269); defaults
59
+ * preserve the pre-fold values so a bare `new GitService(root)` is unchanged.
60
+ */
61
+ constructor(projectsRoot, author = {
62
+ name: "Paddock",
63
+ email: "paddock@localhost",
64
+ }) {
65
+ this.projectsRoot = projectsRoot;
66
+ this.author = author;
67
+ }
68
+ /** Run git in a directory, returning stdout. Throws on non-zero exit. */
69
+ async git(cwd, args) {
70
+ const { stdout } = await run("git", args, { cwd, maxBuffer: MAX_BUFFER });
71
+ return stdout;
72
+ }
73
+ /**
74
+ * Whether the backing store is a git working tree. Cached after the first
75
+ * successful check (repo-ness doesn't change under a running server); a thrown
76
+ * git / missing-binary error resolves to `false`.
77
+ */
78
+ async isRepo() {
79
+ if (this.repoFlag !== null)
80
+ return this.repoFlag;
81
+ try {
82
+ const out = await this.git(this.projectsRoot, [
83
+ "rev-parse",
84
+ "--is-inside-work-tree",
85
+ ]);
86
+ this.repoFlag = out.trim() === "true";
87
+ }
88
+ catch {
89
+ this.repoFlag = false;
90
+ }
91
+ return this.repoFlag;
92
+ }
93
+ /** Force the next `isRepo()` to re-check (e.g. after `git init` at runtime). */
94
+ resetRepoCache() {
95
+ this.repoFlag = null;
96
+ }
97
+ /**
98
+ * Status for one project's subtree. Returns `{ repo: false }` when the store
99
+ * isn't a repo (the UI then hides the git affordance). Paths are relative to
100
+ * `projectDir`.
101
+ */
102
+ async projectStatus(projectDir) {
103
+ if (!(await this.isRepo())) {
104
+ return { repo: false, files: [], clean: true };
105
+ }
106
+ let branch;
107
+ try {
108
+ branch = (await this.git(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
109
+ if (branch === "HEAD")
110
+ branch = undefined; // detached
111
+ }
112
+ catch {
113
+ /* leave branch undefined */
114
+ }
115
+ // git reports porcelain paths relative to the REPO ROOT, but the UI (and the
116
+ // /git/diff ?file= param) want them relative to the PROJECT dir. show-prefix
117
+ // is the repo-root→projectDir path (e.g. "garage-water-heater/"); strip it.
118
+ let prefix = "";
119
+ try {
120
+ prefix = (await this.git(projectDir, ["rev-parse", "--show-prefix"])).trim();
121
+ }
122
+ catch {
123
+ /* leave prefix empty (project dir is the repo root) */
124
+ }
125
+ let files = [];
126
+ try {
127
+ const out = await this.git(projectDir, [
128
+ "status",
129
+ "--porcelain=v1",
130
+ "-z",
131
+ "--untracked-files=all",
132
+ "--",
133
+ ".",
134
+ ]);
135
+ files = parsePorcelainZ(out).map((f) => ({
136
+ ...f,
137
+ path: prefix && f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path,
138
+ }));
139
+ }
140
+ catch {
141
+ /* treat an unreadable status as clean rather than erroring the request */
142
+ }
143
+ // Attach per-file line stats (+/-) for the Changes tab. Tracked changes come
144
+ // from `git diff --numstat`; untracked (new) files count as all-added by
145
+ // reading them (bounded, binary-detected). Best-effort — a stat failure just
146
+ // leaves the counts undefined and the UI degrades to no badge.
147
+ if (files.length) {
148
+ const numstat = await this.trackedNumstat(projectDir);
149
+ for (const f of files) {
150
+ if (f.untracked) {
151
+ Object.assign(f, await untrackedStat(projectDir, f.path));
152
+ }
153
+ else {
154
+ const s = numstat.get(f.path);
155
+ if (s)
156
+ Object.assign(f, s);
157
+ }
158
+ }
159
+ }
160
+ return { repo: true, branch, files, clean: files.length === 0 };
161
+ }
162
+ /**
163
+ * `git diff --numstat` for a project's tracked working-tree changes, keyed by
164
+ * PROJECT-relative path (`--relative` makes git emit paths relative to the
165
+ * project dir, matching the porcelain paths). Binary changes report `-\t-` and
166
+ * are flagged `binary`. Returns an empty map on any failure.
167
+ */
168
+ async trackedNumstat(projectDir) {
169
+ const args = ["diff", "HEAD", "--numstat", "-z", "--relative", "--", "."];
170
+ let out = "";
171
+ try {
172
+ out = await this.git(projectDir, args);
173
+ }
174
+ catch {
175
+ // No HEAD yet (unborn branch) → plain working-tree numstat.
176
+ try {
177
+ out = await this.git(projectDir, ["diff", "--numstat", "-z", "--relative", "--", "."]);
178
+ }
179
+ catch {
180
+ return new Map();
181
+ }
182
+ }
183
+ return parseNumstatZ(out);
184
+ }
185
+ /**
186
+ * Unified diff for a project's tracked changes (working tree vs HEAD), or for
187
+ * a single file when `file` is given. Untracked files don't appear in a diff
188
+ * (they're listed by `projectStatus`). Returns "" when not a repo / no diff.
189
+ */
190
+ async projectDiff(projectDir, file) {
191
+ if (!(await this.isRepo()))
192
+ return "";
193
+ const args = ["diff", "HEAD", "--", file ?? "."];
194
+ try {
195
+ return await this.git(projectDir, args);
196
+ }
197
+ catch {
198
+ // No HEAD yet (unborn branch) → fall back to a plain working-tree diff.
199
+ try {
200
+ return await this.git(projectDir, ["diff", "--", file ?? "."]);
201
+ }
202
+ catch {
203
+ return "";
204
+ }
205
+ }
206
+ }
207
+ // --- phase 2: write surface (commit / push / remote) -------------------
208
+ /** Commit identity (from config, no global git config needed). */
209
+ identity() {
210
+ return this.author;
211
+ }
212
+ /** Origin remote info + ahead/behind vs upstream (best-effort). */
213
+ async remote() {
214
+ if (!(await this.isRepo()))
215
+ return { repo: false, configured: false };
216
+ let branch;
217
+ try {
218
+ branch = (await this.git(this.projectsRoot, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
219
+ if (branch === "HEAD")
220
+ branch = undefined;
221
+ }
222
+ catch {
223
+ /* detached / unborn */
224
+ }
225
+ let url;
226
+ try {
227
+ url = (await this.git(this.projectsRoot, ["remote", "get-url", "origin"])).trim();
228
+ }
229
+ catch {
230
+ return { repo: true, configured: false, branch };
231
+ }
232
+ let ahead;
233
+ let behind;
234
+ try {
235
+ const counts = (await this.git(this.projectsRoot, [
236
+ "rev-list",
237
+ "--left-right",
238
+ "--count",
239
+ "@{upstream}...HEAD",
240
+ ])).trim();
241
+ const [b, a] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10));
242
+ behind = Number.isFinite(b) ? b : undefined;
243
+ ahead = Number.isFinite(a) ? a : undefined;
244
+ }
245
+ catch {
246
+ /* no upstream yet */
247
+ }
248
+ return { repo: true, configured: true, url, branch, ahead, behind };
249
+ }
250
+ /**
251
+ * Stage + commit a single project's changes (tracked mods, deletions, and
252
+ * untracked files within its subtree). `committed: false` when nothing was
253
+ * pending. Explicit identity so the LXC needs no global git config.
254
+ *
255
+ * When `paths` (project-relative) is given, commit ONLY those files — the
256
+ * pathspec scopes both the staging and the commit, so unselected changes stay
257
+ * uncommitted (issue #258). Paths are validated to stay inside the subtree; an
258
+ * all-invalid selection is an error rather than a silent commit-everything.
259
+ * Omitting `paths` keeps the legacy commit-the-whole-subtree behavior.
260
+ */
261
+ async commitProject(projectDir, message, paths) {
262
+ if (!(await this.isRepo()))
263
+ return { committed: false, error: "not a repo" };
264
+ const msg = message.trim() || "Update project";
265
+ let pathspec;
266
+ if (paths && paths.length) {
267
+ const safe = paths.filter(isSafeRelPath);
268
+ if (!safe.length)
269
+ return { committed: false, error: "no valid files selected" };
270
+ pathspec = safe;
271
+ }
272
+ else {
273
+ pathspec = ["."];
274
+ }
275
+ try {
276
+ await this.git(projectDir, ["add", "-A", "--", ...pathspec]);
277
+ const staged = (await this.git(projectDir, ["diff", "--cached", "--name-only", "--", ...pathspec])).trim();
278
+ if (!staged)
279
+ return { committed: false };
280
+ const { name, email } = this.identity();
281
+ await this.git(projectDir, [
282
+ "-c",
283
+ `user.name=${name}`,
284
+ "-c",
285
+ `user.email=${email}`,
286
+ "commit",
287
+ "-m",
288
+ msg,
289
+ "--",
290
+ ...pathspec,
291
+ ]);
292
+ const hash = (await this.git(projectDir, ["rev-parse", "HEAD"])).trim();
293
+ return { committed: true, hash };
294
+ }
295
+ catch (err) {
296
+ return { committed: false, error: errText(err) };
297
+ }
298
+ }
299
+ /**
300
+ * Uncommitted-file counts per top-level project subtree, in ONE `git status`
301
+ * over the whole store (cheap — no per-project fan-out) so the projects grid
302
+ * can flag "N uncommitted" without opening each project (issue #258). Keyed by
303
+ * the first path segment (= project slug); only dirty projects appear. `{}`
304
+ * when the store isn't a repo.
305
+ */
306
+ async dirtyCounts() {
307
+ if (!(await this.isRepo()))
308
+ return {};
309
+ try {
310
+ const out = await this.git(this.projectsRoot, [
311
+ "status",
312
+ "--porcelain=v1",
313
+ "-z",
314
+ "--untracked-files=all",
315
+ ]);
316
+ const counts = {};
317
+ for (const f of parsePorcelainZ(out)) {
318
+ const slug = f.path.split("/")[0];
319
+ if (slug)
320
+ counts[slug] = (counts[slug] ?? 0) + 1;
321
+ }
322
+ return counts;
323
+ }
324
+ catch {
325
+ return {};
326
+ }
327
+ }
328
+ /** Push the current branch to origin (sets upstream on first push). */
329
+ async push() {
330
+ if (!(await this.isRepo()))
331
+ return { pushed: false, error: "not a repo" };
332
+ try {
333
+ const branch = (await this.git(this.projectsRoot, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
334
+ await this.git(this.projectsRoot, ["push", "--set-upstream", "origin", branch]);
335
+ return { pushed: true };
336
+ }
337
+ catch (err) {
338
+ return { pushed: false, error: errText(err) };
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * A project-relative path is safe to hand to `git add`/`commit` as a pathspec
344
+ * iff it stays inside the subtree: not absolute, no `..` segment, no NUL. Guards
345
+ * the selective-commit `paths` param against escaping the project (issue #258).
346
+ */
347
+ function isSafeRelPath(p) {
348
+ if (!p || p.startsWith("/") || p.includes("\0"))
349
+ return false;
350
+ return !p.split("/").includes("..");
351
+ }
352
+ /**
353
+ * Parse `git diff --numstat -z` output into a path→stat map. Each record is
354
+ * `added\tremoved\t<path>`; a rename emits `added\tremoved\t` followed by two
355
+ * extra NUL-separated tokens (old, new) — we key on the new path. `-` for
356
+ * added/removed marks a binary change (no line stat).
357
+ */
358
+ function parseNumstatZ(out) {
359
+ const map = new Map();
360
+ const parts = out.split("\0");
361
+ for (let i = 0; i < parts.length; i++) {
362
+ const entry = parts[i];
363
+ if (!entry)
364
+ continue;
365
+ const firstTab = entry.indexOf("\t");
366
+ const secondTab = entry.indexOf("\t", firstTab + 1);
367
+ if (firstTab < 0 || secondTab < 0)
368
+ continue;
369
+ const a = entry.slice(0, firstTab);
370
+ const r = entry.slice(firstTab + 1, secondTab);
371
+ let filePath = entry.slice(secondTab + 1);
372
+ if (filePath === "") {
373
+ // Rename/copy: the next two tokens are the old then new path.
374
+ i++; // old
375
+ filePath = parts[++i] ?? "";
376
+ }
377
+ if (!filePath)
378
+ continue;
379
+ const binary = a === "-" || r === "-";
380
+ map.set(filePath, binary ? { binary: true } : { added: Number(a) || 0, removed: Number(r) || 0 });
381
+ }
382
+ return map;
383
+ }
384
+ /**
385
+ * All-added line stat for an UNTRACKED file (it has no diff): read it (bounded)
386
+ * and count lines, flagging binary on a NUL byte or when it's too large to read.
387
+ * Best-effort — returns `{}` when the file can't be read.
388
+ */
389
+ async function untrackedStat(projectDir, relPath) {
390
+ const abs = path.join(projectDir, relPath);
391
+ try {
392
+ const st = await fs.stat(abs);
393
+ if (!st.isFile())
394
+ return {};
395
+ if (st.size > UNTRACKED_STAT_CAP)
396
+ return { binary: true };
397
+ const buf = await fs.readFile(abs);
398
+ if (buf.includes(0))
399
+ return { binary: true };
400
+ if (buf.length === 0)
401
+ return { added: 0, removed: 0 };
402
+ let added = 0;
403
+ for (const byte of buf)
404
+ if (byte === 10)
405
+ added++;
406
+ if (buf[buf.length - 1] !== 10)
407
+ added++; // last line without a trailing newline
408
+ return { added, removed: 0 };
409
+ }
410
+ catch {
411
+ return {};
412
+ }
413
+ }
414
+ /** Best-effort error text from an execFile rejection (prefers stderr). */
415
+ function errText(err) {
416
+ const e = err;
417
+ return (e.stderr || e.message || String(err)).trim();
418
+ }
419
+ /**
420
+ * Parse `git status --porcelain=v1 -z` output. Records are NUL-separated; a
421
+ * rename/copy record is followed by an extra NUL-separated source path which we
422
+ * consume (and ignore for display — the destination path is what's shown).
423
+ */
424
+ function parsePorcelainZ(out) {
425
+ const parts = out.split("\0");
426
+ const files = [];
427
+ for (let i = 0; i < parts.length; i++) {
428
+ const entry = parts[i];
429
+ if (!entry || entry.length < 4)
430
+ continue;
431
+ const x = entry[0];
432
+ const y = entry[1];
433
+ const path = entry.slice(3);
434
+ // Rename/copy: the next token is the source path — skip it.
435
+ if (x === "R" || x === "C")
436
+ i++;
437
+ const untracked = x === "?" && y === "?";
438
+ files.push({
439
+ path,
440
+ status: `${x}${y}`.trim(),
441
+ staged: !untracked && x !== " ",
442
+ untracked,
443
+ });
444
+ }
445
+ return files;
446
+ }
@@ -0,0 +1,44 @@
1
+ export interface DeviceFlowStart {
2
+ userCode: string;
3
+ verificationUri: string;
4
+ deviceCode: string;
5
+ interval: number;
6
+ expiresIn: number;
7
+ }
8
+ export interface GithubStatus {
9
+ /** A client id is configured (PADDOCK_GITHUB_CLIENT_ID). */
10
+ configured: boolean;
11
+ /** A token is stored (the user has connected). */
12
+ connected: boolean;
13
+ login?: string;
14
+ }
15
+ export type PollResult = {
16
+ status: "authorized" | "pending" | "slow_down" | "error";
17
+ error?: string;
18
+ };
19
+ export declare class GithubAuth {
20
+ private readonly tokenFile;
21
+ private cached;
22
+ /** Trimmed client id, or undefined when unset/blank. */
23
+ private readonly _clientId;
24
+ /**
25
+ * @param tokenFile Path to the on-disk stored-token file (0600, under the data dir).
26
+ * @param clientId The OAuth/App client id, folded into PaddockConfig from
27
+ * `PADDOCK_GITHUB_CLIENT_ID` (issue #269). Trimmed here; a blank value is
28
+ * treated as unset so the feature reports "not configured".
29
+ */
30
+ constructor(tokenFile: string, clientId?: string);
31
+ /** The configured OAuth/App client id, or undefined when unset. */
32
+ clientId(): string | undefined;
33
+ status(): Promise<GithubStatus>;
34
+ /** Begin the device flow — returns the code + URL to show the user. */
35
+ startDeviceFlow(): Promise<DeviceFlowStart>;
36
+ /** Poll the token endpoint; on success, store the token. */
37
+ pollDeviceFlow(deviceCode: string): Promise<PollResult>;
38
+ /** The stored access token (for injecting into GitHub HTTPS pushes). */
39
+ token(): Promise<string | undefined>;
40
+ disconnect(): Promise<void>;
41
+ private fetchLogin;
42
+ private loadToken;
43
+ private saveToken;
44
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * GitHub OAuth device-flow auth for the git backing store (Phase 2).
3
+ *
4
+ * Lets a self-hosted paddock authenticate to GitHub with NO public callback URL:
5
+ * the user approves a short code at github.com/login/device, paddock exchanges
6
+ * it for a scoped token and stores it (0600, under the data dir — never in the
7
+ * repo). Requires a registered OAuth/GitHub App client id in
8
+ * `PADDOCK_GITHUB_CLIENT_ID`; without it the feature reports "not configured".
9
+ *
10
+ * The push target itself (Option B = a bare repo on the NAS over SSH) does not
11
+ * need this — it's for when GitHub is a remote. The stored token is exposed via
12
+ * `token()` so the git layer can inject it for GitHub HTTPS remotes.
13
+ */
14
+ import { promises as fs } from "node:fs";
15
+ const DEVICE_CODE_URL = "https://github.com/login/device/code";
16
+ const TOKEN_URL = "https://github.com/login/oauth/access_token";
17
+ const USER_URL = "https://api.github.com/user";
18
+ const DEFAULT_SCOPE = "repo";
19
+ function field(obj, key) {
20
+ if (obj && typeof obj === "object" && key in obj) {
21
+ const v = obj[key];
22
+ return typeof v === "string" ? v : undefined;
23
+ }
24
+ return undefined;
25
+ }
26
+ function numField(obj, key) {
27
+ if (obj && typeof obj === "object" && key in obj) {
28
+ const v = obj[key];
29
+ return typeof v === "number" ? v : undefined;
30
+ }
31
+ return undefined;
32
+ }
33
+ export class GithubAuth {
34
+ tokenFile;
35
+ cached;
36
+ /** Trimmed client id, or undefined when unset/blank. */
37
+ _clientId;
38
+ /**
39
+ * @param tokenFile Path to the on-disk stored-token file (0600, under the data dir).
40
+ * @param clientId The OAuth/App client id, folded into PaddockConfig from
41
+ * `PADDOCK_GITHUB_CLIENT_ID` (issue #269). Trimmed here; a blank value is
42
+ * treated as unset so the feature reports "not configured".
43
+ */
44
+ constructor(tokenFile, clientId) {
45
+ this.tokenFile = tokenFile;
46
+ this._clientId = clientId && clientId.trim() ? clientId.trim() : undefined;
47
+ }
48
+ /** The configured OAuth/App client id, or undefined when unset. */
49
+ clientId() {
50
+ return this._clientId;
51
+ }
52
+ async status() {
53
+ const tok = await this.loadToken();
54
+ return { configured: !!this.clientId(), connected: !!tok, login: tok?.login };
55
+ }
56
+ /** Begin the device flow — returns the code + URL to show the user. */
57
+ async startDeviceFlow() {
58
+ const client_id = this.clientId();
59
+ if (!client_id)
60
+ throw new Error("GitHub not configured (set PADDOCK_GITHUB_CLIENT_ID)");
61
+ const res = await fetch(DEVICE_CODE_URL, {
62
+ method: "POST",
63
+ headers: { "content-type": "application/json", accept: "application/json" },
64
+ body: JSON.stringify({ client_id, scope: DEFAULT_SCOPE }),
65
+ });
66
+ if (!res.ok)
67
+ throw new Error(`device code request failed (${res.status})`);
68
+ const j = (await res.json());
69
+ const deviceCode = field(j, "device_code");
70
+ const userCode = field(j, "user_code");
71
+ const verificationUri = field(j, "verification_uri");
72
+ if (!deviceCode || !userCode || !verificationUri) {
73
+ throw new Error("malformed device code response");
74
+ }
75
+ return {
76
+ deviceCode,
77
+ userCode,
78
+ verificationUri,
79
+ interval: numField(j, "interval") ?? 5,
80
+ expiresIn: numField(j, "expires_in") ?? 900,
81
+ };
82
+ }
83
+ /** Poll the token endpoint; on success, store the token. */
84
+ async pollDeviceFlow(deviceCode) {
85
+ const client_id = this.clientId();
86
+ if (!client_id)
87
+ return { status: "error", error: "not configured" };
88
+ const res = await fetch(TOKEN_URL, {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json", accept: "application/json" },
91
+ body: JSON.stringify({
92
+ client_id,
93
+ device_code: deviceCode,
94
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
95
+ }),
96
+ });
97
+ // The token endpoint normally returns JSON even for protocol errors
98
+ // (authorization_pending / slow_down / expired_token), but a transient
99
+ // infra failure (gateway 5xx, rate-limit HTML page) can return a non-JSON
100
+ // body. Parse defensively so the poll resolves to a clean error result
101
+ // instead of throwing an unhandled SyntaxError (issue #21) — mirrors the
102
+ // res.ok guard in startDeviceFlow.
103
+ let j;
104
+ try {
105
+ j = await res.json();
106
+ }
107
+ catch {
108
+ return { status: "error", error: `token request failed (${res.status})` };
109
+ }
110
+ const accessToken = field(j, "access_token");
111
+ if (accessToken) {
112
+ const login = await this.fetchLogin(accessToken);
113
+ await this.saveToken({ access_token: accessToken, login, scope: field(j, "scope") });
114
+ return { status: "authorized" };
115
+ }
116
+ const err = field(j, "error");
117
+ if (err === "authorization_pending")
118
+ return { status: "pending" };
119
+ if (err === "slow_down")
120
+ return { status: "slow_down" };
121
+ return { status: "error", error: field(j, "error_description") ?? err ?? "unknown" };
122
+ }
123
+ /** The stored access token (for injecting into GitHub HTTPS pushes). */
124
+ async token() {
125
+ return (await this.loadToken())?.access_token;
126
+ }
127
+ async disconnect() {
128
+ this.cached = null;
129
+ await fs.rm(this.tokenFile, { force: true }).catch(() => undefined);
130
+ }
131
+ async fetchLogin(token) {
132
+ try {
133
+ const r = await fetch(USER_URL, {
134
+ headers: {
135
+ authorization: `Bearer ${token}`,
136
+ accept: "application/vnd.github+json",
137
+ "user-agent": "paddock",
138
+ },
139
+ });
140
+ if (!r.ok)
141
+ return undefined;
142
+ return field(await r.json(), "login");
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ }
148
+ async loadToken() {
149
+ // Only memoize a PRESENT token. Caching the "absent" state too would mean a
150
+ // token file that appears later (e.g. written by another process, or seeded
151
+ // out of band) is never picked up until restart — the connected state would
152
+ // stay stale (issue #25). A present token is immutable here (disconnect()
153
+ // clears the cache), so caching it is safe; re-reading an absent file on each
154
+ // status()/token() call is cheap.
155
+ if (this.cached)
156
+ return this.cached;
157
+ try {
158
+ this.cached = JSON.parse(await fs.readFile(this.tokenFile, "utf8"));
159
+ }
160
+ catch {
161
+ this.cached = null;
162
+ }
163
+ return this.cached;
164
+ }
165
+ async saveToken(t) {
166
+ this.cached = t;
167
+ await fs.writeFile(this.tokenFile, JSON.stringify(t), { mode: 0o600 });
168
+ }
169
+ }