heartbeads 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +49 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +8 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +6 -0
- package/.next/server/app/_not-found.rsc +9 -0
- package/.next/server/app/api/auth/route.js +1 -0
- package/.next/server/app/api/auth/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/route.js +8 -0
- package/.next/server/app/api/beads/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/stream/route.js +10 -0
- package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/beads.body +1 -0
- package/.next/server/app/api/beads.meta +1 -0
- package/.next/server/app/api/config/route.js +8 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/docs/page.js +120 -0
- package/.next/server/app/api/docs/page.js.nft.json +1 -0
- package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/docs.html +120 -0
- package/.next/server/app/api/docs.meta +5 -0
- package/.next/server/app/api/docs.rsc +70 -0
- package/.next/server/app/api/login/route.js +1 -0
- package/.next/server/app/api/login/route.js.nft.json +1 -0
- package/.next/server/app/api/logout/route.js +1 -0
- package/.next/server/app/api/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/callback/route.js +1 -0
- package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
- package/.next/server/app/api/records/route.js +1 -0
- package/.next/server/app/api/records/route.js.nft.json +1 -0
- package/.next/server/app/api/status/route.js +1 -0
- package/.next/server/app/api/status/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/graph/route.js +1 -0
- package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/ready/route.js +1 -0
- package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +5 -0
- package/.next/server/app/index.rsc +9 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +5 -0
- package/.next/server/app/login.rsc +9 -0
- package/.next/server/app/opengraph-image.png/route.js +1 -0
- package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
- package/.next/server/app/opengraph-image.png.body +0 -0
- package/.next/server/app/opengraph-image.png.meta +1 -0
- package/.next/server/app/page.js +24 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/twitter-image.png/route.js +1 -0
- package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
- package/.next/server/app/twitter-image.png.body +0 -0
- package/.next/server/app/twitter-image.png.meta +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/247.js +12 -0
- package/.next/server/chunks/29.js +1 -0
- package/.next/server/chunks/343.js +1 -0
- package/.next/server/chunks/460.js +12 -0
- package/.next/server/chunks/533.js +38 -0
- package/.next/server/chunks/542.js +27 -0
- package/.next/server/chunks/590.js +6 -0
- package/.next/server/chunks/615.js +15 -0
- package/.next/server/chunks/696.js +25 -0
- package/.next/server/chunks/719.js +2 -0
- package/.next/server/chunks/739.js +1 -0
- package/.next/server/chunks/950.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +32 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/middleware.js +14 -0
- package/.next/server/middleware.js.map +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
- package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
- package/.next/static/chunks/788-aa413085174e935a.js +1 -0
- package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
- package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
- package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
- package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
- package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
- package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
- package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
- package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
- package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
- package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
- package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
- package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
- package/README.md +389 -0
- package/app/api/auth/route.ts +103 -0
- package/app/api/beads/route.ts +27 -0
- package/app/api/beads/stream/route.ts +83 -0
- package/app/api/config/route.ts +48 -0
- package/app/api/docs/page.tsx +497 -0
- package/app/api/login/route.ts +42 -0
- package/app/api/logout/route.ts +14 -0
- package/app/api/oauth/callback/route.ts +97 -0
- package/app/api/oauth/client-metadata.json/route.ts +33 -0
- package/app/api/oauth/jwks.json/route.ts +32 -0
- package/app/api/records/route.ts +168 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/v1/graph/route.ts +251 -0
- package/app/api/v1/issues/[id]/route.ts +158 -0
- package/app/api/v1/ready/route.ts +229 -0
- package/app/globals.css +230 -0
- package/app/layout.tsx +51 -0
- package/app/login/page.tsx +164 -0
- package/app/not-found.tsx +91 -0
- package/app/opengraph-image.png +0 -0
- package/app/page.tsx +2041 -0
- package/app/twitter-image.png +0 -0
- package/bin/heartbeads.mjs +225 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +125 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +270 -0
- package/components/AuthButton.tsx +202 -0
- package/components/BeadTooltip.tsx +246 -0
- package/components/BeadsGraph.tsx +2493 -0
- package/components/BeadsLogo.tsx +94 -0
- package/components/CommentTooltip.tsx +338 -0
- package/components/ContextMenu.tsx +272 -0
- package/components/DescriptionModal.tsx +595 -0
- package/components/GraphStats.tsx +121 -0
- package/components/HeartIcon.tsx +33 -0
- package/components/HelpPanel.tsx +339 -0
- package/components/MobileActionSheet.tsx +255 -0
- package/components/NodeDetail.tsx +793 -0
- package/components/SettingsModal.tsx +315 -0
- package/components/StatusLegend.tsx +99 -0
- package/components/TimelineBar.tsx +116 -0
- package/components/TutorialOverlay.tsx +235 -0
- package/hooks/useBeadsComments.ts +81 -0
- package/hooks/useIsMobile.ts +19 -0
- package/lib/activity.ts +377 -0
- package/lib/agent.ts +29 -0
- package/lib/api-helpers.ts +46 -0
- package/lib/auth/client.ts +221 -0
- package/lib/auth.tsx +159 -0
- package/lib/comments.ts +413 -0
- package/lib/diff-beads.ts +128 -0
- package/lib/discover.ts +228 -0
- package/lib/env.ts +33 -0
- package/lib/gate.ts +55 -0
- package/lib/parse-beads.ts +234 -0
- package/lib/session.ts +52 -0
- package/lib/settings.ts +42 -0
- package/lib/timeline.ts +138 -0
- package/lib/tts.ts +397 -0
- package/lib/types.ts +271 -0
- package/lib/utils.ts +48 -0
- package/lib/watch-beads.ts +97 -0
- package/next.config.mjs +4 -0
- package/package.json +81 -0
- package/postcss.config.mjs +9 -0
- package/public/image.png +0 -0
- package/scripts/generate-jwk.js +38 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# heartbeads
|
|
2
|
+
|
|
3
|
+
**Interactive dependency graph viewer for [beads](https://github.com/GainForest/beads) (bd) issues.**
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
See your entire project's issues, epics, and dependencies as a live, explorable graph. heartbeads auto-discovers your `.beads/` directory and renders everything instantly -- no configuration needed.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Graph Visualization
|
|
14
|
+
|
|
15
|
+
- **5 graph layouts** -- Switch between layout modes via the toolbar:
|
|
16
|
+
- **Force** -- Organic force simulation with collision avoidance and variable link distances
|
|
17
|
+
- **DAG** -- Clean top-down directed acyclic graph with topological layering
|
|
18
|
+
- **Radial** -- Concentric rings by dependency depth. Root nodes (no blockers) sit at center, deeper dependencies on outer rings. Ring spacing scales with node count.
|
|
19
|
+
- **Cluster** -- Groups nodes spatially by project prefix. Each prefix gets its own cluster center arranged in a circle. Cross-project dependencies stretch visibly between clusters.
|
|
20
|
+
- **Spread** -- Like Force but maximally spaced for readability and screenshots. Stronger repulsion, wider link distances.
|
|
21
|
+
- **Collapse / Expand** -- Collapse all epics at once with a single button, or right-click individual epics to collapse/uncollapse them. Shows collapsed task count on each epic node.
|
|
22
|
+
- **Visual encoding** -- Node size = dependency importance (connection count), fill color = configurable via legend (see below), ring color = project prefix ([Catppuccin Latte](https://github.com/catppuccin/palette) palette). Larger nodes are more connected; epics get a size boost.
|
|
23
|
+
- **Legend color modes** -- Switch node fill color between 5 modes via the bottom-right legend panel:
|
|
24
|
+
- **Status** (default) -- Open (green), In Progress (amber), Blocked (red), Deferred (zinc), Closed (emerald)
|
|
25
|
+
- **Priority** -- P0 Critical (red), P1 High (orange), P2 Medium (blue), P3 Low (zinc), P4 Backlog (zinc-300)
|
|
26
|
+
- **Owner** -- Color by `createdBy` field using Catppuccin Latte accent palette (14 colors)
|
|
27
|
+
- **Assignee** -- Color by `assignee` field using Catppuccin Latte accent palette
|
|
28
|
+
- **Prefix** -- Color by project prefix using Catppuccin Latte accent palette
|
|
29
|
+
- **Catppuccin Latte palette** -- All prefix-colored elements (node rings, cluster circles, tooltip accent bars, prefix fill mode) use the [Catppuccin Latte](https://github.com/catppuccin/palette) accent palette (14 saturated colors optimized for light backgrounds). Person/prefix mapping is deterministic via FNV-1a hash.
|
|
30
|
+
- **Dependency arrows** -- Solid emerald arrows with flow particles for blocking relationships, dashed zinc lines for parent-child hierarchy. Curved links with configurable curvature.
|
|
31
|
+
- **Semantic zoom** -- When zooming out, individual nodes smoothly fade and are replaced by epic cluster labels at each cluster's centroid. Clusters show the epic title, ID, and member count, surrounded by a dashed circle in the project's prefix color. Cluster visibility can be toggled with the "Clusters" button in the top-left toolbar.
|
|
32
|
+
- **Spawn & exit animations** -- New nodes pop in with an overshoot easing (easeOutBack), removed nodes shrink out, and status changes trigger a ripple animation. New links flash bright emerald on arrival.
|
|
33
|
+
- **Hover tooltips** -- Hover over any node to see a tooltip card with the project prefix, issue ID, title, creation date, blocker list, priority, owner, and assignee. Smart viewport clamping (prefers above cursor, flips below).
|
|
34
|
+
- **Resizable minimap** -- Always-visible minimap (bottom-left) showing all nodes, links, claimed avatars, and the current viewport rectangle. Click to navigate. Drag the top, right, or top-right corner handles to resize (100-500px wide, 80-400px tall).
|
|
35
|
+
|
|
36
|
+
### Live Updates
|
|
37
|
+
|
|
38
|
+
- **SSE streaming** -- File watchers detect changes to `issues.jsonl` (and all additional repo JSONL files) and push updates via Server-Sent Events. No refresh needed.
|
|
39
|
+
- **Diff/merge pipeline** -- Incoming data is diffed against current state (`diffBeadsData`) and merged with position preservation (`mergeBeadsData`) so the graph layout doesn't reset. Animation metadata (`_spawnTime`, `_removeTime`, `_changedAt`) is stamped during merge.
|
|
40
|
+
|
|
41
|
+
### Timeline Replay
|
|
42
|
+
|
|
43
|
+
- **Step-based playback** -- Replay the entire history of your project as a step-by-step animation. Each step corresponds to one temporal event (issue creation, dependency addition).
|
|
44
|
+
- **Play/pause controls** -- Play, pause, and scrub through the timeline with a slider.
|
|
45
|
+
- **Speed toggle** -- 1x, 2x, 4x playback speed (2 seconds per event at 1x).
|
|
46
|
+
- **Uses the same diff/merge pipeline** as live updates, so nodes get proper positions and spawn animations during replay.
|
|
47
|
+
|
|
48
|
+
### Search
|
|
49
|
+
|
|
50
|
+
- **Smart search** -- `Cmd/Ctrl+F` to fuzzy search across all issues by ID, title, project prefix, owner, assignee, or commenter username. Keyboard navigation through results.
|
|
51
|
+
|
|
52
|
+
### Right-Click Context Menu
|
|
53
|
+
|
|
54
|
+
- **Multi-action menu** -- Right-click any node to open a context menu with actions:
|
|
55
|
+
- **Show description** -- Opens a full-screen modal with the issue's markdown description
|
|
56
|
+
- **Add comment** -- Opens the comment tooltip for posting
|
|
57
|
+
- **Claim task** -- Posts a claim comment (`@handle`) to mark yourself as working on the issue (only shown when authenticated and node is unclaimed)
|
|
58
|
+
- **Unclaim task** -- Removes your claim from the node (only shown when you are the claimant)
|
|
59
|
+
- **Collapse/Uncollapse epic** -- Toggle individual epic collapse on right-click (only shown on epic nodes)
|
|
60
|
+
|
|
61
|
+
### Mobile Responsive
|
|
62
|
+
|
|
63
|
+
- **Hamburger menu** -- On mobile viewports (<=768px), the nav pills are replaced with a hamburger menu that opens a slide-in drawer from the right with all navigation items.
|
|
64
|
+
- **Double-tap context menu** -- Since right-click doesn't exist on mobile, double-tap a node to open a bottom action sheet with the same actions as the desktop context menu.
|
|
65
|
+
- **Touch-optimized** -- Hover tooltips suppressed on mobile (no flicker), comment tooltips render as bottom sheets, text-selection TTS tooltip disabled, activity overlay compact mode (3 events).
|
|
66
|
+
- **Bottom drawer** -- Node detail slides up as a bottom drawer instead of a sidebar on mobile.
|
|
67
|
+
|
|
68
|
+
### Task Claiming
|
|
69
|
+
|
|
70
|
+
- **Claim tasks** -- Right-click a node and select "Claim task" to mark yourself as working on it. Posts a special `@handle` comment via ATProto.
|
|
71
|
+
- **Avatar badges** -- Claimed nodes display the claimant's circular avatar at the bottom-right of the node on the graph canvas. Avatars are drawn at constant screen-space size and also appear on the minimap.
|
|
72
|
+
- **Avatar hover tooltip** -- Hover over a claimed node's avatar to see the claimant's profile picture, handle, and when they claimed it (relative time).
|
|
73
|
+
- **Optimistic UI** -- Claims and unclaims update immediately in the UI before the server round-trip completes. Optimistic claims show the avatar instantly; optimistic unclaims suppress it instantly.
|
|
74
|
+
- **Unclaim** -- Remove your claim via the context menu. Deletes the underlying ATProto comment record.
|
|
75
|
+
|
|
76
|
+
### Node Detail Sidebar
|
|
77
|
+
|
|
78
|
+
- **Click-to-inspect** -- Click any node to open a detail sidebar with:
|
|
79
|
+
- Issue ID, title, type icon, status/priority/prefix badges
|
|
80
|
+
- GitHub repository link (auto-detected from git remote, shown as clickable badge + URL)
|
|
81
|
+
- Metrics grid: blocks count, dependent count
|
|
82
|
+
- Blocker and dependent lists with clickable navigation
|
|
83
|
+
- Dates: created, updated, closed (with hour:minute precision)
|
|
84
|
+
- Owner attribution
|
|
85
|
+
- Full description rendered as Markdown (with GFM support)
|
|
86
|
+
- Copy-to-clipboard button for descriptions (copies raw markdown with a header showing project prefix, issue ID, and GitHub repo URL)
|
|
87
|
+
- Threaded comment section (see below)
|
|
88
|
+
- **Description modal** -- "View in window" opens a full-screen modal with the rendered description and a copy button. Also accessible via right-click context menu "Show description".
|
|
89
|
+
- **Mobile drawer** -- On small screens, the detail panel slides up as a bottom drawer instead of a sidebar.
|
|
90
|
+
|
|
91
|
+
### ATProto Authentication & Comments
|
|
92
|
+
|
|
93
|
+
- **OAuth 2.0 login** -- Sign in with your Bluesky/ATProto account via OAuth 2.0 with PKCE. Avatar and handle shown in the header. Dual mode: public client for dev, confidential (ES256 JWK) for production.
|
|
94
|
+
- **Comment annotations** -- Right-click any node to post a comment via ATProto (`org.impactindexer.review.comment` lexicon). Comments stored on the AT Protocol, fetched from the Hypergoat GraphQL indexer.
|
|
95
|
+
- **Threaded replies** -- Comments support nested replies (`replyTo` field). Root comments sorted newest-first, replies sorted chronologically. Rendered with indentation (`ml-4 pl-3 border-l`).
|
|
96
|
+
- **Likes** -- Heart-toggle likes on comments (`org.impactindexer.review.like` lexicon). Rose-colored when liked by the current user.
|
|
97
|
+
- **Delete** -- Delete your own comments and likes.
|
|
98
|
+
- **Comment badges** -- Nodes with comments show a red notification badge with the comment count on the graph canvas.
|
|
99
|
+
- **All Comments panel** -- Slide-in sidebar showing all comments across all nodes with threaded replies, clickable node navigation pills, and like/delete actions.
|
|
100
|
+
|
|
101
|
+
### Multi-Repo Support
|
|
102
|
+
|
|
103
|
+
- **Automatic aggregation** -- If your `.beads/config.yaml` lists additional repositories, heartbeads loads all of them into a unified graph.
|
|
104
|
+
- **Per-project colors** -- Each project prefix gets a deterministic color from the Catppuccin Latte palette (14 accent colors). Shown as node rings, prefix badges, cluster borders, and tooltip accent bars.
|
|
105
|
+
- **GitHub repo links** -- Auto-detects git remote URLs for each repository (primary + additional). Shown in the node detail card as a clickable GitHub link. Also included in copied description text.
|
|
106
|
+
|
|
107
|
+
### Header / Navbar
|
|
108
|
+
|
|
109
|
+
- **Frosted glass header** -- `bg-white/95 backdrop-blur-sm` sticky header inspired by [plresearch.org](https://plresearch.org).
|
|
110
|
+
- **Animated heartbeat logo** -- An animated ECG/heartbeat trace SVG that continuously pulses — representing the living heartbeat of your project.
|
|
111
|
+
- **Pill navigation** -- Replay and Comments toggle buttons styled as rounded-full pill items. Auth button with rounded avatar dropdown.
|
|
112
|
+
- **Centered search** -- Rounded-full search bar with keyboard shortcut hint and dropdown results panel.
|
|
113
|
+
- **Mobile hamburger menu** -- On small screens, nav pills collapse into a hamburger menu with a slide-in drawer.
|
|
114
|
+
|
|
115
|
+
### Info Panel & Legend
|
|
116
|
+
|
|
117
|
+
- **Bottom-right info panel** -- Shows issue count, dependency count, project count, color mode selector (Status / Priority / Owner / Assignee / Prefix), dynamic legend dots for the active mode, and visual encoding hints. Hidden during timeline replay. Legend shows only items present in visible nodes for owner/assignee/prefix modes.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quick Start
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd ~/my-project
|
|
125
|
+
npx heartbeads@latest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
heartbeads walks up from your current directory to find `.beads/`, just like `git` finds `.git/`.
|
|
129
|
+
|
|
130
|
+
### With an explicit path
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx heartbeads@latest --beads-dir ~/projects/my-project/.beads
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Multi-repo aggregation
|
|
137
|
+
|
|
138
|
+
If your `.beads/config.yaml` lists additional repositories, heartbeads automatically loads all of them:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
# .beads/config.yaml
|
|
142
|
+
repos:
|
|
143
|
+
additional:
|
|
144
|
+
- ../backend
|
|
145
|
+
- ../frontend
|
|
146
|
+
- ../shared-lib
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## CLI Reference
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
heartbeads [options]
|
|
155
|
+
|
|
156
|
+
Options:
|
|
157
|
+
--port <number> Port to serve on (default: 3000)
|
|
158
|
+
--beads-dir <path> Explicit .beads/ directory path
|
|
159
|
+
--password <string> Require a password to access the dashboard and API
|
|
160
|
+
--dev Run in development mode (hot reload)
|
|
161
|
+
--help, -h Show this help message
|
|
162
|
+
|
|
163
|
+
Environment:
|
|
164
|
+
BEADS_DIR Override .beads/ discovery (same as --beads-dir)
|
|
165
|
+
HEARTBEADS_PASSWORD Require a password (same as --password)
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
npx heartbeads@latest # Auto-discover from cwd
|
|
169
|
+
npx heartbeads@latest --port 4000 # Custom port
|
|
170
|
+
npx heartbeads@latest --beads-dir ~/projects/hub/.beads # Explicit path
|
|
171
|
+
npx heartbeads@latest --password secret # Password-protected
|
|
172
|
+
BEADS_DIR=../.beads npx heartbeads@latest --dev # Dev mode with env var
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Public API
|
|
178
|
+
|
|
179
|
+
Heartbeads exposes a read-only REST API for AI agents, CI/CD bots, and integrations. All endpoints return JSON with CORS headers (`Access-Control-Allow-Origin: *`).
|
|
180
|
+
|
|
181
|
+
When running with `--password`, API requests require authentication via one of:
|
|
182
|
+
- **Bearer token:** `curl -H "Authorization: Bearer <password>" ...`
|
|
183
|
+
- **Query param:** `curl "...?token=<password>"`
|
|
184
|
+
|
|
185
|
+
Without `--password`, all endpoints are open (no auth required).
|
|
186
|
+
|
|
187
|
+
### `GET /api/v1/graph` -- Full project snapshot
|
|
188
|
+
|
|
189
|
+
Returns the entire beads graph with issues, dependencies, comments, claims, and recent activity in a single response.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Full snapshot (issues + comments + activity)
|
|
193
|
+
curl http://localhost:3000/api/v1/graph
|
|
194
|
+
|
|
195
|
+
# Only open issues, skip comments for speed
|
|
196
|
+
curl "http://localhost:3000/api/v1/graph?status=open&include="
|
|
197
|
+
|
|
198
|
+
# Open + in_progress with activity feed capped at 20
|
|
199
|
+
curl "http://localhost:3000/api/v1/graph?status=open,in_progress&include=comments,activity&limit=20"
|
|
200
|
+
|
|
201
|
+
# Filter by repo prefix
|
|
202
|
+
curl "http://localhost:3000/api/v1/graph?prefix=beads-map"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Param | Description | Example |
|
|
206
|
+
|-------|-------------|---------|
|
|
207
|
+
| `status` | Filter by status (comma-separated) | `open,in_progress` |
|
|
208
|
+
| `priority` | Filter by priority (comma-separated) | `0,1` |
|
|
209
|
+
| `prefix` | Filter by repo prefix | `beads-map` |
|
|
210
|
+
| `include` | Opt-in expensive fields (default: all) | `comments,activity` |
|
|
211
|
+
| `limit` | Activity feed cap (default 50, max 200) | `20` |
|
|
212
|
+
|
|
213
|
+
Response includes: `project` metadata, `issues[]` with comments and claims, `dependencies[]`, `stats`, `activity[]`, and `_meta`.
|
|
214
|
+
|
|
215
|
+
### `GET /api/v1/issues/:id` -- Single issue detail
|
|
216
|
+
|
|
217
|
+
Returns one issue with full description, threaded comments, and enriched blockers/dependents (with title + status).
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
curl http://localhost:3000/api/v1/issues/beads-map-abc
|
|
221
|
+
|
|
222
|
+
# Returns 404 with hint if not found
|
|
223
|
+
curl http://localhost:3000/api/v1/issues/nonexistent
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `GET /api/v1/ready` -- Actionable issues
|
|
227
|
+
|
|
228
|
+
Returns issues ready to work on: open or in_progress with no unresolved blockers. Sorted by priority (critical first), then by age (oldest first).
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# All actionable issues
|
|
232
|
+
curl http://localhost:3000/api/v1/ready
|
|
233
|
+
|
|
234
|
+
# Only unclaimed bugs
|
|
235
|
+
curl "http://localhost:3000/api/v1/ready?unclaimed=true&type=bug"
|
|
236
|
+
|
|
237
|
+
# Limit to top 10
|
|
238
|
+
curl "http://localhost:3000/api/v1/ready?limit=10"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
| Param | Description | Example |
|
|
242
|
+
|-------|-------------|---------|
|
|
243
|
+
| `unclaimed` | Only unclaimed issues | `true` |
|
|
244
|
+
| `type` | Filter by issue type (comma-separated) | `bug,feature` |
|
|
245
|
+
| `assignee` | Filter by assignee | `alice` |
|
|
246
|
+
| `prefix` | Filter by repo prefix | `beads-map` |
|
|
247
|
+
| `limit` | Max issues returned | `20` |
|
|
248
|
+
|
|
249
|
+
Response includes: `issues[]` with comments and claims, `stats` summary (total_ready, unclaimed, by_priority, by_type), and `_meta`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
git clone https://github.com/GainForest/heartbeads.git
|
|
257
|
+
cd heartbeads
|
|
258
|
+
pnpm install
|
|
259
|
+
|
|
260
|
+
# Run in dev mode against a beads project
|
|
261
|
+
BEADS_DIR=~/path/to/.beads pnpm dev
|
|
262
|
+
|
|
263
|
+
# Build for production
|
|
264
|
+
pnpm build
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Quality gate
|
|
268
|
+
|
|
269
|
+
`pnpm build` must pass with zero errors before committing. There are no tests yet -- the build is the sole gate.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Architecture
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
heartbeads/
|
|
277
|
+
├── middleware.ts # Password gate: auth check on every request (Edge Runtime)
|
|
278
|
+
├── app/
|
|
279
|
+
│ ├── page.tsx # Main page: SSE wiring, merge logic, search, layout, comments, claims
|
|
280
|
+
│ ├── layout.tsx # Root layout, wraps in AuthProvider
|
|
281
|
+
│ ├── not-found.tsx # Custom 404 page with heartbeat logo
|
|
282
|
+
│ ├── login/page.tsx # Password login page (shown when --password flag is set)
|
|
283
|
+
│ ├── globals.css # Timeline slider styles, markdown prose, scrollbar
|
|
284
|
+
│ └── api/
|
|
285
|
+
│ ├── beads/
|
|
286
|
+
│ │ ├── route.ts # GET /api/beads -- one-shot full data load
|
|
287
|
+
│ │ └── stream/route.ts # GET /api/beads/stream -- SSE live updates
|
|
288
|
+
│ ├── auth/route.ts # GET/POST/DELETE /api/auth -- dashboard password gate
|
|
289
|
+
│ ├── v1/
|
|
290
|
+
│ │ ├── graph/route.ts # GET /api/v1/graph -- full project snapshot for AI agents
|
|
291
|
+
│ │ ├── issues/[id]/ # GET /api/v1/issues/:id -- single issue detail
|
|
292
|
+
│ │ └── ready/route.ts # GET /api/v1/ready -- actionable issues
|
|
293
|
+
│ ├── config/route.ts # GET /api/config -- project name, repo count, repo URLs
|
|
294
|
+
│ ├── login/route.ts # POST /api/login -- initiate ATProto OAuth
|
|
295
|
+
│ ├── logout/route.ts # POST /api/logout -- clear session
|
|
296
|
+
│ ├── status/route.ts # GET /api/status -- current auth state
|
|
297
|
+
│ ├── records/route.ts # POST/PUT/DELETE /api/records -- ATProto record CRUD
|
|
298
|
+
│ └── oauth/
|
|
299
|
+
│ ├── callback/route.ts # OAuth callback handler
|
|
300
|
+
│ ├── client-metadata.json/ # OAuth client metadata
|
|
301
|
+
│ └── jwks.json/route.ts # JSON Web Key Set
|
|
302
|
+
├── components/
|
|
303
|
+
│ ├── AllCommentsPanel.tsx # Slide-in panel: all comments across nodes, threaded
|
|
304
|
+
│ ├── AuthButton.tsx # Sign-in modal + avatar dropdown (rounded-full pill style)
|
|
305
|
+
│ ├── BeadsGraph.tsx # Force graph: paintNode/paintLink, minimap, semantic zoom, avatars
|
|
306
|
+
│ ├── BeadsLogo.tsx # Animated heartbeat (ECG) SVG logo
|
|
307
|
+
│ ├── BeadTooltip.tsx # Hover tooltip: prefix, ID, title, date, blockers, priority, owner
|
|
308
|
+
│ ├── CommentTooltip.tsx # Floating right-click comment tooltip (bottom sheet on mobile)
|
|
309
|
+
│ ├── ContextMenu.tsx # Right-click context menu: description, comment, claim/unclaim
|
|
310
|
+
│ ├── DescriptionModal.tsx # Full-screen markdown description modal with copy button
|
|
311
|
+
│ ├── HeartIcon.tsx # Shared heart SVG (outline/filled)
|
|
312
|
+
│ ├── MobileActionSheet.tsx # Mobile bottom action sheet (double-tap context menu)
|
|
313
|
+
│ ├── NodeDetail.tsx # Sidebar detail: metadata, deps, comments, repo link
|
|
314
|
+
│ ├── TimelineBar.tsx # Timeline replay: play/pause, scrubber, speed toggle
|
|
315
|
+
│ ├── GraphStats.tsx # Issue count statistics widget (unused, inlined in BeadsGraph)
|
|
316
|
+
│ └── StatusLegend.tsx # Color legend for statuses (unused, inlined in BeadsGraph)
|
|
317
|
+
├── hooks/
|
|
318
|
+
│ ├── useBeadsComments.ts # React hook wrapper for lib/comments.ts
|
|
319
|
+
│ └── useIsMobile.ts # Mobile viewport detection hook (<=768px)
|
|
320
|
+
├── lib/
|
|
321
|
+
│ ├── comments.ts # Shared comment fetching, threading, claim detection
|
|
322
|
+
│ ├── gate.ts # Password gate: HMAC token generation/validation
|
|
323
|
+
│ ├── api-helpers.ts # CORS headers, JSON response builders for /api/v1/*
|
|
324
|
+
│ ├── auth.tsx # AuthProvider context + useAuth hook
|
|
325
|
+
│ ├── auth/client.ts # OAuth client factory (public/confidential mode)
|
|
326
|
+
│ ├── agent.ts # Authenticated ATProto agent from session
|
|
327
|
+
│ ├── session.ts # iron-session encrypted cookie setup
|
|
328
|
+
│ ├── env.ts # Environment variable validation
|
|
329
|
+
│ ├── discover.ts # .beads/ auto-discovery + git remote URL detection
|
|
330
|
+
│ ├── parse-beads.ts # JSONL parser, multi-repo hydration, graph builder
|
|
331
|
+
│ ├── types.ts # GraphNode, GraphLink, ColorMode, Catppuccin palette, color helpers
|
|
332
|
+
│ ├── diff-beads.ts # Diff engine for detecting added/removed/changed nodes/links
|
|
333
|
+
│ ├── watch-beads.ts # fs.watch wrapper with debounce for JSONL file changes
|
|
334
|
+
│ ├── settings.ts # ElevenLabs TTS settings (localStorage)
|
|
335
|
+
│ ├── tts.ts # Text-to-speech with ElevenLabs API, alignment cache
|
|
336
|
+
│ ├── activity.ts # Activity feed event types and builders
|
|
337
|
+
│ ├── timeline.ts # buildTimelineEvents + filterDataAtTime for replay
|
|
338
|
+
│ └── utils.ts # formatRelativeTime, buildDescriptionCopyText, shared utilities
|
|
339
|
+
├── bin/
|
|
340
|
+
│ └── heartbeads.mjs # CLI entry point
|
|
341
|
+
├── scripts/
|
|
342
|
+
│ └── generate-jwk.js # ES256 JWK key generation for OAuth
|
|
343
|
+
└── public/
|
|
344
|
+
└── image.png # Screenshot for README
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Data flow
|
|
348
|
+
|
|
349
|
+
1. **Discovery** -- `lib/discover.ts` walks up from `cwd` (or reads `BEADS_DIR`) to find `.beads/`. Also detects git remote URLs for all repos.
|
|
350
|
+
2. **Parsing** -- `lib/parse-beads.ts` reads `issues.jsonl` from the primary repo and any additional repos in `config.yaml`, deduplicates, extracts dependencies, and builds a graph structure with computed fields (blocker/dependent counts, prefix colors).
|
|
351
|
+
3. **Rendering** -- `components/BeadsGraph.tsx` uses `react-force-graph-2d` with fully custom canvas rendering (`paintNode`, `paintLink`). All transient state (hover, selection, comments, claimed avatars) stored in refs to avoid re-rendering the force simulation.
|
|
352
|
+
4. **Live updates** -- `app/api/beads/stream/route.ts` opens an SSE connection, watches all JSONL files, and pushes full data on each change. The client diffs and merges with position preservation.
|
|
353
|
+
5. **Timeline** -- `lib/timeline.ts` extracts sorted temporal events from the data. During replay, `filterDataAtTime()` produces a time-slice that flows through the same diff/merge pipeline as live updates.
|
|
354
|
+
6. **Comments** -- `lib/comments.ts` fetches from the Hypergoat GraphQL indexer, resolves Bluesky profiles, and builds threaded trees. Used by both the React hook (`hooks/useBeadsComments.ts`) and the public API routes (`/api/v1/*`).
|
|
355
|
+
7. **Public API** -- `/api/v1/graph`, `/api/v1/issues/:id`, and `/api/v1/ready` combine beads data + comments + activity into enriched JSON for AI agents. No auth required, CORS enabled.
|
|
356
|
+
7. **Claims** -- A claim is a comment with text `@handle` (starts with `@`, no spaces). The `claimedNodeAvatars` useMemo in `page.tsx` scans all comments to build a `Map<nodeId, claimInfo>`. Avatars are drawn on canvas nodes and the minimap by `paintNode` via `claimedNodeAvatarsRef`.
|
|
357
|
+
|
|
358
|
+
### Key design decisions
|
|
359
|
+
|
|
360
|
+
- **paintNode/paintLink use refs, not props** -- Callbacks have `[]` dependencies and read from `selectedNodeRef`, `hoveredNodeRef`, `claimedNodeAvatarsRef`, etc. This prevents re-creating the ForceGraph component on every interaction.
|
|
361
|
+
- **Position preservation is critical** -- `react-force-graph-2d` mutates node objects in-place (`x`, `y`, `vx`, `vy`). The merge logic copies these from old nodes to new nodes to prevent layout resets.
|
|
362
|
+
- **Animation metadata convention** -- Fields prefixed with `_` on nodes/links (`_spawnTime`, `_removeTime`, `_changedAt`, `_prevStatus`) are transient, set by `mergeBeadsData()`, consumed by `paintNode`/`paintLink`, and garbage-collected after 600ms.
|
|
363
|
+
- **Bootstrap trick** -- The graph starts in DAG mode for 15ms to spread nodes into good positions, then auto-switches to Force mode. This gives the organic layout a clean starting arrangement.
|
|
364
|
+
- **Link source/target normalization** -- `filterDataAtTime()` must spread new link objects with string `source`/`target` (not the original mutated object refs from d3-force). Without this, links draw to wrong positions during timeline replay.
|
|
365
|
+
- **Optimistic claim/unclaim** -- Two state variables: `optimisticClaims` (Map) for immediate avatar display, `optimisticUnclaims` (Set) for immediate avatar suppression. Both reconciled with comment-derived data in `claimedNodeAvatars` useMemo.
|
|
366
|
+
- **Avatar image cache** -- Module-level `avatarImageCache` in `BeadsGraph.tsx`. No `crossOrigin` attribute on Image elements (Bluesky CDN CORS issue). `getAvatarImage()` returns cached `HTMLImageElement` or starts loading and returns null.
|
|
367
|
+
- **Portal for modals** -- `DescriptionModal` uses `createPortal(jsx, document.body)` with `z-[100]` to escape any parent stacking contexts.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Tech Stack
|
|
372
|
+
|
|
373
|
+
- [Next.js 14](https://nextjs.org/) (App Router)
|
|
374
|
+
- [react-force-graph-2d](https://github.com/vasturiano/react-force-graph) for canvas-based graph rendering
|
|
375
|
+
- [d3-force](https://github.com/d3/d3-force) (forceCollide, forceRadial, forceX, forceY for layout modes)
|
|
376
|
+
- [Catppuccin Latte](https://github.com/catppuccin/palette) accent palette for prefix/person coloring (14 colors)
|
|
377
|
+
- [Tailwind CSS](https://tailwindcss.com/) for styling
|
|
378
|
+
- [TypeScript](https://www.typescriptlang.org/) throughout
|
|
379
|
+
- [@atproto/oauth-client-node](https://github.com/bluesky-social/atproto) + [@atproto/api](https://github.com/bluesky-social/atproto) for ATProto authentication and record CRUD
|
|
380
|
+
- [iron-session](https://github.com/vvo/iron-session) for encrypted cookie session management
|
|
381
|
+
- [react-markdown](https://github.com/remarkjs/react-markdown) + [remark-gfm](https://github.com/remarkjs/remark-gfm) for description rendering
|
|
382
|
+
- Node.js `fs.watch` for file change detection
|
|
383
|
+
- Server-Sent Events for real-time streaming
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard password authentication API.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/auth — Check if password protection is active and if the request is authenticated
|
|
5
|
+
* POST /api/auth — Validate password and set auth cookie
|
|
6
|
+
* DELETE /api/auth — Clear auth cookie (lock dashboard)
|
|
7
|
+
*
|
|
8
|
+
* This is separate from ATProto OAuth (Sign In button in navbar).
|
|
9
|
+
* ATProto is for identity (comments, likes, claims).
|
|
10
|
+
* This gate controls who can view the dashboard and access the API.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NextResponse } from "next/server";
|
|
14
|
+
import { cookies } from "next/headers";
|
|
15
|
+
import { env } from "@/lib/env";
|
|
16
|
+
import {
|
|
17
|
+
COOKIE_NAME,
|
|
18
|
+
generateGateToken,
|
|
19
|
+
validateGateToken,
|
|
20
|
+
comparePassword,
|
|
21
|
+
} from "@/lib/gate";
|
|
22
|
+
|
|
23
|
+
export const dynamic = "force-dynamic";
|
|
24
|
+
|
|
25
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
26
|
+
|
|
27
|
+
export async function GET() {
|
|
28
|
+
const password = env.HEARTBEADS_PASSWORD;
|
|
29
|
+
|
|
30
|
+
if (!password) {
|
|
31
|
+
return NextResponse.json({ protected: false });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cookieStore = await cookies();
|
|
35
|
+
const token = cookieStore.get(COOKIE_NAME)?.value;
|
|
36
|
+
const authenticated = token ? validateGateToken(token, password) : false;
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({ protected: true, authenticated });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function POST(request: Request) {
|
|
42
|
+
const password = env.HEARTBEADS_PASSWORD;
|
|
43
|
+
|
|
44
|
+
if (!password) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: "Password protection is not enabled" },
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let body: { password?: string };
|
|
52
|
+
try {
|
|
53
|
+
body = await request.json();
|
|
54
|
+
} catch {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: "Invalid request body" },
|
|
57
|
+
{ status: 400 }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const provided = body.password;
|
|
62
|
+
if (!provided || typeof provided !== "string") {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: "Password is required" },
|
|
65
|
+
{ status: 400 }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!comparePassword(provided, password)) {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: "Invalid password" },
|
|
72
|
+
{ status: 401 }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Password matches — set the gate cookie
|
|
77
|
+
const token = generateGateToken(password);
|
|
78
|
+
const response = NextResponse.json({ success: true });
|
|
79
|
+
|
|
80
|
+
response.cookies.set(COOKIE_NAME, token, {
|
|
81
|
+
httpOnly: true,
|
|
82
|
+
sameSite: "lax",
|
|
83
|
+
secure: isProduction,
|
|
84
|
+
path: "/",
|
|
85
|
+
// Session cookie — no maxAge, expires when browser closes
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function DELETE() {
|
|
92
|
+
const response = NextResponse.json({ success: true });
|
|
93
|
+
|
|
94
|
+
response.cookies.set(COOKIE_NAME, "", {
|
|
95
|
+
httpOnly: true,
|
|
96
|
+
sameSite: "lax",
|
|
97
|
+
secure: isProduction,
|
|
98
|
+
path: "/",
|
|
99
|
+
maxAge: 0, // Expire immediately
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return response;
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { loadBeadsData } from "@/lib/parse-beads";
|
|
3
|
+
import { discoverBeadsDir } from "@/lib/discover";
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
const { beadsDir } = discoverBeadsDir();
|
|
8
|
+
const data = loadBeadsData(beadsDir);
|
|
9
|
+
return NextResponse.json(data);
|
|
10
|
+
} catch (error: any) {
|
|
11
|
+
// Distinguish discovery errors (404) from data loading errors (500)
|
|
12
|
+
if (error?.message?.includes("No .beads/ directory found")) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{
|
|
15
|
+
error: "No .beads directory found",
|
|
16
|
+
hint: "Run bd init in your project, then start heartbeads from that directory",
|
|
17
|
+
},
|
|
18
|
+
{ status: 404 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
console.error("Failed to load beads data:", error);
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: "Failed to load beads data" },
|
|
24
|
+
{ status: 500 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { discoverBeadsDir } from "@/lib/discover";
|
|
2
|
+
import { loadBeadsData } from "@/lib/parse-beads";
|
|
3
|
+
import { watchBeadsFiles } from "@/lib/watch-beads";
|
|
4
|
+
|
|
5
|
+
// Prevent Next.js from statically optimizing this route
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
let cleanup: (() => void) | null = null;
|
|
10
|
+
|
|
11
|
+
const stream = new ReadableStream({
|
|
12
|
+
start(controller) {
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
|
|
15
|
+
function send(data: unknown) {
|
|
16
|
+
try {
|
|
17
|
+
controller.enqueue(
|
|
18
|
+
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
// Stream closed — cleanup will handle
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const { beadsDir } = discoverBeadsDir();
|
|
27
|
+
|
|
28
|
+
// Send initial data
|
|
29
|
+
const initialData = loadBeadsData(beadsDir);
|
|
30
|
+
send(initialData);
|
|
31
|
+
|
|
32
|
+
// Watch for changes and push updates
|
|
33
|
+
cleanup = watchBeadsFiles(beadsDir, () => {
|
|
34
|
+
try {
|
|
35
|
+
const newData = loadBeadsData(beadsDir);
|
|
36
|
+
send(newData);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error("Failed to reload beads data:", err);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Heartbeat every 30s to keep connection alive through proxies/firewalls
|
|
43
|
+
const heartbeat = setInterval(() => {
|
|
44
|
+
try {
|
|
45
|
+
controller.enqueue(encoder.encode(": heartbeat\n\n"));
|
|
46
|
+
} catch {
|
|
47
|
+
clearInterval(heartbeat);
|
|
48
|
+
}
|
|
49
|
+
}, 30000);
|
|
50
|
+
|
|
51
|
+
// Clean up when client disconnects
|
|
52
|
+
request.signal.addEventListener("abort", () => {
|
|
53
|
+
clearInterval(heartbeat);
|
|
54
|
+
if (cleanup) cleanup();
|
|
55
|
+
try {
|
|
56
|
+
controller.close();
|
|
57
|
+
} catch {
|
|
58
|
+
/* already closed */
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
// Discovery failed — send error and close
|
|
63
|
+
const message =
|
|
64
|
+
err instanceof Error ? err.message : "Unknown error";
|
|
65
|
+
send({ error: message });
|
|
66
|
+
controller.close();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
cancel() {
|
|
71
|
+
if (cleanup) cleanup();
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return new Response(stream, {
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "text/event-stream",
|
|
78
|
+
"Cache-Control": "no-cache, no-transform",
|
|
79
|
+
Connection: "keep-alive",
|
|
80
|
+
"X-Accel-Buffering": "no", // Disable Nginx buffering
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { discoverBeadsDir, getRepoUrls } from "@/lib/discover";
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
try {
|
|
11
|
+
const discovery = discoverBeadsDir();
|
|
12
|
+
|
|
13
|
+
// Read repos list from config.yaml
|
|
14
|
+
let repos: string[] = ["."];
|
|
15
|
+
const configPath = join(discovery.beadsDir, "config.yaml");
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(configPath)) {
|
|
18
|
+
const content = readFileSync(configPath, "utf-8");
|
|
19
|
+
const config = parseYaml(content);
|
|
20
|
+
const additional = config?.repos?.additional;
|
|
21
|
+
if (Array.isArray(additional)) {
|
|
22
|
+
repos = [config?.repos?.primary || ".", ...additional];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// config.yaml unreadable — use defaults
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Build prefix → GitHub URL mapping from git remotes
|
|
30
|
+
const repoUrls = getRepoUrls(discovery.beadsDir);
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({
|
|
33
|
+
name: discovery.issuePrefix || discovery.repoName,
|
|
34
|
+
prefix: discovery.issuePrefix || null,
|
|
35
|
+
repoCount: 1 + discovery.additionalRepos,
|
|
36
|
+
repos,
|
|
37
|
+
repoUrls,
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
// No .beads/ found — return safe defaults
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
name: "Beads",
|
|
43
|
+
prefix: null,
|
|
44
|
+
repoCount: 0,
|
|
45
|
+
repos: [],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|