@dsdenes/devboard 0.1.560 → 0.1.563
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +729 -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/watchdog/main.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { runCycle } from "./coordinator";
|
|
4
|
+
import { acquireLock } from "./lock";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `bun run watchdog -- --once` runs one cycle; `--collect --dry-run` prints the
|
|
8
|
+
* cycle's report and changes nothing. The systemd oneshot service runs the
|
|
9
|
+
* former. Both respect the machine-wide lock.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const usage = `Usage: bun run watchdog [options]
|
|
13
|
+
|
|
14
|
+
Runs one watchdog cycle over the last ten minutes of dashboard evidence and
|
|
15
|
+
files one GitHub issue per distinct signal.
|
|
16
|
+
|
|
17
|
+
--once Discover, collect, and file. The default.
|
|
18
|
+
--collect Collect and print the report; requires --dry-run.
|
|
19
|
+
--dry-run Print the report without filing, writing, or changing anything.
|
|
20
|
+
--root <path> Explicit canonical repository root.
|
|
21
|
+
-h, --help Show this text.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
let once = false;
|
|
26
|
+
let collect = false;
|
|
27
|
+
let dryRun = false;
|
|
28
|
+
let rootOption: string | undefined;
|
|
29
|
+
|
|
30
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
31
|
+
const argument = args[index];
|
|
32
|
+
if (argument === "--once") once = true;
|
|
33
|
+
else if (argument === "--collect") collect = true;
|
|
34
|
+
else if (argument === "--dry-run") dryRun = true;
|
|
35
|
+
else if (argument === "--root") {
|
|
36
|
+
rootOption = args[index + 1];
|
|
37
|
+
index += 1;
|
|
38
|
+
if (rootOption === undefined) {
|
|
39
|
+
console.error("watchdog: --root needs a path");
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
} else if (argument === "-h" || argument === "--help") {
|
|
43
|
+
console.log(usage);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
} else {
|
|
46
|
+
console.error(`watchdog: unknown option ${argument}`);
|
|
47
|
+
console.error(usage);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (collect && !dryRun) {
|
|
53
|
+
console.error("watchdog: --collect runs with --dry-run");
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (once && (collect || dryRun)) {
|
|
58
|
+
console.error("watchdog: --once and --dry-run are exclusive");
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function gitTopLevel(): string {
|
|
63
|
+
const result = Bun.spawnSync({
|
|
64
|
+
cmd: ["git", "-C", dirname(import.meta.dir), "rev-parse", "--show-toplevel"],
|
|
65
|
+
stdin: "ignore",
|
|
66
|
+
stdout: "pipe",
|
|
67
|
+
stderr: "pipe",
|
|
68
|
+
});
|
|
69
|
+
const top = result.stdout.toString().trim();
|
|
70
|
+
if (result.exitCode !== 0 || top.length === 0) {
|
|
71
|
+
console.error("watchdog: the harness is not inside a git checkout");
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
return top;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const root = resolve(rootOption ?? gitTopLevel());
|
|
78
|
+
if (!existsSync(join(root, ".devboard"))) {
|
|
79
|
+
console.error(`watchdog: missing prerequisite: ${join(root, ".devboard")} does not exist`);
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lock = acquireLock(join(root, ".devboard", ".tmp", "watchdog", "lock"));
|
|
84
|
+
if (!lock.acquired) {
|
|
85
|
+
console.log(
|
|
86
|
+
`watchdog: already running${lock.owner === undefined ? "" : ` (pid ${lock.owner})`}`,
|
|
87
|
+
);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let exitCode = 0;
|
|
92
|
+
try {
|
|
93
|
+
const result = await runCycle({ root, dryRun: dryRun || collect });
|
|
94
|
+
if (dryRun || collect) {
|
|
95
|
+
console.log(JSON.stringify(result.report, null, 2));
|
|
96
|
+
}
|
|
97
|
+
exitCode = result.exitCode;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const detail = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
100
|
+
console.error(`watchdog: ${detail}`);
|
|
101
|
+
exitCode = 1;
|
|
102
|
+
} finally {
|
|
103
|
+
lock.release();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { latestWatchdogCycle, readWatchdogCycles, type WatchdogCycle } from "./history";
|
|
4
|
+
import { loadLedger } from "./ledger";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The read-only watchdog view the dashboard serves. It is derived from the
|
|
8
|
+
* newest cycle report, the retained run directories, and the ledger, and it
|
|
9
|
+
* never carries an environment value: only the action, the counts, and the
|
|
10
|
+
* issue URLs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface WatchdogIssue {
|
|
14
|
+
url: string;
|
|
15
|
+
category: string;
|
|
16
|
+
line: string;
|
|
17
|
+
filedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WatchdogStatus {
|
|
21
|
+
/** The newest cycle's start, UTC ISO 8601, or null before the first cycle. */
|
|
22
|
+
cycle: string | null;
|
|
23
|
+
action: string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
/** Findings in the newest cycle. */
|
|
26
|
+
findings: number;
|
|
27
|
+
/** Every issue the watchdog filed, newest first. */
|
|
28
|
+
issues: WatchdogIssue[];
|
|
29
|
+
/** Fingerprints still waiting for a successful filing. */
|
|
30
|
+
pending: number;
|
|
31
|
+
/** The retained cycles, newest first; empty before the first cycle. */
|
|
32
|
+
cycles: WatchdogCycle[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readWatchdogStatus(watchdogDir: string): WatchdogStatus {
|
|
36
|
+
let latest: Record<string, unknown> = {};
|
|
37
|
+
try {
|
|
38
|
+
const parsed: unknown = JSON.parse(readFileSync(join(watchdogDir, "latest.json"), "utf8"));
|
|
39
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
40
|
+
latest = parsed as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// No cycle has run yet.
|
|
44
|
+
}
|
|
45
|
+
const ledger = loadLedger(watchdogDir);
|
|
46
|
+
const issues = Object.values(ledger.findings)
|
|
47
|
+
.filter(
|
|
48
|
+
(entry): entry is typeof entry & { url: string } =>
|
|
49
|
+
entry.disposition.startsWith("filed:") && typeof entry.url === "string",
|
|
50
|
+
)
|
|
51
|
+
.map((entry) => ({
|
|
52
|
+
url: entry.url,
|
|
53
|
+
category: entry.category,
|
|
54
|
+
line: entry.line,
|
|
55
|
+
filedAt: entry.filedAt ?? entry.firstSeen,
|
|
56
|
+
}))
|
|
57
|
+
.sort((left, right) => right.filedAt.localeCompare(left.filedAt) || left.url.localeCompare(right.url));
|
|
58
|
+
return {
|
|
59
|
+
cycle: typeof latest.cycle === "string" ? latest.cycle : null,
|
|
60
|
+
action: typeof latest.action === "string" ? latest.action : "none",
|
|
61
|
+
...(typeof latest.reason === "string" ? { reason: latest.reason } : {}),
|
|
62
|
+
findings: Array.isArray(latest.findings) ? latest.findings.length : 0,
|
|
63
|
+
issues,
|
|
64
|
+
pending: ledger.pendingFiling.length,
|
|
65
|
+
cycles: readWatchdogCycles(
|
|
66
|
+
watchdogDir,
|
|
67
|
+
latestWatchdogCycle(latest, (fingerprint) => ledger.findings[fingerprint]?.url),
|
|
68
|
+
(fingerprint) => ledger.findings[fingerprint]?.url,
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, userInfo } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The systemd user adapter. It is the only part of the watchdog that writes
|
|
7
|
+
* outside the repository: two unit files and the timer's enable symlink.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const TIMER_NAME = "devboard-watchdog.timer";
|
|
11
|
+
export const SERVICE_NAME = "devboard-watchdog.service";
|
|
12
|
+
|
|
13
|
+
export interface UnitPaths {
|
|
14
|
+
directory: string;
|
|
15
|
+
service: string;
|
|
16
|
+
timer: string;
|
|
17
|
+
enableLink: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function unitPaths(): UnitPaths {
|
|
21
|
+
const directory = join(homedir(), ".config", "systemd", "user");
|
|
22
|
+
return {
|
|
23
|
+
directory,
|
|
24
|
+
service: join(directory, SERVICE_NAME),
|
|
25
|
+
timer: join(directory, TIMER_NAME),
|
|
26
|
+
enableLink: join(directory, "timers.target.wants", TIMER_NAME),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CommandResult {
|
|
31
|
+
code: number;
|
|
32
|
+
stdout: string;
|
|
33
|
+
stderr: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function systemctl(...arguments_: string[]): CommandResult {
|
|
37
|
+
const result = Bun.spawnSync({
|
|
38
|
+
cmd: ["systemctl", "--user", ...arguments_],
|
|
39
|
+
stdin: "ignore",
|
|
40
|
+
stdout: "pipe",
|
|
41
|
+
stderr: "pipe",
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
code: result.exitCode ?? 1,
|
|
45
|
+
stdout: result.stdout.toString().trim(),
|
|
46
|
+
stderr: result.stderr.toString().trim(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runCommand(argv: string[]): CommandResult {
|
|
51
|
+
const result = Bun.spawnSync({
|
|
52
|
+
cmd: argv,
|
|
53
|
+
stdin: "ignore",
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
code: result.exitCode ?? 1,
|
|
59
|
+
stdout: result.stdout.toString().trim(),
|
|
60
|
+
stderr: result.stderr.toString().trim(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Refuses a non-`yes` Linger before anything is installed. */
|
|
65
|
+
export function requireLinger(): string {
|
|
66
|
+
const result = runCommand(["loginctl", "show-user", userInfo().username, "-p", "Linger"]);
|
|
67
|
+
if (result.code === 0 && /Linger=yes/.test(result.stdout)) return "Linger=yes";
|
|
68
|
+
throw new Error(
|
|
69
|
+
"missing prerequisite: systemd user lingering is off. " +
|
|
70
|
+
`Run 'loginctl enable-linger ${userInfo().username}' and rerun the installer.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderUnit(source: string, values: Record<string, string>): string {
|
|
75
|
+
let text = readFileSync(source, "utf8");
|
|
76
|
+
for (const [key, value] of Object.entries(values)) {
|
|
77
|
+
text = text.split(`@${key}@`).join(value);
|
|
78
|
+
}
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function writeUnit(path: string, content: string): void {
|
|
83
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
84
|
+
writeFileSync(path, content);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function uniqueDirectories(paths: Array<string | undefined>): string {
|
|
88
|
+
const directories = new Set<string>();
|
|
89
|
+
for (const path of paths) {
|
|
90
|
+
if (path === undefined || path.length === 0) continue;
|
|
91
|
+
const directory = dirname(path);
|
|
92
|
+
if (directory.length > 0) directories.add(directory);
|
|
93
|
+
}
|
|
94
|
+
for (const fallback of ["/usr/local/bin", "/usr/bin", "/bin"]) directories.add(fallback);
|
|
95
|
+
return [...directories].join(":");
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { systemctl, unitPaths } from "./systemd";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stops and removes the user timer. It touches no repository file, so an
|
|
6
|
+
* uninstall leaves the harness exactly as it was.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const paths = unitPaths();
|
|
10
|
+
systemctl("stop", "devboard-watchdog.timer");
|
|
11
|
+
systemctl("stop", "devboard-watchdog.service");
|
|
12
|
+
systemctl("disable", "devboard-watchdog.timer");
|
|
13
|
+
for (const path of [paths.service, paths.timer, paths.enableLink]) {
|
|
14
|
+
if (!existsSync(path)) continue;
|
|
15
|
+
try {
|
|
16
|
+
unlinkSync(path);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(
|
|
19
|
+
`watchdog:uninstall: could not remove ${path}: ${
|
|
20
|
+
error instanceof Error ? error.message : String(error)
|
|
21
|
+
}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const reload = systemctl("daemon-reload");
|
|
26
|
+
if (reload.code !== 0) {
|
|
27
|
+
console.error(`watchdog:uninstall: daemon-reload failed: ${reload.stderr || reload.stdout}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.log("watchdog:uninstall: timer stopped, disabled, and removed");
|