@dsdenes/devboard 0.1.560 → 0.1.563

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 (323) hide show
  1. package/AGENTS.md +59 -0
  2. package/README.md +2497 -26
  3. package/bin/devboard.mjs +352 -118
  4. package/client/App.svelte +523 -0
  5. package/client/FeedbackOverlay.svelte +1125 -0
  6. package/client/FlowMode.svelte +1015 -0
  7. package/client/app.css +3922 -0
  8. package/client/env.d.ts +23 -0
  9. package/client/lib/commands.ts +836 -0
  10. package/client/lib/components/ActivityBar.svelte +295 -0
  11. package/client/lib/components/AdrTable.svelte +241 -0
  12. package/client/lib/components/CommandPalette.svelte +198 -0
  13. package/client/lib/components/ConceptKind.svelte +66 -0
  14. package/client/lib/components/ConfirmDialog.svelte +62 -0
  15. package/client/lib/components/DataTable.svelte +243 -0
  16. package/client/lib/components/DetailSplit.svelte +68 -0
  17. package/client/lib/components/DiscoverOverview.svelte +898 -0
  18. package/client/lib/components/DiscoverWorkspace.svelte +437 -0
  19. package/client/lib/components/DraftsPage.svelte +186 -0
  20. package/client/lib/components/GoalWorkspace.svelte +1042 -0
  21. package/client/lib/components/IssuesAgentFocus.svelte +117 -0
  22. package/client/lib/components/IssuesAgentLog.svelte +50 -0
  23. package/client/lib/components/IssuesQuestion.svelte +95 -0
  24. package/client/lib/components/IssuesWorkspace.svelte +577 -0
  25. package/client/lib/components/MermaidDiagram.svelte +89 -0
  26. package/client/lib/components/MonacoEditor.svelte +190 -0
  27. package/client/lib/components/NotificationRegion.svelte +195 -0
  28. package/client/lib/components/PaneHandle.svelte +25 -0
  29. package/client/lib/components/ScoutIssues.svelte +215 -0
  30. package/client/lib/components/ScoutWorkspace.svelte +180 -0
  31. package/client/lib/components/Sidebar.svelte +622 -0
  32. package/client/lib/components/StatusBar.svelte +118 -0
  33. package/client/lib/components/Terminal.svelte +193 -0
  34. package/client/lib/components/WatchdogWorkspace.svelte +537 -0
  35. package/client/lib/components/ui/button.svelte +56 -0
  36. package/client/lib/components/ui/icon-button.svelte +56 -0
  37. package/client/lib/components/ui/input.svelte +19 -0
  38. package/client/lib/components/ui/page-header.svelte +120 -0
  39. package/client/lib/components/ui/stage-track.svelte +141 -0
  40. package/client/lib/components/watchdog-indicator.svelte +189 -0
  41. package/client/lib/connection.svelte.ts +210 -0
  42. package/client/lib/dependency-diagram.test.ts +90 -0
  43. package/client/lib/dependency-diagram.ts +75 -0
  44. package/client/lib/discover-format.test.ts +265 -0
  45. package/client/lib/discover-format.ts +394 -0
  46. package/client/lib/discover.svelte.ts +412 -0
  47. package/client/lib/draft-format.test.ts +48 -0
  48. package/client/lib/draft-format.ts +13 -0
  49. package/client/lib/flow-save.test.ts +148 -0
  50. package/client/lib/flow.svelte.ts +445 -0
  51. package/client/lib/goal-reconnect.test.ts +282 -0
  52. package/client/lib/goal.svelte.ts +652 -0
  53. package/client/lib/issues.svelte.ts +439 -0
  54. package/client/lib/layout.svelte.ts +234 -0
  55. package/client/lib/notification-issue.test.ts +53 -0
  56. package/client/lib/notification-issue.ts +27 -0
  57. package/client/lib/notifications.svelte.ts +62 -0
  58. package/client/lib/poll-status.test.ts +141 -0
  59. package/client/lib/poll-status.ts +88 -0
  60. package/client/lib/record-status.test.ts +19 -0
  61. package/client/lib/record-status.ts +25 -0
  62. package/client/lib/scout.svelte.ts +164 -0
  63. package/client/lib/skill-document.test.ts +77 -0
  64. package/client/lib/skill-document.ts +57 -0
  65. package/client/lib/status-tone.test.ts +40 -0
  66. package/client/lib/status-tone.ts +33 -0
  67. package/client/lib/types.ts +578 -0
  68. package/client/lib/utils.ts +6 -0
  69. package/client/lib/viewport.svelte.ts +20 -0
  70. package/client/lib/watchdog.svelte.ts +69 -0
  71. package/client/main.ts +7 -0
  72. package/dist/apple-touch-icon.png +0 -0
  73. package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
  74. package/dist/assets/arc-BZgCSX2H.js +1 -0
  75. package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
  76. package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
  77. package/dist/assets/array-BifhSqXX.js +1 -0
  78. package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
  79. package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
  80. package/dist/assets/channel-CgM4lcUW.js +1 -0
  81. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
  83. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  84. package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
  85. package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
  86. package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
  87. package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
  88. package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
  89. package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
  90. package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
  91. package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  92. package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
  93. package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
  94. package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
  95. package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
  96. package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
  97. package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
  98. package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
  99. package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
  100. package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
  101. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  102. package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
  103. package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
  104. package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
  105. package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
  106. package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
  107. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  108. package/dist/assets/dagre-D7JM2Yhv.js +1 -0
  109. package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
  110. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  111. package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
  112. package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
  113. package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
  114. package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
  115. package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
  116. package/dist/assets/dist-Dce-2YbA.js +1 -0
  117. package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
  118. package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
  119. package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
  120. package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
  121. package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
  122. package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
  123. package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
  124. package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
  125. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  126. package/dist/assets/index-ByQ-XX38.css +1 -0
  127. package/dist/assets/index-CIf5TpfY.js +214 -0
  128. package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
  129. package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
  130. package/dist/assets/init-C-OQMol4.js +1 -0
  131. package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
  132. package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
  133. package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
  134. package/dist/assets/katex-DJhrJSbw.js +257 -0
  135. package/dist/assets/line-N9XiCREw.js +1 -0
  136. package/dist/assets/linear-C9hnqmJv.js +1 -0
  137. package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
  138. package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
  139. package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
  140. package/dist/assets/monaco-DjpsE8to.css +1 -0
  141. package/dist/assets/monaco-DqzJaNIe.js +908 -0
  142. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  143. package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
  144. package/dist/assets/path-fybaL0A-.js +1 -0
  145. package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
  146. package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
  147. package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
  148. package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
  149. package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
  150. package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
  151. package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
  152. package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
  153. package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
  154. package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
  155. package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
  156. package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  157. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  158. package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
  159. package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
  160. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  161. package/dist/assets/src-CGp3bfio.js +1 -0
  162. package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
  163. package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
  164. package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
  165. package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
  166. package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
  167. package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
  168. package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
  169. package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
  170. package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
  171. package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
  172. package/dist/assets/xterm-BrP-ENHg.css +1 -0
  173. package/dist/assets/xterm-C8zLTNGk.js +16 -0
  174. package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
  175. package/dist/favicon.ico +0 -0
  176. package/dist/favicon.svg +6 -0
  177. package/dist/index.html +34 -0
  178. package/extensions/implement-goal-issue.ts +368 -0
  179. package/extensions/implement-next-epic.ts +835 -0
  180. package/index.html +28 -0
  181. package/install.sh +111 -0
  182. package/issues/control.ts +91 -0
  183. package/issues/guards.ts +12 -0
  184. package/issues/ledger.ts +122 -0
  185. package/issues/legacy.ts +68 -0
  186. package/issues/lock.ts +305 -0
  187. package/issues/paths.ts +76 -0
  188. package/issues/preflight.ts +189 -0
  189. package/issues/repository.ts +203 -0
  190. package/issues/scout/actions.ts +255 -0
  191. package/issues/scout/control.ts +80 -0
  192. package/issues/scout/github.ts +358 -0
  193. package/issues/scout/paths.ts +129 -0
  194. package/issues/scout/preflight.ts +277 -0
  195. package/issues/scout/recommendation.ts +400 -0
  196. package/issues/scout/scan.ts +143 -0
  197. package/issues/scout/state.ts +448 -0
  198. package/issues/scout/worker.ts +1282 -0
  199. package/issues/state.ts +584 -0
  200. package/issues/tracker.ts +271 -0
  201. package/issues/worker.ts +1065 -0
  202. package/lib/dependencies.ts +61 -0
  203. package/lib/forge-auth.ts +953 -0
  204. package/lib/forge.ts +141 -0
  205. package/lib/gitea.ts +729 -0
  206. package/lib/github-release.ts +318 -0
  207. package/lib/inventory.test.ts +79 -0
  208. package/lib/inventory.ts +53 -0
  209. package/lib/release.test.ts +269 -0
  210. package/lib/release.ts +801 -0
  211. package/omp/commands/adr-resolve.md +8 -0
  212. package/omp/commands/discover.md +6 -0
  213. package/omp/commands/epic-next.md +8 -0
  214. package/omp/commands/flow-change.md +8 -0
  215. package/omp/commands/flow-goal.md +8 -0
  216. package/omp/commands/flow-implement.md +8 -0
  217. package/omp/commands/flow-widget.md +8 -0
  218. package/omp/commands/goal-draft.md +8 -0
  219. package/omp/commands/goal-outcome.md +8 -0
  220. package/omp/commands/implement-issue.md +8 -0
  221. package/omp/commands/prepare-skills.md +6 -0
  222. package/omp/commands/scout-recommendations.md +8 -0
  223. package/omp/commands/setup-devboard.md +24 -0
  224. package/omp/config.yml +12 -0
  225. package/omp/extensions/implement-goal-issue.ts +2 -0
  226. package/omp/extensions/implement-next-epic.ts +2 -0
  227. package/omp/mcp.json +9 -0
  228. package/package.json +86 -7
  229. package/prompts/adr-resolve.md +130 -0
  230. package/prompts/discover.md +541 -0
  231. package/prompts/epic-next.md +533 -0
  232. package/prompts/flow-change.md +76 -0
  233. package/prompts/flow-goal.md +126 -0
  234. package/prompts/flow-implement.md +103 -0
  235. package/prompts/flow-widget.md +54 -0
  236. package/prompts/goal-draft.md +147 -0
  237. package/prompts/goal-outcome.md +87 -0
  238. package/prompts/implement-issue.md +66 -0
  239. package/prompts/prepare-skills.md +142 -0
  240. package/prompts/scout-recommendations.md +143 -0
  241. package/public/apple-touch-icon.png +0 -0
  242. package/public/favicon.ico +0 -0
  243. package/public/favicon.svg +6 -0
  244. package/server/acp.ts +540 -0
  245. package/server/activity.test.ts +96 -0
  246. package/server/activity.ts +48 -0
  247. package/server/app.test.ts +882 -0
  248. package/server/app.ts +6179 -0
  249. package/server/ask.test.ts +543 -0
  250. package/server/ask.ts +589 -0
  251. package/server/dependencies.test.ts +118 -0
  252. package/server/discover-remote.test.ts +1602 -0
  253. package/server/discover-sheet.test.ts +176 -0
  254. package/server/discover.test.ts +1510 -0
  255. package/server/discover.ts +3198 -0
  256. package/server/feedback.test.ts +458 -0
  257. package/server/feedback.ts +369 -0
  258. package/server/fixtures/adr-source.ts +71 -0
  259. package/server/fixtures/hang-download.ts +14 -0
  260. package/server/fixtures/stub-agent.ts +1870 -0
  261. package/server/fixtures/stub-forge.ts +141 -0
  262. package/server/fixtures/stub-gh.ts +57 -0
  263. package/server/flow-pr.test.ts +976 -0
  264. package/server/flow.test.ts +1632 -0
  265. package/server/flow.ts +2655 -0
  266. package/server/forge-pr.ts +387 -0
  267. package/server/goal-creation.test.ts +957 -0
  268. package/server/goal-draft.test.ts +348 -0
  269. package/server/goal-drafts.ts +449 -0
  270. package/server/goal-proposal.test.ts +479 -0
  271. package/server/goal-proposal.ts +232 -0
  272. package/server/goal-revision.test.ts +613 -0
  273. package/server/goal-slug.test.ts +56 -0
  274. package/server/goal-slug.ts +96 -0
  275. package/server/goal.ts +20 -0
  276. package/server/index.ts +147 -0
  277. package/server/issue-close.test.ts +100 -0
  278. package/server/issue-close.ts +178 -0
  279. package/server/issues.test.ts +167 -0
  280. package/server/issues.ts +479 -0
  281. package/server/journal.test.ts +350 -0
  282. package/server/journal.ts +329 -0
  283. package/server/lifecycle-pause.test.ts +1795 -0
  284. package/server/lifecycle.test.ts +1879 -0
  285. package/server/parallel-lifecycle.test.ts +1029 -0
  286. package/server/persistence-restart.test.ts +723 -0
  287. package/server/persistence.test.ts +1935 -0
  288. package/server/records.test.ts +180 -0
  289. package/server/records.ts +99 -0
  290. package/server/reset-implementation.test.ts +997 -0
  291. package/server/run.test.ts +611 -0
  292. package/server/scout.test.ts +877 -0
  293. package/server/scout.ts +459 -0
  294. package/server/skills-preparation.test.ts +1073 -0
  295. package/server/state.ts +249 -0
  296. package/server/tracker.ts +369 -0
  297. package/server/updates.test.ts +444 -0
  298. package/server/updates.ts +198 -0
  299. package/server/version.test.ts +140 -0
  300. package/server/version.ts +151 -0
  301. package/server/watchdog.ts +389 -0
  302. package/svelte.config.js +5 -0
  303. package/templates/host-AGENTS.md +235 -0
  304. package/tsconfig.json +21 -0
  305. package/vite.config.ts +21 -0
  306. package/watchdog/cleanup-main.ts +98 -0
  307. package/watchdog/cleanup.ts +301 -0
  308. package/watchdog/collect.ts +311 -0
  309. package/watchdog/coordinator.ts +397 -0
  310. package/watchdog/delivery.ts +107 -0
  311. package/watchdog/discovery.ts +209 -0
  312. package/watchdog/findings.ts +211 -0
  313. package/watchdog/github.ts +189 -0
  314. package/watchdog/history.ts +138 -0
  315. package/watchdog/install.ts +103 -0
  316. package/watchdog/ledger.ts +188 -0
  317. package/watchdog/lock.ts +63 -0
  318. package/watchdog/main.ts +106 -0
  319. package/watchdog/status.ts +71 -0
  320. package/watchdog/systemd.ts +96 -0
  321. package/watchdog/uninstall.ts +30 -0
  322. package/watchdog/units/devboard-watchdog.service +9 -0
  323. package/watchdog/units/devboard-watchdog.timer +10 -0
