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 CHANGED
@@ -1,48 +1,94 @@
1
1
  # bmad-viewer
2
2
 
3
- Visual dashboard for BMAD (Boring Maintainable Agile Development) projects.
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
- ## Features
5
+ ![bmad-viewer demo](./docs/demo.gif)
6
6
 
7
- - 📊 Live project dashboard with sprint status visualization
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
- ## Quick Start
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
- # Or specify a custom path
21
- npx bmad-viewer --path /path/to/bmad/project
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
- ## Development
81
+ ## Try without a BMAD project
32
82
 
33
83
  ```bash
34
- # Install dependencies
35
- npm install
84
+ npx bmad-viewer --path ./node_modules/bmad-viewer/example-data
85
+ ```
36
86
 
37
- # Run tests
38
- npm test
87
+ Bundled example data lets you explore the dashboard without a real project.
39
88
 
40
- # Lint code
41
- npm run lint
89
+ ## Contributing
42
90
 
43
- # Format code
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.5",
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 { handleUpgrade, broadcastChange } from './websocket.js';
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 upgrade handler
64
- server.on('upgrade', (req, socket, head) => {
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 = [];
@@ -1,110 +1,35 @@
1
- import { createHash } from 'node:crypto';
1
+ import { WebSocketServer } from 'ws';
2
2
 
3
- /** @type {Set<import('net').Socket>} */
4
- const clients = new Set();
3
+ /** @type {import('ws').WebSocketServer|null} */
4
+ let wss = null;
5
5
 
6
6
  /**
7
- * Handle WebSocket upgrade request manually using Node.js built-in APIs.
8
- * No external dependencies needed.
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 handleUpgrade(req, socket, head) {
15
- const key = req.headers['sec-websocket-key'];
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
- socket.write(responseHeaders);
35
- clients.add(socket);
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 - Array of changed file paths
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
- try {
103
- if (!client.destroyed) {
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
- for (const client of clients) {
117
- try {
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: 100,
21
- pollInterval: 50,
22
+ stabilityThreshold: 200,
23
+ pollInterval: 100,
22
24
  },
23
25
  });
24
26