@dsdenes/devboard 0.1.559 → 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.
- package/AGENTS.md +59 -0
- package/README.md +2497 -26
- package/bin/devboard.mjs +352 -118
- package/client/App.svelte +523 -0
- package/client/FeedbackOverlay.svelte +1125 -0
- package/client/FlowMode.svelte +1015 -0
- package/client/app.css +3922 -0
- package/client/env.d.ts +23 -0
- package/client/lib/commands.ts +836 -0
- package/client/lib/components/ActivityBar.svelte +295 -0
- package/client/lib/components/AdrTable.svelte +241 -0
- package/client/lib/components/CommandPalette.svelte +198 -0
- package/client/lib/components/ConceptKind.svelte +66 -0
- package/client/lib/components/ConfirmDialog.svelte +62 -0
- package/client/lib/components/DataTable.svelte +243 -0
- package/client/lib/components/DetailSplit.svelte +68 -0
- package/client/lib/components/DiscoverOverview.svelte +898 -0
- package/client/lib/components/DiscoverWorkspace.svelte +437 -0
- package/client/lib/components/DraftsPage.svelte +186 -0
- package/client/lib/components/GoalWorkspace.svelte +1042 -0
- package/client/lib/components/IssuesAgentFocus.svelte +117 -0
- package/client/lib/components/IssuesAgentLog.svelte +50 -0
- package/client/lib/components/IssuesQuestion.svelte +95 -0
- package/client/lib/components/IssuesWorkspace.svelte +577 -0
- package/client/lib/components/MermaidDiagram.svelte +89 -0
- package/client/lib/components/MonacoEditor.svelte +190 -0
- package/client/lib/components/NotificationRegion.svelte +195 -0
- package/client/lib/components/PaneHandle.svelte +25 -0
- package/client/lib/components/ScoutIssues.svelte +215 -0
- package/client/lib/components/ScoutWorkspace.svelte +180 -0
- package/client/lib/components/Sidebar.svelte +622 -0
- package/client/lib/components/StatusBar.svelte +118 -0
- package/client/lib/components/Terminal.svelte +193 -0
- package/client/lib/components/WatchdogWorkspace.svelte +537 -0
- package/client/lib/components/ui/button.svelte +56 -0
- package/client/lib/components/ui/icon-button.svelte +56 -0
- package/client/lib/components/ui/input.svelte +19 -0
- package/client/lib/components/ui/page-header.svelte +120 -0
- package/client/lib/components/ui/stage-track.svelte +141 -0
- package/client/lib/components/watchdog-indicator.svelte +189 -0
- package/client/lib/connection.svelte.ts +210 -0
- package/client/lib/dependency-diagram.test.ts +90 -0
- package/client/lib/dependency-diagram.ts +75 -0
- package/client/lib/discover-format.test.ts +265 -0
- package/client/lib/discover-format.ts +394 -0
- package/client/lib/discover.svelte.ts +412 -0
- package/client/lib/draft-format.test.ts +48 -0
- package/client/lib/draft-format.ts +13 -0
- package/client/lib/flow-save.test.ts +148 -0
- package/client/lib/flow.svelte.ts +445 -0
- package/client/lib/goal-reconnect.test.ts +282 -0
- package/client/lib/goal.svelte.ts +652 -0
- package/client/lib/issues.svelte.ts +439 -0
- package/client/lib/layout.svelte.ts +234 -0
- package/client/lib/notification-issue.test.ts +53 -0
- package/client/lib/notification-issue.ts +27 -0
- package/client/lib/notifications.svelte.ts +62 -0
- package/client/lib/poll-status.test.ts +141 -0
- package/client/lib/poll-status.ts +88 -0
- package/client/lib/record-status.test.ts +19 -0
- package/client/lib/record-status.ts +25 -0
- package/client/lib/scout.svelte.ts +164 -0
- package/client/lib/skill-document.test.ts +77 -0
- package/client/lib/skill-document.ts +57 -0
- package/client/lib/status-tone.test.ts +40 -0
- package/client/lib/status-tone.ts +33 -0
- package/client/lib/types.ts +578 -0
- package/client/lib/utils.ts +6 -0
- package/client/lib/viewport.svelte.ts +20 -0
- package/client/lib/watchdog.svelte.ts +69 -0
- package/client/main.ts +7 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
- package/dist/assets/arc-BZgCSX2H.js +1 -0
- package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
- package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
- package/dist/assets/channel-CgM4lcUW.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
- package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
- package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
- package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
- package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
- package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
- package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
- package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
- package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
- package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
- package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
- package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-D7JM2Yhv.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
- package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
- package/dist/assets/dist-Dce-2YbA.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
- package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
- package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
- package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
- package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-ByQ-XX38.css +1 -0
- package/dist/assets/index-CIf5TpfY.js +214 -0
- package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
- package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
- package/dist/assets/katex-DJhrJSbw.js +257 -0
- package/dist/assets/line-N9XiCREw.js +1 -0
- package/dist/assets/linear-C9hnqmJv.js +1 -0
- package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
- package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
- package/dist/assets/monaco-DjpsE8to.css +1 -0
- package/dist/assets/monaco-DqzJaNIe.js +908 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
- package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
- package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
- package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
- package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-CGp3bfio.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
- package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
- package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
- package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/assets/xterm-C8zLTNGk.js +16 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +6 -0
- package/dist/index.html +34 -0
- package/extensions/implement-goal-issue.ts +368 -0
- package/extensions/implement-next-epic.ts +835 -0
- package/index.html +28 -0
- package/install.sh +111 -0
- package/issues/control.ts +91 -0
- package/issues/guards.ts +12 -0
- package/issues/ledger.ts +122 -0
- package/issues/legacy.ts +68 -0
- package/issues/lock.ts +305 -0
- package/issues/paths.ts +76 -0
- package/issues/preflight.ts +189 -0
- package/issues/repository.ts +203 -0
- package/issues/scout/actions.ts +255 -0
- package/issues/scout/control.ts +80 -0
- package/issues/scout/github.ts +358 -0
- package/issues/scout/paths.ts +129 -0
- package/issues/scout/preflight.ts +277 -0
- package/issues/scout/recommendation.ts +400 -0
- package/issues/scout/scan.ts +143 -0
- package/issues/scout/state.ts +448 -0
- package/issues/scout/worker.ts +1282 -0
- package/issues/state.ts +584 -0
- package/issues/tracker.ts +271 -0
- package/issues/worker.ts +1065 -0
- package/lib/dependencies.ts +61 -0
- package/lib/forge-auth.ts +953 -0
- package/lib/forge.ts +141 -0
- package/lib/gitea.ts +709 -0
- package/lib/github-release.ts +318 -0
- package/lib/inventory.test.ts +79 -0
- package/lib/inventory.ts +53 -0
- package/lib/release.test.ts +269 -0
- package/lib/release.ts +801 -0
- package/omp/commands/adr-resolve.md +8 -0
- package/omp/commands/discover.md +6 -0
- package/omp/commands/epic-next.md +8 -0
- package/omp/commands/flow-change.md +8 -0
- package/omp/commands/flow-goal.md +8 -0
- package/omp/commands/flow-implement.md +8 -0
- package/omp/commands/flow-widget.md +8 -0
- package/omp/commands/goal-draft.md +8 -0
- package/omp/commands/goal-outcome.md +8 -0
- package/omp/commands/implement-issue.md +8 -0
- package/omp/commands/prepare-skills.md +6 -0
- package/omp/commands/scout-recommendations.md +8 -0
- package/omp/commands/setup-devboard.md +24 -0
- package/omp/config.yml +12 -0
- package/omp/extensions/implement-goal-issue.ts +2 -0
- package/omp/extensions/implement-next-epic.ts +2 -0
- package/omp/mcp.json +9 -0
- package/package.json +86 -7
- package/prompts/adr-resolve.md +130 -0
- package/prompts/discover.md +541 -0
- package/prompts/epic-next.md +533 -0
- package/prompts/flow-change.md +76 -0
- package/prompts/flow-goal.md +126 -0
- package/prompts/flow-implement.md +103 -0
- package/prompts/flow-widget.md +54 -0
- package/prompts/goal-draft.md +147 -0
- package/prompts/goal-outcome.md +87 -0
- package/prompts/implement-issue.md +66 -0
- package/prompts/prepare-skills.md +142 -0
- package/prompts/scout-recommendations.md +143 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/server/acp.ts +540 -0
- package/server/activity.test.ts +96 -0
- package/server/activity.ts +48 -0
- package/server/app.test.ts +882 -0
- package/server/app.ts +6179 -0
- package/server/ask.test.ts +543 -0
- package/server/ask.ts +589 -0
- package/server/dependencies.test.ts +118 -0
- package/server/discover-remote.test.ts +1602 -0
- package/server/discover-sheet.test.ts +176 -0
- package/server/discover.test.ts +1510 -0
- package/server/discover.ts +3198 -0
- package/server/feedback.test.ts +458 -0
- package/server/feedback.ts +369 -0
- package/server/fixtures/adr-source.ts +71 -0
- package/server/fixtures/hang-download.ts +14 -0
- package/server/fixtures/stub-agent.ts +1870 -0
- package/server/fixtures/stub-forge.ts +141 -0
- package/server/fixtures/stub-gh.ts +57 -0
- package/server/flow-pr.test.ts +976 -0
- package/server/flow.test.ts +1632 -0
- package/server/flow.ts +2655 -0
- package/server/forge-pr.ts +387 -0
- package/server/goal-creation.test.ts +957 -0
- package/server/goal-draft.test.ts +348 -0
- package/server/goal-drafts.ts +449 -0
- package/server/goal-proposal.test.ts +479 -0
- package/server/goal-proposal.ts +232 -0
- package/server/goal-revision.test.ts +613 -0
- package/server/goal-slug.test.ts +56 -0
- package/server/goal-slug.ts +96 -0
- package/server/goal.ts +20 -0
- package/server/index.ts +147 -0
- package/server/issue-close.test.ts +100 -0
- package/server/issue-close.ts +178 -0
- package/server/issues.test.ts +167 -0
- package/server/issues.ts +479 -0
- package/server/journal.test.ts +350 -0
- package/server/journal.ts +329 -0
- package/server/lifecycle-pause.test.ts +1795 -0
- package/server/lifecycle.test.ts +1879 -0
- package/server/parallel-lifecycle.test.ts +1029 -0
- package/server/persistence-restart.test.ts +723 -0
- package/server/persistence.test.ts +1935 -0
- package/server/records.test.ts +180 -0
- package/server/records.ts +99 -0
- package/server/reset-implementation.test.ts +997 -0
- package/server/run.test.ts +611 -0
- package/server/scout.test.ts +877 -0
- package/server/scout.ts +459 -0
- package/server/skills-preparation.test.ts +1073 -0
- package/server/state.ts +249 -0
- package/server/tracker.ts +369 -0
- package/server/updates.test.ts +444 -0
- package/server/updates.ts +198 -0
- package/server/version.test.ts +140 -0
- package/server/version.ts +151 -0
- package/server/watchdog.ts +389 -0
- package/svelte.config.js +5 -0
- package/templates/host-AGENTS.md +235 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +21 -0
- package/watchdog/cleanup-main.ts +98 -0
- package/watchdog/cleanup.ts +301 -0
- package/watchdog/collect.ts +311 -0
- package/watchdog/coordinator.ts +397 -0
- package/watchdog/delivery.ts +107 -0
- package/watchdog/discovery.ts +209 -0
- package/watchdog/findings.ts +211 -0
- package/watchdog/github.ts +189 -0
- package/watchdog/history.ts +138 -0
- package/watchdog/install.ts +103 -0
- package/watchdog/ledger.ts +188 -0
- package/watchdog/lock.ts +63 -0
- package/watchdog/main.ts +106 -0
- package/watchdog/status.ts +71 -0
- package/watchdog/systemd.ts +96 -0
- package/watchdog/uninstall.ts +30 -0
- package/watchdog/units/devboard-watchdog.service +9 -0
- 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
|
|
3
|
+
* @dsdenes/devboard command line: install, update, and doctor.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
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
|
|
42
|
-
const
|
|
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-
|
|
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
|
|
53
|
-
update Install the newest
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
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
|
-
|
|
337
|
-
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
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
|
|
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
|
|
476
|
-
return join(prefix, "node_modules",
|
|
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(
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
[
|
|
526
|
-
"
|
|
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 =
|
|
531
|
-
|
|
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
|
-
|
|
538
|
-
await
|
|
539
|
-
|
|
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
|
|
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
|
|
565
|
-
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
+
resolved.version,
|
|
930
1111
|
Number.isInteger(portOption) ? portOption : undefined,
|
|
931
1112
|
);
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1140
|
+
throw new LauncherError(`no devboard is installed in ${prefix}; run install`);
|
|
957
1141
|
}
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
1097
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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: "
|
|
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(`${
|
|
1349
|
+
throw new LauncherError(`${packageDir(prefix)} is missing; run install`);
|
|
1116
1350
|
}
|
|
1117
|
-
return `${
|
|
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
|
|
1126
|
-
if (version !==
|
|
1127
|
-
throw new LauncherError(`stale install: installed ${version}, latest ${
|
|
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
|
|
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,
|
|
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(
|
|
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 =
|
|
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
|
}
|