bmad-viewer 0.1.5 → 0.1.7
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/README.md +70 -24
- package/package.json +3 -2
- package/src/server/http-server.js +4 -6
- package/src/server/websocket.js +18 -98
- package/src/watchers/file-watcher.js +4 -2
package/README.md
CHANGED
|
@@ -1,48 +1,94 @@
|
|
|
1
1
|
# bmad-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Stop wasting tokens asking "where am I?"** — see your BMAD project status, browse the full agent/workflow catalog, and search everything with Ctrl+K. Zero tokens, zero config.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 🔍 Fuzzy search across agents, workflows, and tools
|
|
9
|
-
- 📝 Markdown-based wiki with auto-refresh
|
|
10
|
-
- 🎨 Dark/light theme support
|
|
11
|
-
- 🚀 Zero-config - auto-detects `_bmad/` folder
|
|
12
|
-
- 📦 Installable via npx - no global installation needed
|
|
7
|
+
## The problem
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
BMAD is powerful — 21 agents, 43 workflows, hundreds of resources. But:
|
|
10
|
+
|
|
11
|
+
- **New users** face 500+ markdown files with no map
|
|
12
|
+
- **Active developers** burn tokens every morning asking the agent "where did I leave off?"
|
|
13
|
+
- **Non-technical stakeholders** can't see project progress without opening a terminal
|
|
14
|
+
- **Solo devs** don't know half the capabilities BMAD offers
|
|
15
|
+
|
|
16
|
+
bmad-viewer fixes all of that with a single command.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
**From Claude Code** — just type:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
/viewer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The package includes a `/viewer` slash command that auto-installs as a skill. Claude launches the dashboard for you in the background.
|
|
27
|
+
|
|
28
|
+
**From terminal:**
|
|
15
29
|
|
|
16
30
|
```bash
|
|
17
|
-
# Run in a BMAD project directory
|
|
18
31
|
npx bmad-viewer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Auto-detects your `_bmad/` folder, opens your browser, dashboard ready.
|
|
35
|
+
|
|
36
|
+
## What you get
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
**Wiki / Catalog** — Browse all BMAD modules (Core, BMB, BMM, CIS) with a navigable sidebar. Click any agent or workflow to read its full description rendered as clean HTML.
|
|
39
|
+
|
|
40
|
+
**Project Viewer** — Sprint status at a glance: stats boxes + kanban columns (Pending → In Progress → Done). Reads directly from your `sprint-status.yaml`. Auto-refreshes when files change on disk.
|
|
41
|
+
|
|
42
|
+
**Fuzzy Search (Ctrl+K)** — Find any agent, workflow, or tool instantly. Tolerates typos. You type "brainstrom", you get "brainstorming".
|
|
43
|
+
|
|
44
|
+
**Dark/Light theme** — Respects your OS preference, toggle manually anytime. Persists across sessions.
|
|
45
|
+
|
|
46
|
+
## Who is this for
|
|
47
|
+
|
|
48
|
+
| You are... | You get... |
|
|
49
|
+
|------------|-----------|
|
|
50
|
+
| New to BMAD | A visual map of everything BMAD offers — no more guessing which agent does what |
|
|
51
|
+
| A developer mid-sprint | Sprint dashboard on your second monitor, always up to date, zero tokens spent |
|
|
52
|
+
| A solo dev | Discover workflows you didn't know existed via search |
|
|
53
|
+
| A stakeholder | A shareable link with project progress — no terminal needed |
|
|
54
|
+
| An open source contributor | A complete catalog to understand how BMAD fits together |
|
|
55
|
+
|
|
56
|
+
## CLI options
|
|
22
57
|
|
|
23
|
-
# Custom port
|
|
24
|
-
npx bmad-viewer --port 8080
|
|
25
58
|
```
|
|
59
|
+
bmad-viewer [options]
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--port <number> Custom port (default: auto-detect from 3000)
|
|
63
|
+
--path <directory> Path to BMAD project (default: auto-detect _bmad/ in cwd)
|
|
64
|
+
--output <directory> Generate static HTMLs (no server)
|
|
65
|
+
--no-open Don't open browser automatically
|
|
66
|
+
--version Show version
|
|
67
|
+
--help Show help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
|
|
72
|
+
bmad-viewer reads the files BMAD already generates — CSV manifests, YAML configs, Markdown docs — and renders them as a local HTML dashboard. The filesystem is the database. No backend, no data entry, no sync.
|
|
73
|
+
|
|
74
|
+
When you edit a file, a file watcher detects the change and pushes an update to your browser via WebSocket. The dashboard refreshes automatically.
|
|
26
75
|
|
|
27
76
|
## Requirements
|
|
28
77
|
|
|
29
78
|
- Node.js 18+ (LTS)
|
|
79
|
+
- A project with BMAD installed (`_bmad/` folder)
|
|
30
80
|
|
|
31
|
-
##
|
|
81
|
+
## Try without a BMAD project
|
|
32
82
|
|
|
33
83
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
84
|
+
npx bmad-viewer --path ./node_modules/bmad-viewer/example-data
|
|
85
|
+
```
|
|
36
86
|
|
|
37
|
-
|
|
38
|
-
npm test
|
|
87
|
+
Bundled example data lets you explore the dashboard without a real project.
|
|
39
88
|
|
|
40
|
-
|
|
41
|
-
npm run lint
|
|
89
|
+
## Contributing
|
|
42
90
|
|
|
43
|
-
|
|
44
|
-
npm run format
|
|
45
|
-
```
|
|
91
|
+
Issues and PRs welcome at [github.com/CamiloValderramaGonzalez/bmad-viewer](https://github.com/CamiloValderramaGonzalez/bmad-viewer).
|
|
46
92
|
|
|
47
93
|
## License
|
|
48
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmad-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Visual dashboard for BMAD (Boring Maintainable Agile Development) projects. Wiki browser + sprint status viewer with live reload.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"chokidar": "^4.0.1",
|
|
31
31
|
"fuse.js": "^7.0.0",
|
|
32
32
|
"js-yaml": "^4.1.0",
|
|
33
|
-
"marked": "^15.0.4"
|
|
33
|
+
"marked": "^15.0.4",
|
|
34
|
+
"ws": "^8.19.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@biomejs/biome": "^1.9.4"
|
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { dirname } from 'node:path';
|
|
6
6
|
import { getMimeType } from './mime-types.js';
|
|
7
7
|
import { findAvailablePort } from './port-finder.js';
|
|
8
|
-
import {
|
|
8
|
+
import { attachWebSocket, broadcastChange } from './websocket.js';
|
|
9
9
|
import { createFileWatcher } from '../watchers/file-watcher.js';
|
|
10
10
|
import { buildDataModel } from '../data/data-model.js';
|
|
11
11
|
import { renderDashboard } from './renderer.js';
|
|
@@ -30,7 +30,7 @@ export async function startServer({ port, bmadDir, open }) {
|
|
|
30
30
|
const pathname = url.pathname;
|
|
31
31
|
|
|
32
32
|
// Security headers
|
|
33
|
-
res.setHeader('Content-Security-Policy', "default-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
|
33
|
+
res.setHeader('Content-Security-Policy', "default-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:* ws://127.0.0.1:*");
|
|
34
34
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
35
35
|
|
|
36
36
|
// API endpoint: get fresh HTML
|
|
@@ -60,10 +60,8 @@ export async function startServer({ port, bmadDir, open }) {
|
|
|
60
60
|
res.end(html);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
// WebSocket
|
|
64
|
-
server
|
|
65
|
-
handleUpgrade(req, socket, head);
|
|
66
|
-
});
|
|
63
|
+
// WebSocket server
|
|
64
|
+
attachWebSocket(server);
|
|
67
65
|
|
|
68
66
|
// File watcher for live reload
|
|
69
67
|
const pendingChanges = [];
|
package/src/server/websocket.js
CHANGED
|
@@ -1,110 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
2
|
|
|
3
|
-
/** @type {
|
|
4
|
-
|
|
3
|
+
/** @type {import('ws').WebSocketServer|null} */
|
|
4
|
+
let wss = null;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @param {import('http').IncomingMessage} req
|
|
11
|
-
* @param {import('net').Socket} socket
|
|
12
|
-
* @param {Buffer} head
|
|
7
|
+
* Attach WebSocket server to an existing HTTP server.
|
|
8
|
+
* @param {import('http').Server} server
|
|
13
9
|
*/
|
|
14
|
-
export function
|
|
15
|
-
|
|
16
|
-
if (!key) {
|
|
17
|
-
socket.destroy();
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const acceptKey = createHash('sha1')
|
|
22
|
-
.update(`${key}258EAFA5-E914-47DA-95CA-5AB5DC11CE10`)
|
|
23
|
-
.digest('base64');
|
|
24
|
-
|
|
25
|
-
const responseHeaders = [
|
|
26
|
-
'HTTP/1.1 101 Switching Protocols',
|
|
27
|
-
'Upgrade: websocket',
|
|
28
|
-
'Connection: Upgrade',
|
|
29
|
-
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
30
|
-
'',
|
|
31
|
-
'',
|
|
32
|
-
].join('\r\n');
|
|
10
|
+
export function attachWebSocket(server) {
|
|
11
|
+
wss = new WebSocketServer({ server });
|
|
33
12
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
socket.on('close', () => clients.delete(socket));
|
|
38
|
-
socket.on('error', () => {
|
|
39
|
-
clients.delete(socket);
|
|
40
|
-
socket.destroy();
|
|
13
|
+
wss.on('connection', (ws) => {
|
|
14
|
+
ws.on('error', () => {});
|
|
41
15
|
});
|
|
42
|
-
|
|
43
|
-
// Handle incoming frames (ping/pong, close)
|
|
44
|
-
socket.on('data', (buffer) => {
|
|
45
|
-
const opcode = buffer[0] & 0x0f;
|
|
46
|
-
// Close frame
|
|
47
|
-
if (opcode === 0x08) {
|
|
48
|
-
clients.delete(socket);
|
|
49
|
-
socket.end();
|
|
50
|
-
}
|
|
51
|
-
// Ping frame — respond with pong
|
|
52
|
-
if (opcode === 0x09) {
|
|
53
|
-
const pong = Buffer.alloc(2);
|
|
54
|
-
pong[0] = 0x8a; // FIN + pong opcode
|
|
55
|
-
pong[1] = 0;
|
|
56
|
-
socket.write(pong);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Send a WebSocket text frame to a socket.
|
|
63
|
-
* @param {import('net').Socket} socket
|
|
64
|
-
* @param {string} message
|
|
65
|
-
*/
|
|
66
|
-
function sendFrame(socket, message) {
|
|
67
|
-
const payload = Buffer.from(message, 'utf8');
|
|
68
|
-
const frame = [];
|
|
69
|
-
|
|
70
|
-
// FIN + text opcode
|
|
71
|
-
frame.push(0x81);
|
|
72
|
-
|
|
73
|
-
// Payload length
|
|
74
|
-
if (payload.length < 126) {
|
|
75
|
-
frame.push(payload.length);
|
|
76
|
-
} else if (payload.length < 65536) {
|
|
77
|
-
frame.push(126);
|
|
78
|
-
frame.push((payload.length >> 8) & 0xff);
|
|
79
|
-
frame.push(payload.length & 0xff);
|
|
80
|
-
} else {
|
|
81
|
-
frame.push(127);
|
|
82
|
-
const lenBuf = Buffer.alloc(8);
|
|
83
|
-
lenBuf.writeBigUInt64BE(BigInt(payload.length));
|
|
84
|
-
frame.push(...lenBuf);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const header = Buffer.from(frame);
|
|
88
|
-
socket.write(Buffer.concat([header, payload]));
|
|
89
16
|
}
|
|
90
17
|
|
|
91
18
|
/**
|
|
92
19
|
* Broadcast a file change event to all connected WebSocket clients.
|
|
93
|
-
* @param {string[]} changedPaths
|
|
20
|
+
* @param {string[]} changedPaths
|
|
94
21
|
*/
|
|
95
22
|
export function broadcastChange(changedPaths) {
|
|
23
|
+
if (!wss) return;
|
|
24
|
+
|
|
96
25
|
const message = JSON.stringify({
|
|
97
26
|
type: 'file-changed',
|
|
98
27
|
paths: changedPaths,
|
|
99
28
|
});
|
|
100
29
|
|
|
101
|
-
for (const client of clients) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
sendFrame(client, message);
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
clients.delete(client);
|
|
30
|
+
for (const client of wss.clients) {
|
|
31
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
32
|
+
client.send(message);
|
|
108
33
|
}
|
|
109
34
|
}
|
|
110
35
|
}
|
|
@@ -113,14 +38,9 @@ export function broadcastChange(changedPaths) {
|
|
|
113
38
|
* Close all WebSocket connections.
|
|
114
39
|
*/
|
|
115
40
|
export function closeAllConnections() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
client.end();
|
|
119
|
-
} catch {
|
|
120
|
-
// ignore
|
|
121
|
-
}
|
|
41
|
+
if (wss) {
|
|
42
|
+
wss.close();
|
|
122
43
|
}
|
|
123
|
-
clients.clear();
|
|
124
44
|
}
|
|
125
45
|
|
|
126
46
|
/**
|
|
@@ -128,5 +48,5 @@ export function closeAllConnections() {
|
|
|
128
48
|
* @returns {number}
|
|
129
49
|
*/
|
|
130
50
|
export function getClientCount() {
|
|
131
|
-
return clients.size;
|
|
51
|
+
return wss ? wss.clients.size : 0;
|
|
132
52
|
}
|
|
@@ -16,9 +16,11 @@ export function createFileWatcher(bmadDir, onChange) {
|
|
|
16
16
|
const watcher = chokidar.watch([watchPath, outputPath], {
|
|
17
17
|
persistent: true,
|
|
18
18
|
ignoreInitial: true,
|
|
19
|
+
usePolling: true,
|
|
20
|
+
interval: 500,
|
|
19
21
|
awaitWriteFinish: {
|
|
20
|
-
stabilityThreshold:
|
|
21
|
-
pollInterval:
|
|
22
|
+
stabilityThreshold: 200,
|
|
23
|
+
pollInterval: 100,
|
|
22
24
|
},
|
|
23
25
|
});
|
|
24
26
|
|