@dsdenes/devboard 0.1.560 → 0.1.562

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 +709 -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
package/bin/devboard.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @dsdenes/devboard launcher.
3
+ * @dsdenes/devboard command line: install, update, and doctor.
4
4
  *
5
- * Bootstrap only: this package resolves and installs the private harness
6
- * package, keeps a host's npm prefix and wrapper copies in sync, and reports
7
- * diagnostics. The harness server, client, prompts, and omp wrappers live in
8
- * the separately released @dsdenes/devboard-harness tarball and never in this
9
- * package.
5
+ * npm is the authority for the version and artifact. The package installed
6
+ * into a host prefix is this same package, so it carries the server, client,
7
+ * built bundle, prompts, wrappers, and templates. When npm cannot resolve or
8
+ * serve the artifact and a Gitea release address is configured, the release
9
+ * tarball and its `.sha256` are used instead. The CLI never reads another
10
+ * checkout, another local installation, or a running dashboard belonging to
11
+ * a different host.
10
12
  */
11
13
 
12
14
  import { spawn, spawnSync } from "node:child_process";
@@ -16,6 +18,7 @@ import {
16
18
  createWriteStream,
17
19
  existsSync,
18
20
  lstatSync,
21
+ mkdirSync,
19
22
  openSync,
20
23
  readFileSync,
21
24
  readdirSync,
@@ -38,10 +41,13 @@ import { pipeline } from "node:stream/promises";
38
41
  import { basename, dirname, join, resolve, sep } from "node:path";
39
42
  import { fileURLToPath } from "node:url";
40
43
 
41
- const HARNESS_PACKAGE = "@dsdenes/devboard-harness";
42
- const HARNESS_SCOPE = "@dsdenes";
44
+ const PACKAGE_NAME = "@dsdenes/devboard";
45
+ const PACKAGE_SCOPE = "@dsdenes";
46
+ const PACKAGE_DIR = "devboard";
47
+ /** The package directory the old two-package layout used; migration removes it. */
48
+ const LEGACY_DIR = "devboard-harness";
43
49
  const RELEASE_REPO = process.env.DEVBOARD_RELEASE_REPO ?? "dsdenes/devboard";
44
- const RELEASE_PREFIX = "devboard-harness-";
50
+ const RELEASE_PREFIX = "devboard-";
45
51
  const UPDATE_INTERVAL_MS = 60_000;
46
52
  const DEFAULT_PORT = 8080;
47
53
  const START_TIMEOUT_MS = 30_000;
@@ -49,8 +55,8 @@ const START_TIMEOUT_MS = 30_000;
49
55
  const usage = `Usage: devboard <command> [options]
50
56
 
51
57
  Commands:
52
- install Install or refresh the harness for a host and start its dashboard.
53
- update Install the newest harness release without touching the dashboard.
58
+ install Install or refresh Devboard for a host and start its dashboard.
59
+ update Install the newest npm version without touching the dashboard.
54
60
  doctor Check one host installation and print one PASS or FAIL line per check.
55
61
 
56
62
  Options:
@@ -286,6 +292,20 @@ async function waitForHttp(port, timeoutMs = START_TIMEOUT_MS) {
286
292
  return false;
287
293
  }
288
294
 
295
+ function npmBinary() {
296
+ return process.env.DEVBOARD_NPM ?? "npm";
297
+ }
298
+
299
+ function npmCache(prefix) {
300
+ return join(prefix, ".tmp", "npm-cache");
301
+ }
302
+
303
+ function npmEnv(prefix) {
304
+ const cache = npmCache(prefix);
305
+ mkdirSync(cache, { recursive: true });
306
+ return { npm_config_cache: cache };
307
+ }
308
+
289
309
  function configuredReleaseUrl() {
290
310
  for (const name of ["DEVBOARD_RELEASE_URL", "DEVBOARD_GITEA_URL", "GITEA_URL"]) {
291
311
  const value = process.env[name];
@@ -296,17 +316,27 @@ function configuredReleaseUrl() {
296
316
  return undefined;
297
317
  }
298
318
 
299
- function releaseSourceFromEnv(prefix) {
300
- if (process.env.DEVBOARD_RELEASE_DIR !== undefined && process.env.DEVBOARD_RELEASE_DIR.length > 0) {
301
- return { kind: "dir", directory: resolve(process.env.DEVBOARD_RELEASE_DIR) };
302
- }
303
- const persisted = prefix === undefined ? {} : readInstallState(prefix).release ?? {};
304
- const baseUrl = configuredReleaseUrl() ?? persisted.url ?? "";
305
- const repo = process.env.DEVBOARD_RELEASE_REPO ?? persisted.repo ?? RELEASE_REPO;
319
+ /**
320
+ * The optional release fallback. npm is tried first; this source is used only
321
+ * when npm cannot resolve or serve the artifact. Without one of these
322
+ * variables there is no fallback and npm failure is final.
323
+ */
324
+ function releaseSourceFromEnv() {
325
+ const directory = process.env.DEVBOARD_RELEASE_DIR;
326
+ if (typeof directory === "string" && directory.length > 0) {
327
+ return { kind: "dir", directory: resolve(directory) };
328
+ }
329
+ const baseUrl = configuredReleaseUrl();
330
+ if (baseUrl === undefined) return undefined;
331
+ const repo = process.env.DEVBOARD_RELEASE_REPO ?? RELEASE_REPO;
306
332
  const token = process.env.GITEA_TOKEN ?? process.env.DEVBOARD_GITEA_TOKEN;
307
333
  return { kind: "gitea", baseUrl, repo, ...(token === undefined ? {} : { token }) };
308
334
  }
309
335
 
336
+ function describeSource(source) {
337
+ return source.kind === "dir" ? source.directory : `${source.baseUrl}/${source.repo}`;
338
+ }
339
+
310
340
  function tarballName(version) {
311
341
  return `${RELEASE_PREFIX}${version}.tgz`;
312
342
  }
@@ -332,9 +362,71 @@ function versionFromTarball(name) {
332
362
  return match?.[1];
333
363
  }
334
364
 
365
+ function versionFromTag(tag) {
366
+ return tag.replace(/^v/, "");
367
+ }
368
+
369
+ async function npmLatest(prefix) {
370
+ const result = run(
371
+ npmBinary(),
372
+ ["view", PACKAGE_NAME, "version", "--prefer-online"],
373
+ { env: npmEnv(prefix), timeoutMs: 120_000 },
374
+ );
375
+ if (result.code !== 0) {
376
+ throw new LauncherError(
377
+ `npm could not resolve ${PACKAGE_NAME}: ${firstLine(result.stderr || result.stdout)}`,
378
+ );
379
+ }
380
+ const lines = result.stdout.trim().split("\n").filter((line) => line.trim().length > 0);
381
+ const version = lines.at(-1)?.trim() ?? "";
382
+ if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
383
+ throw new LauncherError(`npm returned an unreadable version: ${firstLine(result.stdout)}`);
384
+ }
385
+ return version;
386
+ }
387
+
388
+ async function npmArtifact(prefix, version) {
389
+ const result = run(
390
+ npmBinary(),
391
+ ["view", `${PACKAGE_NAME}@${version}`, "dist.tarball", "--prefer-online"],
392
+ { env: npmEnv(prefix), timeoutMs: 120_000 },
393
+ );
394
+ if (result.code !== 0) {
395
+ throw new LauncherError(
396
+ `npm could not serve ${PACKAGE_NAME}@${version}: ${firstLine(result.stderr || result.stdout)}`,
397
+ );
398
+ }
399
+ const value = result.stdout.trim().split("\n").filter((line) => line.length > 0).at(-1) ?? "";
400
+ if (!/^https?:\/\//.test(value)) {
401
+ throw new LauncherError(`npm returned no tarball for ${PACKAGE_NAME}@${version}`);
402
+ }
403
+ return value;
404
+ }
405
+
335
406
  async function latestRelease(source) {
336
- if (source.kind === "dir") return directoryRelease(source.directory);
337
- return giteaRelease(source);
407
+ return source.kind === "dir" ? directoryRelease(source.directory) : giteaLatest(source);
408
+ }
409
+
410
+ async function releaseForVersion(source, version) {
411
+ if (source.kind === "dir") {
412
+ const tarball = join(source.directory, tarballName(version));
413
+ const checksum = join(source.directory, checksumName(version));
414
+ if (!existsSync(tarball) || !existsSync(checksum)) {
415
+ throw new LauncherError(
416
+ `the release directory has no ${tarballName(version)} and checksum for ${version}`,
417
+ );
418
+ }
419
+ return {
420
+ version,
421
+ tag: `v${version}`,
422
+ assets: [{ name: tarballName(version) }, { name: checksumName(version) }],
423
+ };
424
+ }
425
+ const release = await giteaReleaseByTag(source, version);
426
+ if (release === undefined) {
427
+ throw new LauncherError(`the release feed has no ${PACKAGE_NAME}@${version}`);
428
+ }
429
+ return release;
338
430
  }
339
431
 
340
432
  async function directoryRelease(directory) {
@@ -376,7 +468,7 @@ function apiHeaders(source, accept) {
376
468
  };
377
469
  }
378
470
 
379
- async function giteaRelease(source) {
471
+ async function giteaLatest(source) {
380
472
  if (typeof source.baseUrl !== "string" || source.baseUrl.length === 0) {
381
473
  throw new LauncherError("the Gitea release address is not configured; set DEVBOARD_GITEA_URL");
382
474
  }
@@ -386,8 +478,24 @@ async function giteaRelease(source) {
386
478
  const detail = firstLine(await response.text().catch(() => ""));
387
479
  throw new LauncherError(`the release feed answered HTTP ${response.status}: ${detail}`);
388
480
  }
389
- const parsed = await response.json();
390
- if (typeof parsed.tag_name !== "string") throw new LauncherError("the latest release has no tag");
481
+ return releaseFromDocument(await response.json());
482
+ }
483
+
484
+ async function giteaReleaseByTag(source, version) {
485
+ const url =
486
+ `${source.baseUrl}/api/v1/repos/${apiPath(source.repo, "/releases/tags")}/` +
487
+ encodeURIComponent(`v${version}`);
488
+ const response = await fetch(url, { headers: apiHeaders(source, "application/json") });
489
+ if (response.status === 404) return undefined;
490
+ if (!response.ok) {
491
+ const detail = firstLine(await response.text().catch(() => ""));
492
+ throw new LauncherError(`the release feed answered HTTP ${response.status}: ${detail}`);
493
+ }
494
+ return releaseFromDocument(await response.json());
495
+ }
496
+
497
+ function releaseFromDocument(parsed) {
498
+ if (typeof parsed.tag_name !== "string") throw new LauncherError("the release has no tag");
391
499
  const assets = Array.isArray(parsed.assets)
392
500
  ? parsed.assets
393
501
  .filter((asset) => asset !== null && typeof asset === "object" && typeof asset.name === "string")
@@ -398,7 +506,7 @@ async function giteaRelease(source) {
398
506
  browser_download_url: asset.browser_download_url,
399
507
  }))
400
508
  : [];
401
- return { version: parsed.tag_name.replace(/^v/, ""), tag: parsed.tag_name, assets };
509
+ return { version: versionFromTag(parsed.tag_name), tag: parsed.tag_name, assets };
402
510
  }
403
511
 
404
512
  async function downloadRelease(source, release, destination) {
@@ -439,7 +547,6 @@ async function downloadRelease(source, release, destination) {
439
547
  throw new LauncherError(`downloading ${name} failed (HTTP ${response.status}): ${detail}`);
440
548
  }
441
549
  if (response.body === null) throw new LauncherError(`downloading ${name} returned no body`);
442
- await mkdir(destination, { recursive: true });
443
550
  try {
444
551
  await pipeline(
445
552
  Readable.fromWeb(response.body),
@@ -472,13 +579,27 @@ function verifyChecksum(tarball, checksumFile) {
472
579
  }
473
580
  }
474
581
 
475
- function harnessDir(prefix) {
476
- return join(prefix, "node_modules", HARNESS_SCOPE, "devboard-harness");
582
+ function packageDir(prefix) {
583
+ return join(prefix, "node_modules", PACKAGE_SCOPE, PACKAGE_DIR);
584
+ }
585
+
586
+ function legacyPackageDir(prefix) {
587
+ return join(prefix, "node_modules", PACKAGE_SCOPE, LEGACY_DIR);
477
588
  }
478
589
 
479
590
  function installedVersion(prefix) {
480
591
  try {
481
- const parsed = JSON.parse(readFileSync(join(harnessDir(prefix), "package.json"), "utf8"));
592
+ const parsed = JSON.parse(readFileSync(join(packageDir(prefix), "package.json"), "utf8"));
593
+ return typeof parsed.version === "string" ? parsed.version : null;
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+
599
+ /** The version the old two-package layout left in the prefix, if any. */
600
+ function legacyInstalledVersion(prefix) {
601
+ try {
602
+ const parsed = JSON.parse(readFileSync(join(legacyPackageDir(prefix), "package.json"), "utf8"));
482
603
  return typeof parsed.version === "string" ? parsed.version : null;
483
604
  } catch {
484
605
  return null;
@@ -498,7 +619,8 @@ async function writePrefixManifest(prefix, version) {
498
619
  existing.dependencies !== null && typeof existing.dependencies === "object"
499
620
  ? { ...existing.dependencies }
500
621
  : {};
501
- dependencies[HARNESS_PACKAGE] = version;
622
+ delete dependencies[`${PACKAGE_SCOPE}/${LEGACY_DIR}`];
623
+ dependencies[PACKAGE_NAME] = version;
502
624
  await writeFile(
503
625
  path,
504
626
  `${JSON.stringify(
@@ -514,55 +636,80 @@ async function writePrefixManifest(prefix, version) {
514
636
  );
515
637
  }
516
638
 
517
- async function installTarball(prefix, tarball, version) {
639
+ /**
640
+ * Replaces the prefix's whole dependency tree with the staged one. The package
641
+ * and its runtime dependencies move together, so the installed server never
642
+ * depends on modules that were discarded with the staging directory.
643
+ */
644
+ async function swapNodeModules(prefix, stagedNodeModules) {
645
+ const target = join(prefix, "node_modules");
646
+ const backup = join(prefix, ".tmp", "updates", `previous-${Date.now()}-${randomUUID()}`);
647
+ let moved = false;
648
+ if (existsSync(target)) {
649
+ await rename(target, backup);
650
+ moved = true;
651
+ }
652
+ try {
653
+ await rename(stagedNodeModules, target);
654
+ } catch (error) {
655
+ if (moved) await rename(backup, target).catch(() => undefined);
656
+ throw error;
657
+ }
658
+ await rm(backup, { recursive: true, force: true });
659
+ }
660
+
661
+ async function stagedInstall(prefix, spec, version) {
518
662
  const updates = join(prefix, ".tmp", "updates");
519
663
  await mkdir(updates, { recursive: true });
520
664
  const staging = join(updates, `stage-${version}-${randomUUID()}`);
521
665
  await mkdir(staging, { recursive: true });
522
666
  try {
523
667
  mustRun(
524
- "npm",
525
- ["install", "--prefix", staging, "--ignore-scripts", "--no-audit", "--no-fund",
526
- "--no-save", tarball],
668
+ npmBinary(),
669
+ [
670
+ "install",
671
+ "--prefix",
672
+ staging,
673
+ "--ignore-scripts",
674
+ "--no-audit",
675
+ "--no-fund",
676
+ "--no-save",
677
+ spec,
678
+ ],
527
679
  "npm install",
528
- { cwd: staging, timeoutMs: 10 * 60_000 },
680
+ { cwd: staging, env: npmEnv(prefix), timeoutMs: 10 * 60_000 },
529
681
  );
530
- const staged = harnessDir(staging);
531
- const stagedVersion = installedVersion(staging);
682
+ const staged = packageDir(staging);
683
+ let stagedVersion = null;
684
+ try {
685
+ stagedVersion = JSON.parse(readFileSync(join(staged, "package.json"), "utf8")).version;
686
+ } catch {
687
+ stagedVersion = null;
688
+ }
532
689
  if (stagedVersion !== version) {
533
690
  throw new LauncherError(
534
691
  `npm installed ${stagedVersion ?? "no version"}, expected ${version}`,
535
692
  );
536
693
  }
537
- const parent = join(prefix, "node_modules", HARNESS_SCOPE);
538
- await mkdir(parent, { recursive: true });
539
- const target = join(parent, "devboard-harness");
540
- const backup = join(updates, `previous-${Date.now()}-${randomUUID()}`);
541
- let moved = false;
542
- if (existsSync(target)) {
543
- await rename(target, backup);
544
- moved = true;
545
- }
546
- try {
547
- await rename(staged, target);
548
- } catch (error) {
549
- if (moved) await rename(backup, target).catch(() => undefined);
550
- throw error;
551
- }
552
- await rm(backup, { recursive: true, force: true });
553
- return target;
694
+ await swapNodeModules(prefix, join(staging, "node_modules"));
695
+ await writePrefixManifest(prefix, version);
696
+ return packageDir(prefix);
554
697
  } finally {
555
698
  await rm(staging, { recursive: true, force: true });
556
699
  }
557
700
  }
558
701
 
559
- async function installRelease(source, release, prefix, stateDir) {
702
+ async function installFromNpm(prefix, version) {
703
+ return stagedInstall(prefix, `${PACKAGE_NAME}@${version}`, version);
704
+ }
705
+
706
+ async function installFromRelease(source, release, prefix, stateDir) {
560
707
  const downloadDir = join(stateDir, "updates", `download-${randomUUID()}`);
561
708
  try {
562
709
  const files = await downloadRelease(source, release, downloadDir);
563
710
  verifyChecksum(files.tarball, files.checksum);
564
- const target = await installTarball(prefix, files.tarball, release.version);
565
- await writePrefixManifest(prefix, release.version);
711
+ const target = await stagedInstall(prefix, files.tarball, release.version);
712
+ console.log(`devboard: used release tarball ${files.tarball} from ${describeSource(source)}`);
566
713
  return target;
567
714
  } finally {
568
715
  await rm(downloadDir, { recursive: true, force: true });
@@ -621,18 +768,7 @@ async function replacePathWithDirectory(path) {
621
768
  }
622
769
 
623
770
  async function ensureFixedDirectory(path) {
624
- const info = lstatOrUndefined(path);
625
- if (info === undefined) {
626
- await mkdir(path, { recursive: true });
627
- return;
628
- }
629
- if (info.isDirectory()) return;
630
- if (info.isSymbolicLink()) {
631
- await unlink(path);
632
- await mkdir(path, { recursive: true });
633
- return;
634
- }
635
- throw new LauncherError(`${path} exists and is not a directory`);
771
+ await replacePathWithDirectory(path);
636
772
  }
637
773
 
638
774
  function trackedPaths(host, paths) {
@@ -783,16 +919,6 @@ async function writeInstallState(prefix, state) {
783
919
  await writeFile(installStatePath(prefix), `${JSON.stringify(state, null, 2)}\n`);
784
920
  }
785
921
 
786
- /** Persist the release coordinates so the dashboard feed needs no environment. */
787
- async function persistRelease(prefix, source) {
788
- if (source.kind !== "gitea") return;
789
- const state = readInstallState(prefix);
790
- await writeInstallState(prefix, {
791
- ...state,
792
- release: { url: source.baseUrl, repo: source.repo },
793
- });
794
- }
795
-
796
922
  function isDashboardFrom(pid, harness) {
797
923
  const entry = processInfo(pid);
798
924
  if (entry === undefined) return false;
@@ -817,7 +943,7 @@ function canonicalSource(host) {
817
943
  if (info === undefined || info.isSymbolicLink()) return false;
818
944
  try {
819
945
  const parsed = JSON.parse(readFileSync(join(prefix, "package.json"), "utf8"));
820
- if (parsed.name !== HARNESS_PACKAGE) return false;
946
+ if (parsed.name !== PACKAGE_NAME) return false;
821
947
  } catch {
822
948
  return false;
823
949
  }
@@ -893,16 +1019,44 @@ async function ensureOneDashboard(host, harness, version, requestedPort) {
893
1019
  return { port, pid, reused: false };
894
1020
  }
895
1021
 
1022
+ /**
1023
+ * Installs one version. npm is tried first; a configured release source is
1024
+ * used only after npm resolution or installation fails.
1025
+ */
1026
+ async function installResolved(prefix, version, fallback, stateDir, npmFailure) {
1027
+ if (npmFailure === undefined) {
1028
+ try {
1029
+ await installFromNpm(prefix, version);
1030
+ return { version, via: "npm" };
1031
+ } catch (error) {
1032
+ if (fallback === undefined) throw error;
1033
+ console.log(`devboard: npm could not install ${version}: ${firstLine(error.message)}`);
1034
+ }
1035
+ } else {
1036
+ if (fallback === undefined) {
1037
+ throw new LauncherError(
1038
+ `${npmFailure.message}; set DEVBOARD_GITEA_URL to enable the release-tarball fallback`,
1039
+ );
1040
+ }
1041
+ console.log(`devboard: npm could not resolve a version: ${firstLine(npmFailure.message)}`);
1042
+ }
1043
+ const release =
1044
+ version === undefined
1045
+ ? await latestRelease(fallback)
1046
+ : await releaseForVersion(fallback, version);
1047
+ await installFromRelease(fallback, release, prefix, stateDir);
1048
+ return { version: release.version, via: "release" };
1049
+ }
1050
+
896
1051
  async function installCommand(options) {
897
1052
  const host = resolveHost(options.host);
898
1053
  if (canonicalSource(host)) {
899
1054
  throw new LauncherError(
900
- `${host} is the canonical harness source checkout; install runs in a host project instead`,
1055
+ `${host} is the canonical devboard source checkout; install runs in a host project instead`,
901
1056
  );
902
1057
  }
903
1058
  const prefix = join(host, ".devboard");
904
- const source = releaseSourceFromEnv(prefix);
905
- const release = await latestRelease(source);
1059
+ const fallback = releaseSourceFromEnv();
906
1060
  const skip = trackedPaths(host, [".omp/commands", ".omp/extensions", "AGENTS.md"]);
907
1061
  const trackedHarness = [...trackedPaths(host, [".devboard"])].filter(
908
1062
  (path) => path === prefix || path.startsWith(`${prefix}${sep}`),
@@ -918,7 +1072,34 @@ async function installCommand(options) {
918
1072
  await replacePathWithDirectory(prefix);
919
1073
  if (captured.length > 0) await restoreTemporaryState(prefix, captured);
920
1074
  await ensureFixedDirectory(join(prefix, ".tmp"));
921
- const harness = await installRelease(source, release, prefix, join(prefix, ".tmp"));
1075
+
1076
+ let version;
1077
+ let npmFailure;
1078
+ try {
1079
+ version = await npmLatest(prefix);
1080
+ } catch (error) {
1081
+ npmFailure = error;
1082
+ version = undefined;
1083
+ if (fallback === undefined) {
1084
+ throw new LauncherError(
1085
+ `${error.message}; set DEVBOARD_GITEA_URL to enable the release-tarball fallback`,
1086
+ );
1087
+ }
1088
+ }
1089
+ const resolved = await installResolved(
1090
+ prefix,
1091
+ version,
1092
+ fallback,
1093
+ join(prefix, ".tmp"),
1094
+ npmFailure,
1095
+ );
1096
+ const installed = installedVersion(prefix);
1097
+ if (installed !== resolved.version) {
1098
+ throw new LauncherError(
1099
+ `installed ${installed ?? "no version"}, expected ${resolved.version}`,
1100
+ );
1101
+ }
1102
+ const harness = packageDir(prefix);
922
1103
  await syncWrappers(host, harness, skip);
923
1104
  const agents = await syncAgents(host, harness, skip);
924
1105
  const ignored = await ensureGitignore(host);
@@ -926,15 +1107,16 @@ async function installCommand(options) {
926
1107
  const dashboard = await ensureOneDashboard(
927
1108
  host,
928
1109
  harness,
929
- release.version,
1110
+ resolved.version,
930
1111
  Number.isInteger(portOption) ? portOption : undefined,
931
1112
  );
932
- await persistRelease(prefix, source);
933
- console.log(`devboard: installed ${HARNESS_PACKAGE}@${release.version} in ${prefix}`);
1113
+ console.log(
1114
+ `devboard: installed ${PACKAGE_NAME}@${resolved.version} in ${prefix} (${resolved.via})`,
1115
+ );
934
1116
  if (agents === "drift") {
935
- console.log("devboard: AGENTS.md differs from the harness template; left unchanged (drift)");
1117
+ console.log("devboard: AGENTS.md differs from the template; left unchanged (drift)");
936
1118
  } else if (agents === "installed") {
937
- console.log("devboard: installed AGENTS.md from the harness template");
1119
+ console.log("devboard: installed AGENTS.md from the package template");
938
1120
  } else if (agents === "tracked-missing") {
939
1121
  console.log("devboard: AGENTS.md is tracked and deleted; left absent (drift)");
940
1122
  }
@@ -951,22 +1133,57 @@ async function updateCommand(options) {
951
1133
  if (lstatOrUndefined(prefix)?.isSymbolicLink() ?? false) {
952
1134
  throw new LauncherError(`${prefix} is a symlink; run install first`);
953
1135
  }
954
- const before = installedVersion(prefix);
1136
+ const current = installedVersion(prefix);
1137
+ const legacy = current === null ? legacyInstalledVersion(prefix) : null;
1138
+ const before = current ?? legacy;
955
1139
  if (before === null) {
956
- throw new LauncherError(`no harness is installed in ${prefix}; run install`);
1140
+ throw new LauncherError(`no devboard is installed in ${prefix}; run install`);
957
1141
  }
958
- const source = releaseSourceFromEnv(prefix);
959
- const release = await latestRelease(source);
960
- if (!newerVersion(release.version, before) && release.version !== before) {
961
- // An older latest release is still installed as asked, never downgraded.
962
- console.log(`devboard: installed ${before}; latest release is ${release.version}`);
963
- return;
1142
+ if (legacy !== null) {
1143
+ console.log(`devboard: migrating the legacy ${legacy} install to ${PACKAGE_NAME}`);
964
1144
  }
965
- const harness = await installRelease(source, release, prefix, join(prefix, ".tmp"));
966
- const skip = trackedPaths(host, [".omp/commands", ".omp/extensions"]);
967
- await syncWrappers(host, harness, skip);
968
- await persistRelease(prefix, source);
969
- console.log(`devboard: harness ${before} -> ${release.version}`);
1145
+ const fallback = releaseSourceFromEnv();
1146
+ let latest;
1147
+ let npmFailure;
1148
+ try {
1149
+ latest = await npmLatest(prefix);
1150
+ } catch (error) {
1151
+ npmFailure = error;
1152
+ if (fallback === undefined) {
1153
+ throw new LauncherError(
1154
+ `${error.message}; set DEVBOARD_GITEA_URL to enable the release-tarball fallback`,
1155
+ );
1156
+ }
1157
+ }
1158
+ const targetVersion = latest ?? (await latestRelease(fallback)).version;
1159
+ if (legacy === null) {
1160
+ if (!newerVersion(targetVersion, before) && targetVersion !== before) {
1161
+ // An older latest release is still installed as asked, never downgraded.
1162
+ console.log(`devboard: installed ${before}; newest version is ${targetVersion}`);
1163
+ return;
1164
+ }
1165
+ if (targetVersion === before) {
1166
+ console.log(`devboard: ${PACKAGE_NAME}@${before} is already installed`);
1167
+ return;
1168
+ }
1169
+ }
1170
+ const resolved = await installResolved(
1171
+ prefix,
1172
+ latest,
1173
+ fallback,
1174
+ join(prefix, ".tmp"),
1175
+ npmFailure,
1176
+ );
1177
+ const harness = packageDir(prefix);
1178
+ if (legacy === null) {
1179
+ const skip = trackedPaths(host, [".omp/commands", ".omp/extensions"]);
1180
+ await syncWrappers(host, harness, skip);
1181
+ } else {
1182
+ // A legacy host keeps its wrapper copies and AGENTS.md; migration only
1183
+ // replaces the package and leaves local content byte-for-byte intact.
1184
+ console.log("devboard: kept the existing omp wrappers and AGENTS.md");
1185
+ }
1186
+ console.log(`devboard: ${PACKAGE_NAME} ${before} -> ${resolved.version} (${resolved.via})`);
970
1187
  console.log("devboard: the dashboard keeps running; reload it to restart on the new version");
971
1188
  }
972
1189
 
@@ -1046,6 +1263,17 @@ function lastUpdateFailure(prefix, host) {
1046
1263
  return typeof last.text === "string" ? last.text : "update failed";
1047
1264
  }
1048
1265
 
1266
+ async function latestForDoctor(prefix) {
1267
+ try {
1268
+ return { version: await npmLatest(prefix), via: "npm" };
1269
+ } catch (error) {
1270
+ const fallback = releaseSourceFromEnv();
1271
+ if (fallback === undefined) throw error;
1272
+ const release = await latestRelease(fallback);
1273
+ return { version: release.version, via: describeSource(fallback) };
1274
+ }
1275
+ }
1276
+
1049
1277
  async function readReleaseDigest(source, release, prefix) {
1050
1278
  await mkdir(join(prefix, ".tmp"), { recursive: true });
1051
1279
  const scratch = join(prefix, ".tmp", `release-check-${randomUUID()}`);
@@ -1093,28 +1321,34 @@ const CHECKS = [
1093
1321
  {
1094
1322
  name: "release access",
1095
1323
  run: async (_host, prefix) => {
1096
- const source = releaseSourceFromEnv(prefix);
1097
- const release = await latestRelease(source);
1098
- return `the public feed serves ${release.tag}`;
1324
+ const latest = await latestForDoctor(prefix);
1325
+ return `${latest.via} serves ${PACKAGE_NAME}@${latest.version}`;
1099
1326
  },
1100
1327
  },
1101
1328
  {
1102
1329
  name: "release",
1103
1330
  run: async (_host, prefix) => {
1104
- const source = releaseSourceFromEnv(prefix);
1105
- const release = await latestRelease(source);
1106
- await readReleaseDigest(source, release, prefix);
1107
- return `${release.tag} with a matching digest`;
1331
+ try {
1332
+ const version = await npmLatest(prefix);
1333
+ const tarball = await npmArtifact(prefix, version);
1334
+ return `${PACKAGE_NAME}@${version} is served by npm (${tarball})`;
1335
+ } catch (error) {
1336
+ const fallback = releaseSourceFromEnv();
1337
+ if (fallback === undefined) throw error;
1338
+ const release = await latestRelease(fallback);
1339
+ await readReleaseDigest(fallback, release, prefix);
1340
+ return `${release.tag} with a matching digest`;
1341
+ }
1108
1342
  },
1109
1343
  },
1110
1344
  {
1111
- name: "harness install",
1345
+ name: "package install",
1112
1346
  run: async (_host, prefix) => {
1113
1347
  const version = installedVersion(prefix);
1114
1348
  if (version === null) {
1115
- throw new LauncherError(`${harnessDir(prefix)} is missing; run install`);
1349
+ throw new LauncherError(`${packageDir(prefix)} is missing; run install`);
1116
1350
  }
1117
- return `${HARNESS_PACKAGE}@${version}`;
1351
+ return `${PACKAGE_NAME}@${version}`;
1118
1352
  },
1119
1353
  },
1120
1354
  {
@@ -1122,11 +1356,11 @@ const CHECKS = [
1122
1356
  run: async (_host, prefix) => {
1123
1357
  const version = installedVersion(prefix);
1124
1358
  if (version === null) throw new LauncherError("no installed version");
1125
- const release = await latestRelease(releaseSourceFromEnv(prefix));
1126
- if (version !== release.version) {
1127
- throw new LauncherError(`stale install: installed ${version}, latest ${release.version}`);
1359
+ const latest = await latestForDoctor(prefix);
1360
+ if (version !== latest.version) {
1361
+ throw new LauncherError(`stale install: installed ${version}, latest ${latest.version}`);
1128
1362
  }
1129
- return `${version} matches the latest release`;
1363
+ return `${version} matches ${latest.via}`;
1130
1364
  },
1131
1365
  },
1132
1366
  {
@@ -1143,7 +1377,7 @@ const CHECKS = [
1143
1377
  {
1144
1378
  name: "wrappers",
1145
1379
  run: async (host, prefix) => {
1146
- const { failures, drift } = wrapperMismatches(host, harnessDir(prefix));
1380
+ const { failures, drift } = wrapperMismatches(host, packageDir(prefix));
1147
1381
  if (failures.length > 0) {
1148
1382
  throw new LauncherError(`does not match the installed package: ${failures.join(", ")}`);
1149
1383
  }
@@ -1160,7 +1394,7 @@ const CHECKS = [
1160
1394
  const info = lstatOrUndefined(target);
1161
1395
  if (info === undefined) throw new LauncherError("AGENTS.md is missing; run install");
1162
1396
  if (info.isSymbolicLink()) throw new LauncherError("AGENTS.md is a symlink; run install");
1163
- const template = join(harnessDir(prefix), "templates", "host-AGENTS.md");
1397
+ const template = join(packageDir(prefix), "templates", "host-AGENTS.md");
1164
1398
  if (!readFileSync(target).equals(readFileSync(template))) {
1165
1399
  return "drift: AGENTS.md differs from the template (left unchanged)";
1166
1400
  }
@@ -1176,7 +1410,7 @@ const CHECKS = [
1176
1410
  throw new LauncherError(`${dashboards.length} dashboards run for this host`);
1177
1411
  }
1178
1412
  const [dashboard] = dashboards;
1179
- const harness = harnessDir(join(host, ".devboard"));
1413
+ const harness = packageDir(join(host, ".devboard"));
1180
1414
  if (!isDashboardFrom(dashboard.pid, harness)) {
1181
1415
  throw new LauncherError(`pid ${dashboard.pid} does not run the installed package`);
1182
1416
  }