@@ -0,0 +1,387 @@
1
+ import {
2
+ authEnvironment,
3
+ isAuthFailure,
4
+ resolveForgeAuth,
5
+ type AuthOutcome,
6
+ type Exec,
7
+ type Forge,
8
+ type ResolveOptions,
9
+ type ResolvedAuth,
10
+ } from "../lib/forge-auth.ts";
11
+ import {
12
+ DRAFT_PREFIX,
13
+ draftTitle,
14
+ giteaCreatePull,
15
+ giteaEditPull,
16
+ giteaMergePull,
17
+ giteaPull,
18
+ readyTitle,
19
+ type FetchLike,
20
+ type GiteaTarget,
21
+ } from "../lib/gitea";
22
+
23
+ /**
24
+ * The Flow session's pull-request lifecycle against the host's forge.
25
+ *
26
+ * The adapter owns nothing but command shapes: it trades a session branch for a
27
+ * draft PR/MR, turns it live, merges it with a merge commit, and reports its
28
+ * state. Every credential stays in the child environment; no token is ever
29
+ * placed in argv or in an error message. Nothing here creates an issue, an
30
+ * epic, or a tracker query.
31
+ */
32
+
33
+ export interface ForgeTarget {
34
+ forge: Forge;
35
+ host: string;
36
+ project: string;
37
+ /** Gitea HTTP origin; resolved from configuration or an HTTP origin. */
38
+ baseUrl?: string;
39
+ }
40
+
41
+ export interface CommandResult {
42
+ stdout: string;
43
+ stderr: string;
44
+ code: number;
45
+ }
46
+
47
+ /** One PR/MR as the forge reports it. */
48
+ export interface ForgePrState {
49
+ number: number;
50
+ url: string;
51
+ state: "open" | "merged" | "closed";
52
+ draft: boolean;
53
+ }
54
+
55
+ export interface ForgePrInput {
56
+ cwd: string;
57
+ target: ForgeTarget;
58
+ head: string;
59
+ base: string;
60
+ title: string;
61
+ body: string;
62
+ }
63
+
64
+ export interface FlowPrAdapter {
65
+ createDraft(input: ForgePrInput): Promise<ForgePrState>;
66
+ update(input: ForgePrInput & { number: number }): Promise<void>;
67
+ markReady(cwd: string, target: ForgeTarget, number: number): Promise<void>;
68
+ merge(cwd: string, target: ForgeTarget, number: number): Promise<void>;
69
+ view(cwd: string, target: ForgeTarget, number: number): Promise<ForgePrState>;
70
+ deleteRemoteBranch(cwd: string, branch: string): Promise<void>;
71
+ }
72
+
73
+ /** True when a forge command failed; the message keeps the forge's stderr. */
74
+ export function forgeFailure(operation: string, result: CommandResult): Error {
75
+ const detail = result.stderr.trim() || result.stdout.trim();
76
+ return new Error(
77
+ `Could not ${operation}: ${detail.length === 0 ? `exit code ${result.code}` : detail}`,
78
+ );
79
+ }
80
+
81
+ function numberFromUrl(url: string): number | undefined {
82
+ const match = /\/(?:pulls?|merge_requests)\/(\d+)\/?$/.exec(url.trim());
83
+ const value = match?.[1];
84
+ if (value === undefined) return undefined;
85
+ const number = Number(value);
86
+ return Number.isInteger(number) ? number : undefined;
87
+ }
88
+
89
+ function firstUrl(text: string): string | undefined {
90
+ const match = /https?:\/\/\S+/.exec(text);
91
+ return match?.[0];
92
+ }
93
+
94
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
95
+ return typeof value === "object" && value !== null
96
+ ? (value as Record<string, unknown>)
97
+ : undefined;
98
+ }
99
+
100
+ /** Normalizes one forge's state spelling to the three states the view knows. */
101
+ function normalizedState(value: unknown): ForgePrState["state"] | undefined {
102
+ if (typeof value !== "string") return undefined;
103
+ switch (value.trim().toLowerCase()) {
104
+ case "open":
105
+ case "opened":
106
+ return "open";
107
+ case "merged":
108
+ return "merged";
109
+ case "closed":
110
+ return "closed";
111
+ default:
112
+ return undefined;
113
+ }
114
+ }
115
+
116
+ const GIT_ENV = {
117
+ GIT_TERMINAL_PROMPT: "0",
118
+ GIT_ASKPASS: "true",
119
+ NO_COLOR: "1",
120
+ GH_PROMPT_DISABLED: "1",
121
+ GH_NO_UPDATE_NOTIFIER: "1",
122
+ } as const;
123
+
124
+ /**
125
+ * Runs the host's `gh`/`glab` after resolving a write-capable account for the
126
+ * exact repository. The resolved token lives only in the child environment.
127
+ */
128
+ export class CommandFlowForge implements FlowPrAdapter {
129
+ readonly #exec: Exec;
130
+ readonly #env: Record<string, string>;
131
+ readonly #resolver: (exec: Exec, options: ResolveOptions) => Promise<AuthOutcome>;
132
+ readonly #auth = new Map<string, Promise<ResolvedAuth>>();
133
+ readonly #giteaFetch: FetchLike | undefined;
134
+
135
+ /**
136
+ * `resolve` is injectable so a test can prove the command shapes without a
137
+ * real credential store. Production always uses `resolveForgeAuth`.
138
+ */
139
+ constructor(
140
+ exec: Exec,
141
+ env: Record<string, string> = {},
142
+ resolve: (exec: Exec, options: ResolveOptions) => Promise<AuthOutcome> = resolveForgeAuth,
143
+ giteaFetch?: FetchLike,
144
+ ) {
145
+ this.#exec = exec;
146
+ this.#env = env;
147
+ this.#resolver = resolve;
148
+ this.#giteaFetch = giteaFetch;
149
+ }
150
+
151
+ async #resolve(target: ForgeTarget, cwd: string): Promise<ResolvedAuth> {
152
+ const key = `${target.forge}:${target.host}:${target.project}`;
153
+ const cached = this.#auth.get(key);
154
+ if (cached !== undefined) return await cached;
155
+ const pending = (async () => {
156
+ const outcome = await this.#resolver(this.#exec, {
157
+ forge: target.forge,
158
+ host: target.host,
159
+ project: target.project,
160
+ cwd,
161
+ required: "write",
162
+ env: { ...process.env, ...this.#env },
163
+ ...(target.baseUrl === undefined ? {} : { baseUrl: target.baseUrl }),
164
+ ...(this.#giteaFetch === undefined ? {} : { giteaFetch: this.#giteaFetch }),
165
+ });
166
+ if (isAuthFailure(outcome)) throw new Error(outcome.error);
167
+ return outcome;
168
+ })();
169
+ this.#auth.set(key, pending);
170
+ try {
171
+ return await pending;
172
+ } catch (error) {
173
+ this.#auth.delete(key);
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ async #run(
179
+ cwd: string,
180
+ target: ForgeTarget,
181
+ command: string,
182
+ args: string[],
183
+ ): Promise<CommandResult> {
184
+ const auth = await this.#resolve(target, cwd);
185
+ let result: CommandResult;
186
+ try {
187
+ result = await this.#exec(command, args, {
188
+ cwd,
189
+ env: { ...authEnvironment(auth), ...GIT_ENV },
190
+ });
191
+ } catch (error) {
192
+ result = {
193
+ stdout: "",
194
+ stderr: error instanceof Error ? error.message : String(error),
195
+ code: -1,
196
+ };
197
+ }
198
+ return result;
199
+ }
200
+
201
+ async #gitea(target: ForgeTarget, cwd: string): Promise<GiteaTarget> {
202
+ const auth = await this.#resolve(target, cwd);
203
+ if (auth.baseUrl === undefined || auth.baseUrl.length === 0) {
204
+ throw new Error("Creating a Gitea pull request needs DEVBOARD_GITEA_URL.");
205
+ }
206
+ return {
207
+ baseUrl: auth.baseUrl,
208
+ project: target.project,
209
+ ...(auth.token === undefined ? {} : { token: auth.token }),
210
+ };
211
+ }
212
+
213
+ async createDraft(input: ForgePrInput): Promise<ForgePrState> {
214
+ if (input.target.forge === "gitea") {
215
+ const pull = await giteaCreatePull(
216
+ await this.#gitea(input.target, input.cwd),
217
+ { head: input.head, base: input.base, title: input.title, body: input.body },
218
+ this.#giteaFetch,
219
+ );
220
+ return { number: pull.number, url: pull.url, state: "open", draft: true };
221
+ }
222
+ const args =
223
+ input.target.forge === "github"
224
+ ? [
225
+ "pr",
226
+ "create",
227
+ "--draft",
228
+ "--base",
229
+ input.base,
230
+ "--head",
231
+ input.head,
232
+ "--title",
233
+ input.title,
234
+ "--body",
235
+ input.body,
236
+ ]
237
+ : [
238
+ "mr",
239
+ "create",
240
+ "--draft",
241
+ "--source-branch",
242
+ input.head,
243
+ "--target-branch",
244
+ input.base,
245
+ "--title",
246
+ input.title,
247
+ "--description",
248
+ input.body,
249
+ "--yes",
250
+ ];
251
+ const result = await this.#run(
252
+ input.cwd,
253
+ input.target,
254
+ input.target.forge === "github" ? "gh" : "glab",
255
+ args,
256
+ );
257
+ if (result.code !== 0) throw forgeFailure("create the draft pull request", result);
258
+
259
+ const url = firstUrl(result.stdout) ?? firstUrl(result.stderr);
260
+ const number = url === undefined ? undefined : numberFromUrl(url);
261
+ if (url === undefined || number === undefined) {
262
+ throw new Error("Could not create the draft pull request: the forge returned no URL.");
263
+ }
264
+ return { number, url, state: "open", draft: true };
265
+ }
266
+
267
+ async update(input: ForgePrInput & { number: number }): Promise<void> {
268
+ if (input.target.forge === "gitea") {
269
+ await giteaEditPull(
270
+ await this.#gitea(input.target, input.cwd),
271
+ input.number,
272
+ { title: draftTitle(input.title), body: input.body },
273
+ this.#giteaFetch,
274
+ );
275
+ return;
276
+ }
277
+ const args =
278
+ input.target.forge === "github"
279
+ ? ["pr", "edit", String(input.number), "--title", input.title, "--body", input.body]
280
+ : [
281
+ "mr",
282
+ "update",
283
+ String(input.number),
284
+ "--title",
285
+ input.title,
286
+ "--description",
287
+ input.body,
288
+ ];
289
+ const result = await this.#run(
290
+ input.cwd,
291
+ input.target,
292
+ input.target.forge === "github" ? "gh" : "glab",
293
+ args,
294
+ );
295
+ if (result.code !== 0) throw forgeFailure("update the pull request", result);
296
+ }
297
+
298
+ async markReady(cwd: string, target: ForgeTarget, number: number): Promise<void> {
299
+ if (target.forge === "gitea") {
300
+ const gitea = await this.#gitea(target, cwd);
301
+ const current = await giteaPull(gitea, number, this.#giteaFetch);
302
+ await giteaEditPull(
303
+ gitea,
304
+ number,
305
+ { title: current.title.startsWith(DRAFT_PREFIX) ? readyTitle(current.title) : current.title },
306
+ this.#giteaFetch,
307
+ );
308
+ return;
309
+ }
310
+ const args =
311
+ target.forge === "github"
312
+ ? ["pr", "ready", String(number)]
313
+ : ["mr", "update", String(number), "--ready"];
314
+ const result = await this.#run(cwd, target, target.forge === "github" ? "gh" : "glab", args);
315
+ if (result.code !== 0) throw forgeFailure("mark the pull request ready", result);
316
+ }
317
+
318
+ async merge(cwd: string, target: ForgeTarget, number: number): Promise<void> {
319
+ if (target.forge === "gitea") {
320
+ await giteaMergePull(await this.#gitea(target, cwd), number, this.#giteaFetch);
321
+ return;
322
+ }
323
+ // A merge commit is the only accepted method: never squash, never rebase.
324
+ const args =
325
+ target.forge === "github"
326
+ ? ["pr", "merge", String(number), "--merge"]
327
+ : ["mr", "merge", String(number), "--merge", "--yes"];
328
+ const result = await this.#run(cwd, target, target.forge === "github" ? "gh" : "glab", args);
329
+ if (result.code !== 0) throw forgeFailure("merge the pull request", result);
330
+ }
331
+
332
+ async view(cwd: string, target: ForgeTarget, number: number): Promise<ForgePrState> {
333
+ if (target.forge === "gitea") {
334
+ const pull = await giteaPull(await this.#gitea(target, cwd), number, this.#giteaFetch);
335
+ return { number: pull.number, url: pull.url, state: pull.state, draft: pull.draft };
336
+ }
337
+ const args =
338
+ target.forge === "github"
339
+ ? ["pr", "view", String(number), "--json", "number,url,state,isDraft"]
340
+ : ["mr", "view", String(number), "--output", "json"];
341
+ const result = await this.#run(cwd, target, target.forge === "github" ? "gh" : "glab", args);
342
+ if (result.code !== 0) throw forgeFailure("read the pull request", result);
343
+
344
+ let parsed: Record<string, unknown> | undefined;
345
+ try {
346
+ parsed = asRecord(JSON.parse(result.stdout));
347
+ } catch {
348
+ parsed = undefined;
349
+ }
350
+ if (parsed === undefined) {
351
+ throw new Error("Could not read the pull request: unexpected forge output.");
352
+ }
353
+
354
+ const state = normalizedState(parsed.state);
355
+ const url =
356
+ typeof parsed.url === "string"
357
+ ? parsed.url
358
+ : typeof parsed.web_url === "string"
359
+ ? parsed.web_url
360
+ : undefined;
361
+ if (state === undefined || url === undefined) {
362
+ throw new Error("Could not read the pull request: unexpected forge output.");
363
+ }
364
+ const draft =
365
+ parsed.isDraft === true ||
366
+ parsed.draft === true ||
367
+ (typeof parsed.draft === "boolean" ? parsed.draft : false);
368
+ return { number, url, state, draft };
369
+ }
370
+
371
+ async deleteRemoteBranch(cwd: string, branch: string): Promise<void> {
372
+ let result: CommandResult;
373
+ try {
374
+ result = await this.#exec("git", ["push", "origin", "--delete", branch], {
375
+ cwd,
376
+ env: { ...process.env, ...GIT_ENV } as Record<string, string>,
377
+ });
378
+ } catch (error) {
379
+ result = {
380
+ stdout: "",
381
+ stderr: error instanceof Error ? error.message : String(error),
382
+ code: -1,
383
+ };
384
+ }
385
+ if (result.code !== 0) throw forgeFailure(`delete the remote branch ${branch}`, result);
386
+ }
387
+ }