bmad-viewer 0.1.7 → 0.2.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/package.json +7 -3
- package/public/client.js +119 -8
- package/public/styles.css +5 -1
- package/src/components/sidebar-nav.js +65 -18
- package/src/data/data-model.js +235 -8
- package/src/server/renderer.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmad-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"bmad",
|
|
46
|
+
"bmad-method",
|
|
47
|
+
"bmad-viewer",
|
|
46
48
|
"agile",
|
|
47
49
|
"dashboard",
|
|
48
50
|
"cli",
|
|
@@ -51,8 +53,10 @@
|
|
|
51
53
|
"sprint",
|
|
52
54
|
"kanban",
|
|
53
55
|
"markdown",
|
|
54
|
-
"project-management"
|
|
56
|
+
"project-management",
|
|
57
|
+
"claude-code",
|
|
58
|
+
"ai-development"
|
|
55
59
|
],
|
|
56
|
-
"author": "",
|
|
60
|
+
"author": "Camilo Valderrama",
|
|
57
61
|
"license": "MIT"
|
|
58
62
|
}
|
package/public/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
var contentMap = window.__BMAD_CONTENT__ || {};
|
|
6
6
|
var wikiWelcomeHtml = '';
|
|
7
7
|
var wikiBreadcrumbHtml = '';
|
|
8
|
+
var pendingHighlight = null;
|
|
8
9
|
|
|
9
10
|
/* ── Hash Router ── */
|
|
10
11
|
function parseHash() {
|
|
@@ -82,6 +83,11 @@
|
|
|
82
83
|
});
|
|
83
84
|
breadcrumb.innerHTML = crumbs.join(' <span class="breadcrumb__sep">›</span> ');
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
if (pendingHighlight) {
|
|
88
|
+
highlightAndScroll(contentBody, pendingHighlight);
|
|
89
|
+
pendingHighlight = null;
|
|
90
|
+
}
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
function loadProjectContent(id) {
|
|
@@ -106,6 +112,62 @@
|
|
|
106
112
|
' <span class="breadcrumb__sep">›</span> ' +
|
|
107
113
|
'<span class="breadcrumb__current">' + escapeText(item.name) + '</span>';
|
|
108
114
|
}
|
|
115
|
+
|
|
116
|
+
if (pendingHighlight) {
|
|
117
|
+
highlightAndScroll(contentBody, pendingHighlight);
|
|
118
|
+
pendingHighlight = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function highlightAndScroll(container, query) {
|
|
123
|
+
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
|
|
124
|
+
var q = query.toLowerCase();
|
|
125
|
+
var firstMark = null;
|
|
126
|
+
|
|
127
|
+
var nodesToProcess = [];
|
|
128
|
+
while (walker.nextNode()) {
|
|
129
|
+
var node = walker.currentNode;
|
|
130
|
+
if (node.nodeValue && node.nodeValue.toLowerCase().includes(q)) {
|
|
131
|
+
nodesToProcess.push(node);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (var i = 0; i < nodesToProcess.length; i++) {
|
|
136
|
+
var textNode = nodesToProcess[i];
|
|
137
|
+
var text = textNode.nodeValue;
|
|
138
|
+
var idx = text.toLowerCase().indexOf(q);
|
|
139
|
+
if (idx === -1) continue;
|
|
140
|
+
|
|
141
|
+
var before = text.substring(0, idx);
|
|
142
|
+
var match = text.substring(idx, idx + query.length);
|
|
143
|
+
var after = text.substring(idx + query.length);
|
|
144
|
+
|
|
145
|
+
var mark = document.createElement('mark');
|
|
146
|
+
mark.className = 'search-highlight';
|
|
147
|
+
mark.textContent = match;
|
|
148
|
+
|
|
149
|
+
var parent = textNode.parentNode;
|
|
150
|
+
if (before) parent.insertBefore(document.createTextNode(before), textNode);
|
|
151
|
+
parent.insertBefore(mark, textNode);
|
|
152
|
+
if (after) parent.insertBefore(document.createTextNode(after), textNode);
|
|
153
|
+
parent.removeChild(textNode);
|
|
154
|
+
|
|
155
|
+
if (!firstMark) firstMark = mark;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (firstMark) {
|
|
159
|
+
setTimeout(function () {
|
|
160
|
+
firstMark.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
161
|
+
}, 50);
|
|
162
|
+
|
|
163
|
+
// Clear highlights after 4 seconds
|
|
164
|
+
setTimeout(function () {
|
|
165
|
+
container.querySelectorAll('mark.search-highlight').forEach(function (el) {
|
|
166
|
+
var txt = document.createTextNode(el.textContent);
|
|
167
|
+
el.parentNode.replaceChild(txt, el);
|
|
168
|
+
});
|
|
169
|
+
}, 4000);
|
|
170
|
+
}
|
|
109
171
|
}
|
|
110
172
|
|
|
111
173
|
function showWikiWelcome() {
|
|
@@ -190,6 +252,18 @@
|
|
|
190
252
|
if (modal) modal.hidden = true;
|
|
191
253
|
}
|
|
192
254
|
|
|
255
|
+
// Cache stripped text for content search
|
|
256
|
+
var textCache = {};
|
|
257
|
+
function getPlainText(item) {
|
|
258
|
+
if (!item._cacheKey) item._cacheKey = item.name + '|' + item.type;
|
|
259
|
+
if (textCache[item._cacheKey] !== undefined) return textCache[item._cacheKey];
|
|
260
|
+
var tmp = document.createElement('div');
|
|
261
|
+
tmp.innerHTML = item.html || '';
|
|
262
|
+
var text = (tmp.textContent || tmp.innerText || '').toLowerCase();
|
|
263
|
+
textCache[item._cacheKey] = text;
|
|
264
|
+
return text;
|
|
265
|
+
}
|
|
266
|
+
|
|
193
267
|
function handleSearch(query) {
|
|
194
268
|
var results = document.getElementById('search-results');
|
|
195
269
|
if (!results) return;
|
|
@@ -202,20 +276,21 @@
|
|
|
202
276
|
var q = query.toLowerCase();
|
|
203
277
|
var matches = [];
|
|
204
278
|
|
|
205
|
-
// Search through content map
|
|
279
|
+
// Search through content map — name, type, module, group, and body content
|
|
206
280
|
for (var id in contentMap) {
|
|
207
281
|
var item = contentMap[id];
|
|
208
282
|
var nameMatch = (item.name || '').toLowerCase().includes(q);
|
|
209
283
|
var typeMatch = (item.type || '').toLowerCase().includes(q);
|
|
210
284
|
var moduleMatch = (item.module || '').toLowerCase().includes(q);
|
|
211
285
|
var groupMatch = (item.group || '').toLowerCase().includes(q);
|
|
286
|
+
var contentMatch = !nameMatch && !typeMatch && !moduleMatch && !groupMatch && getPlainText(item).includes(q);
|
|
212
287
|
|
|
213
|
-
if (nameMatch || typeMatch || moduleMatch || groupMatch) {
|
|
214
|
-
matches.push({ id: id, item: item, score: nameMatch ?
|
|
288
|
+
if (nameMatch || typeMatch || moduleMatch || groupMatch || contentMatch) {
|
|
289
|
+
matches.push({ id: id, item: item, score: nameMatch ? 2 : (contentMatch ? 0 : 1), contentMatch: contentMatch });
|
|
215
290
|
}
|
|
216
291
|
}
|
|
217
292
|
|
|
218
|
-
// Sort
|
|
293
|
+
// Sort: name matches first, then metadata, then content
|
|
219
294
|
matches.sort(function (a, b) { return b.score - a.score; });
|
|
220
295
|
|
|
221
296
|
if (matches.length === 0) {
|
|
@@ -233,15 +308,45 @@
|
|
|
233
308
|
grouped[type].push(m);
|
|
234
309
|
});
|
|
235
310
|
|
|
311
|
+
var categoryLabels = {
|
|
312
|
+
'planning': 'Planning', 'research': 'Research', 'analysis': 'Analysis',
|
|
313
|
+
'test-arch': 'Test Architecture', 'cis': 'CIS Sessions',
|
|
314
|
+
'bmb-creation': 'BMB Creations', 'diagram': 'Diagrams',
|
|
315
|
+
'story': 'Stories', 'agent': 'Agents', 'workflow': 'Workflows',
|
|
316
|
+
'other': 'Other'
|
|
317
|
+
};
|
|
318
|
+
|
|
236
319
|
var html = '';
|
|
237
320
|
for (var type in grouped) {
|
|
238
|
-
|
|
321
|
+
var groupLabel = categoryLabels[type] || (type.charAt(0).toUpperCase() + type.slice(1) + 's');
|
|
322
|
+
html += '<div class="search-modal__group-label">' + escapeText(groupLabel) + '</div>';
|
|
239
323
|
grouped[type].forEach(function (m) {
|
|
240
324
|
var view = m.id.startsWith('artifact/') || m.id.startsWith('story/') ? 'project' : 'wiki';
|
|
325
|
+
var subtitle = m.item.module
|
|
326
|
+
? (m.item.module + (m.item.group ? ' > ' + m.item.group : ''))
|
|
327
|
+
: (categoryLabels[m.item.type] || m.item.type || '');
|
|
328
|
+
var snippet = '';
|
|
329
|
+
if (m.contentMatch) {
|
|
330
|
+
var text = getPlainText(m.item);
|
|
331
|
+
var idx = text.indexOf(q);
|
|
332
|
+
if (idx !== -1) {
|
|
333
|
+
var start = Math.max(0, idx - 30);
|
|
334
|
+
var end = Math.min(text.length, idx + q.length + 50);
|
|
335
|
+
var raw = text.substring(start, end).replace(/\s+/g, ' ');
|
|
336
|
+
snippet = '<span class="search-modal__result-snippet">' +
|
|
337
|
+
(start > 0 ? '...' : '') +
|
|
338
|
+
escapeText(raw.substring(0, idx - start)) +
|
|
339
|
+
'<mark>' + escapeText(raw.substring(idx - start, idx - start + q.length)) + '</mark>' +
|
|
340
|
+
escapeText(raw.substring(idx - start + q.length)) +
|
|
341
|
+
(end < text.length ? '...' : '') +
|
|
342
|
+
'</span>';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
241
345
|
html +=
|
|
242
|
-
'<a class="search-modal__result" href="#' + view + '/' + m.id + '">' +
|
|
346
|
+
'<a class="search-modal__result" href="#' + view + '/' + m.id + '"' + (m.contentMatch ? ' data-content-match="1"' : '') + '>' +
|
|
243
347
|
'<span class="search-modal__result-title">' + escapeText(m.item.name) + '</span>' +
|
|
244
|
-
'<span class="search-modal__result-type">' + escapeText(
|
|
348
|
+
'<span class="search-modal__result-type">' + escapeText(subtitle) + '</span>' +
|
|
349
|
+
snippet +
|
|
245
350
|
'</a>';
|
|
246
351
|
});
|
|
247
352
|
}
|
|
@@ -367,7 +472,13 @@
|
|
|
367
472
|
if (searchResults) {
|
|
368
473
|
searchResults.addEventListener('click', function (e) {
|
|
369
474
|
var result = e.target.closest('.search-modal__result');
|
|
370
|
-
if (result)
|
|
475
|
+
if (result) {
|
|
476
|
+
if (result.dataset.contentMatch) {
|
|
477
|
+
var input = document.getElementById('search-input');
|
|
478
|
+
pendingHighlight = input ? input.value : null;
|
|
479
|
+
}
|
|
480
|
+
closeSearch();
|
|
481
|
+
}
|
|
371
482
|
});
|
|
372
483
|
}
|
|
373
484
|
|
package/public/styles.css
CHANGED
|
@@ -136,10 +136,14 @@ body{font-family:var(--font-family);background:var(--bg);color:var(--text);line-
|
|
|
136
136
|
.search-modal__input{width:100%;padding:16px 20px;border:none;border-bottom:1px solid var(--border);background:transparent;font-size:1.1rem;color:var(--text);outline:none;font-family:var(--font-family)}
|
|
137
137
|
.search-modal__results{max-height:300px;overflow-y:auto}
|
|
138
138
|
.search-modal__group-label{padding:8px 20px 4px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted)}
|
|
139
|
-
.search-modal__result{display:flex;align-items:
|
|
139
|
+
.search-modal__result{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;padding:10px 20px;cursor:pointer;text-decoration:none;color:var(--text);gap:2px 8px}
|
|
140
140
|
.search-modal__result:hover{background:var(--accent-soft)}
|
|
141
141
|
.search-modal__result-title{font-weight:500}
|
|
142
142
|
.search-modal__result-type{font-size:.75rem;color:var(--text-muted)}
|
|
143
|
+
.search-modal__result-snippet{width:100%;font-size:.75rem;color:var(--text-muted);line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
144
|
+
.search-modal__result-snippet mark{background:rgba(245,158,11,.3);color:var(--text);border-radius:2px;padding:0 1px}
|
|
145
|
+
mark.search-highlight{background:rgba(245,158,11,.4);color:var(--text);border-radius:3px;padding:1px 2px;animation:highlight-fade 4s ease-out forwards}
|
|
146
|
+
@keyframes highlight-fade{0%,70%{background:rgba(245,158,11,.4)}100%{background:transparent}}
|
|
143
147
|
.search-modal__no-results{padding:20px;text-align:center;color:var(--text-muted);font-size:.9rem}
|
|
144
148
|
.search-modal__footer{display:flex;gap:16px;padding:10px 20px;border-top:1px solid var(--border);font-size:.75rem;color:var(--text-muted)}
|
|
145
149
|
/* Warning Banner */
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { escapeHtml } from '../utils/html-escape.js';
|
|
2
2
|
|
|
3
|
+
const CATEGORY_ORDER = [
|
|
4
|
+
{ key: 'planning', label: 'Planning', icon: '📋' },
|
|
5
|
+
{ key: 'research', label: 'Research', icon: '🔎' },
|
|
6
|
+
{ key: 'analysis', label: 'Analysis', icon: '💡' },
|
|
7
|
+
{ key: 'test-arch', label: 'Test Architecture', icon: '🔧' },
|
|
8
|
+
{ key: 'cis', label: 'CIS Sessions', icon: '✨' },
|
|
9
|
+
{ key: 'bmb-creation', label: 'BMB Creations', icon: '🔨' },
|
|
10
|
+
{ key: 'diagram', label: 'Diagrams', icon: '📊' },
|
|
11
|
+
{ key: 'other', label: 'Other', icon: '📄' },
|
|
12
|
+
];
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* Render sidebar navigation tree.
|
|
5
16
|
* Wiki lens: Modules > Groups > Items
|
|
6
|
-
* Project lens: Epics with stories +
|
|
17
|
+
* Project lens: Epics with stories + Artifact categories
|
|
7
18
|
*
|
|
8
|
-
* @param {{modules: Array, artifacts: Array, epics: Array}} props
|
|
19
|
+
* @param {{modules: Array, artifacts: Array, epics: Array, artifactGroups: object}} props
|
|
9
20
|
* @returns {string} HTML string
|
|
10
21
|
*/
|
|
11
|
-
export function SidebarNav({ modules, artifacts, epics }) {
|
|
22
|
+
export function SidebarNav({ modules, artifacts, epics, artifactGroups }) {
|
|
12
23
|
// Wiki sidebar content
|
|
13
24
|
const modulesList = (modules || [])
|
|
14
25
|
.map(
|
|
@@ -46,7 +57,7 @@ export function SidebarNav({ modules, artifacts, epics }) {
|
|
|
46
57
|
)
|
|
47
58
|
.join('\n');
|
|
48
59
|
|
|
49
|
-
// Project sidebar: Sprint Dashboard link + Epics with stories + Artifacts
|
|
60
|
+
// Project sidebar: Sprint Dashboard link + Epics with stories + Categorized Artifacts
|
|
50
61
|
const epicsList = (epics || [])
|
|
51
62
|
.map(
|
|
52
63
|
(epic) => `
|
|
@@ -74,16 +85,36 @@ export function SidebarNav({ modules, artifacts, epics }) {
|
|
|
74
85
|
)
|
|
75
86
|
.join('\n');
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
// Build categorized artifact sections
|
|
89
|
+
const groups = artifactGroups || {};
|
|
90
|
+
const categorySections = CATEGORY_ORDER
|
|
91
|
+
.filter((cat) => groups[cat.key] && groups[cat.key].length > 0)
|
|
92
|
+
.map((cat) => {
|
|
93
|
+
const items = groups[cat.key];
|
|
94
|
+
const itemsHtml = items
|
|
95
|
+
.map(
|
|
96
|
+
(art) => `
|
|
97
|
+
<li class="sidebar-nav__item">
|
|
98
|
+
<a href="#project/${escapeHtml(art.id)}" class="sidebar-nav__link sidebar-nav__link--artifact" data-id="${escapeHtml(art.id)}">
|
|
99
|
+
<span class="sidebar-nav__type-icon">${getArtifactIcon(art.name, cat.key)}</span>
|
|
100
|
+
${escapeHtml(art.name || 'Untitled')}
|
|
101
|
+
</a>
|
|
102
|
+
</li>`,
|
|
103
|
+
)
|
|
104
|
+
.join('\n');
|
|
105
|
+
|
|
106
|
+
return `
|
|
107
|
+
<li class="sidebar-nav__module">
|
|
108
|
+
<button class="sidebar-nav__toggle" aria-expanded="false">
|
|
109
|
+
<span class="sidebar-nav__arrow">▸</span>
|
|
110
|
+
<span class="sidebar-nav__type-icon">${cat.icon}</span>
|
|
111
|
+
${escapeHtml(cat.label)} <span class="sidebar-nav__count">${items.length}</span>
|
|
112
|
+
</button>
|
|
113
|
+
<ul class="sidebar-nav__items" hidden>
|
|
114
|
+
${itemsHtml}
|
|
115
|
+
</ul>
|
|
116
|
+
</li>`;
|
|
117
|
+
})
|
|
87
118
|
.join('\n');
|
|
88
119
|
|
|
89
120
|
return `<nav class="sidebar-nav" aria-label="BMAD Navigation">
|
|
@@ -99,8 +130,8 @@ export function SidebarNav({ modules, artifacts, epics }) {
|
|
|
99
130
|
</a>
|
|
100
131
|
${epicsList ? `<h2 class="sidebar-nav__heading">Epics</h2>
|
|
101
132
|
<ul class="sidebar-nav__list">${epicsList}</ul>` : ''}
|
|
102
|
-
${
|
|
103
|
-
<ul class="sidebar-nav__list">${
|
|
133
|
+
${categorySections ? `<h2 class="sidebar-nav__heading">Artifacts</h2>
|
|
134
|
+
<ul class="sidebar-nav__list">${categorySections}</ul>` : ''}
|
|
104
135
|
</div>
|
|
105
136
|
</nav>`;
|
|
106
137
|
}
|
|
@@ -118,6 +149,12 @@ function getTypeIcon(type) {
|
|
|
118
149
|
case 'story': return '📋';
|
|
119
150
|
case 'data': return '📊';
|
|
120
151
|
case 'testarch': return '🔧';
|
|
152
|
+
case 'research': return '🔎';
|
|
153
|
+
case 'analysis': return '💡';
|
|
154
|
+
case 'test-arch': return '🔧';
|
|
155
|
+
case 'cis': return '✨';
|
|
156
|
+
case 'bmb-creation': return '🔨';
|
|
157
|
+
case 'diagram': return '📊';
|
|
121
158
|
default: return '📄';
|
|
122
159
|
}
|
|
123
160
|
}
|
|
@@ -134,9 +171,19 @@ function getEpicStatusIcon(status) {
|
|
|
134
171
|
}
|
|
135
172
|
|
|
136
173
|
/**
|
|
137
|
-
* Get icon for artifact by name.
|
|
174
|
+
* Get icon for artifact by name and category.
|
|
138
175
|
*/
|
|
139
|
-
function getArtifactIcon(name) {
|
|
176
|
+
function getArtifactIcon(name, category) {
|
|
177
|
+
if (category) {
|
|
178
|
+
switch (category) {
|
|
179
|
+
case 'research': return '🔎';
|
|
180
|
+
case 'analysis': return '💡';
|
|
181
|
+
case 'test-arch': return '🔧';
|
|
182
|
+
case 'cis': return '✨';
|
|
183
|
+
case 'bmb-creation': return '🔨';
|
|
184
|
+
case 'diagram': return '📊';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
140
187
|
const lower = (name || '').toLowerCase();
|
|
141
188
|
if (lower.includes('prd')) return '📜';
|
|
142
189
|
if (lower.includes('architecture')) return '🏗';
|
package/src/data/data-model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdirSync, existsSync, statSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, extname, basename } from 'node:path';
|
|
2
|
+
import { join, extname, basename, relative } from 'node:path';
|
|
3
3
|
import { parseYaml } from '../parsers/parse-yaml.js';
|
|
4
4
|
import { parseMarkdownContent } from '../parsers/parse-markdown.js';
|
|
5
5
|
import { ErrorAggregator } from '../utils/error-aggregator.js';
|
|
@@ -257,22 +257,27 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
// Scan planning artifacts
|
|
260
|
+
// Scan planning artifacts (recursive to catch research/ subdir, and include .html files)
|
|
261
261
|
const planningDir = join(outputPath, 'planning-artifacts');
|
|
262
262
|
if (existsSync(planningDir)) {
|
|
263
|
-
const files =
|
|
263
|
+
const files = scanFilesRecursive(planningDir, ['.md', '.html']);
|
|
264
264
|
for (const file of files) {
|
|
265
|
-
const
|
|
266
|
-
const
|
|
265
|
+
const ext = extname(file).toLowerCase();
|
|
266
|
+
const name = basename(file, ext);
|
|
267
|
+
const content = ext === '.html'
|
|
268
|
+
? readHtmlSafe(file)
|
|
269
|
+
: readMarkdownSafe(file, aggregator);
|
|
270
|
+
const type = categorizeArtifact(file, outputPath);
|
|
267
271
|
project.artifacts.push({
|
|
268
272
|
id: `artifact/${name}`,
|
|
269
273
|
name: formatName(name),
|
|
270
274
|
path: file,
|
|
275
|
+
type,
|
|
271
276
|
...content,
|
|
272
277
|
});
|
|
273
278
|
|
|
274
279
|
// Parse stories from epics.md
|
|
275
|
-
if (name === 'epics' && content.raw) {
|
|
280
|
+
if (name === 'epics' && ext === '.md' && content.raw) {
|
|
276
281
|
const storyContents = parseStoriesFromEpics(content.raw, aggregator);
|
|
277
282
|
project.storyContents = storyContents;
|
|
278
283
|
}
|
|
@@ -282,7 +287,6 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
282
287
|
// Scan implementation artifact story files (direct .md files in impl dir + stories/ subdir)
|
|
283
288
|
const implDir = join(outputPath, 'implementation-artifacts');
|
|
284
289
|
if (existsSync(implDir)) {
|
|
285
|
-
// Scan direct .md files in implementation-artifacts (story files live here)
|
|
286
290
|
const implFiles = scanDirectMarkdownFiles(implDir);
|
|
287
291
|
for (const file of implFiles) {
|
|
288
292
|
const name = basename(file, '.md');
|
|
@@ -296,7 +300,6 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
296
300
|
});
|
|
297
301
|
}
|
|
298
302
|
|
|
299
|
-
// Also check stories/ subdirectory if it exists
|
|
300
303
|
const storyDir = join(implDir, 'stories');
|
|
301
304
|
if (existsSync(storyDir)) {
|
|
302
305
|
const files = scanDirectMarkdownFiles(storyDir);
|
|
@@ -314,6 +317,84 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
// Scan analysis/ directory
|
|
321
|
+
const analysisDir = join(outputPath, 'analysis');
|
|
322
|
+
if (existsSync(analysisDir)) {
|
|
323
|
+
const files = scanDirectFiles(analysisDir, ['.md']);
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
const name = basename(file, '.md');
|
|
326
|
+
const content = readMarkdownSafe(file, aggregator);
|
|
327
|
+
project.artifacts.push({
|
|
328
|
+
id: `artifact/${name}`,
|
|
329
|
+
name: formatName(name),
|
|
330
|
+
path: file,
|
|
331
|
+
type: 'analysis',
|
|
332
|
+
...content,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Scan excalidraw-diagrams/ directory
|
|
338
|
+
const excalidrawDir = join(outputPath, 'excalidraw-diagrams');
|
|
339
|
+
if (existsSync(excalidrawDir)) {
|
|
340
|
+
const files = scanDirectFiles(excalidrawDir, ['.excalidraw']);
|
|
341
|
+
for (const file of files) {
|
|
342
|
+
const name = basename(file, '.excalidraw');
|
|
343
|
+
const content = readExcalidrawSafe(file);
|
|
344
|
+
project.artifacts.push({
|
|
345
|
+
id: `artifact/${name}`,
|
|
346
|
+
name: formatName(name),
|
|
347
|
+
path: file,
|
|
348
|
+
type: 'diagram',
|
|
349
|
+
...content,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Scan bmb-creations/ directory (recursive, .md + .yaml)
|
|
355
|
+
const bmbDir = join(outputPath, 'bmb-creations');
|
|
356
|
+
if (existsSync(bmbDir)) {
|
|
357
|
+
const files = scanFilesRecursive(bmbDir, ['.md', '.yaml']);
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
const ext = extname(file).toLowerCase();
|
|
360
|
+
const name = basename(file, ext);
|
|
361
|
+
const content = ext === '.yaml'
|
|
362
|
+
? readYamlSafe(file)
|
|
363
|
+
: readMarkdownSafe(file, aggregator);
|
|
364
|
+
project.artifacts.push({
|
|
365
|
+
id: `artifact/${name}`,
|
|
366
|
+
name: formatName(name),
|
|
367
|
+
path: file,
|
|
368
|
+
type: 'bmb-creation',
|
|
369
|
+
...content,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Scan root-level files in _bmad-output/ (CIS sessions, test-arch outputs, etc.)
|
|
375
|
+
const rootFiles = scanDirectFiles(outputPath, ['.md']);
|
|
376
|
+
for (const file of rootFiles) {
|
|
377
|
+
const name = basename(file, '.md');
|
|
378
|
+
const content = readMarkdownSafe(file, aggregator);
|
|
379
|
+
const type = categorizeArtifact(file, outputPath);
|
|
380
|
+
project.artifacts.push({
|
|
381
|
+
id: `artifact/${name}`,
|
|
382
|
+
name: formatName(name),
|
|
383
|
+
path: file,
|
|
384
|
+
type,
|
|
385
|
+
...content,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Build artifactGroups by category (excluding stories which are shown under epics)
|
|
390
|
+
project.artifactGroups = {};
|
|
391
|
+
for (const art of project.artifacts) {
|
|
392
|
+
if (art.type === 'story') continue;
|
|
393
|
+
const cat = art.type || 'other';
|
|
394
|
+
if (!project.artifactGroups[cat]) project.artifactGroups[cat] = [];
|
|
395
|
+
project.artifactGroups[cat].push(art);
|
|
396
|
+
}
|
|
397
|
+
|
|
317
398
|
return project;
|
|
318
399
|
}
|
|
319
400
|
|
|
@@ -448,6 +529,152 @@ function scanDirectMarkdownFiles(dir) {
|
|
|
448
529
|
return files.sort();
|
|
449
530
|
}
|
|
450
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Scan a directory for direct files matching given extensions (non-recursive).
|
|
534
|
+
*/
|
|
535
|
+
function scanDirectFiles(dir, extensions) {
|
|
536
|
+
const files = [];
|
|
537
|
+
try {
|
|
538
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
539
|
+
for (const entry of entries) {
|
|
540
|
+
if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
541
|
+
files.push(join(dir, entry.name));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
// Ignore
|
|
546
|
+
}
|
|
547
|
+
return files.sort();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Recursively scan a directory for files matching given extensions.
|
|
552
|
+
*/
|
|
553
|
+
function scanFilesRecursive(dir, extensions) {
|
|
554
|
+
const files = [];
|
|
555
|
+
try {
|
|
556
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
557
|
+
for (const entry of entries) {
|
|
558
|
+
const fullPath = join(dir, entry.name);
|
|
559
|
+
if (entry.isDirectory()) {
|
|
560
|
+
files.push(...scanFilesRecursive(fullPath, extensions));
|
|
561
|
+
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
562
|
+
files.push(fullPath);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
// Ignore
|
|
567
|
+
}
|
|
568
|
+
return files.sort();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Read an Excalidraw file and produce an HTML viewer using the Excalidraw React component via CDN.
|
|
573
|
+
*/
|
|
574
|
+
function readExcalidrawSafe(filePath) {
|
|
575
|
+
try {
|
|
576
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
577
|
+
const sceneData = JSON.parse(raw);
|
|
578
|
+
|
|
579
|
+
// Escape the JSON for safe embedding in HTML
|
|
580
|
+
const escapedJson = JSON.stringify(sceneData)
|
|
581
|
+
.replace(/&/g, '&')
|
|
582
|
+
.replace(/</g, '<')
|
|
583
|
+
.replace(/>/g, '>')
|
|
584
|
+
.replace(/"/g, '"');
|
|
585
|
+
|
|
586
|
+
const html = `<div class="excalidraw-viewer" style="width:100%;height:70vh;border:1px solid var(--border-color,#ddd);border-radius:8px;overflow:hidden;">
|
|
587
|
+
<iframe style="width:100%;height:100%;border:none;" srcdoc="<!DOCTYPE html>
|
|
588
|
+
<html>
|
|
589
|
+
<head>
|
|
590
|
+
<meta charset='utf-8'>
|
|
591
|
+
<style>
|
|
592
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
593
|
+
html, body, #root { width:100%; height:100%; }
|
|
594
|
+
.excalidraw .App-menu_top .buttonList { display:none; }
|
|
595
|
+
</style>
|
|
596
|
+
</head>
|
|
597
|
+
<body>
|
|
598
|
+
<div id='root'></div>
|
|
599
|
+
<script src='https://unpkg.com/react@18/umd/react.production.min.js'><\/script>
|
|
600
|
+
<script src='https://unpkg.com/react-dom@18/umd/react-dom.production.min.js'><\/script>
|
|
601
|
+
<script src='https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js'><\/script>
|
|
602
|
+
<script>
|
|
603
|
+
var scene = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(sceneData))}"));
|
|
604
|
+
var App = function() {
|
|
605
|
+
return React.createElement(ExcalidrawLib.Excalidraw, {
|
|
606
|
+
initialData: { elements: scene.elements || [], appState: { viewBackgroundColor: scene.appState?.viewBackgroundColor || '#ffffff', theme: 'light' }, files: scene.files || {} },
|
|
607
|
+
viewModeEnabled: true,
|
|
608
|
+
zenModeEnabled: true,
|
|
609
|
+
gridModeEnabled: false,
|
|
610
|
+
UIOptions: { canvasActions: { changeViewBackgroundColor: false, clearCanvas: false, export: false, loadScene: false, saveToActiveFile: false, toggleTheme: false, saveAsImage: false } }
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
|
|
614
|
+
<\/script>
|
|
615
|
+
</body>
|
|
616
|
+
</html>"></iframe></div>`;
|
|
617
|
+
|
|
618
|
+
return { html, frontmatter: null, raw };
|
|
619
|
+
} catch {
|
|
620
|
+
return { html: '<p>Error loading Excalidraw file</p>', frontmatter: null, raw: '' };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Read an HTML file and embed it in an isolated iframe.
|
|
626
|
+
*/
|
|
627
|
+
function readHtmlSafe(filePath) {
|
|
628
|
+
try {
|
|
629
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
630
|
+
// Escape for srcdoc attribute
|
|
631
|
+
const escaped = raw
|
|
632
|
+
.replace(/&/g, '&')
|
|
633
|
+
.replace(/"/g, '"');
|
|
634
|
+
const html = `<div class="html-artifact-viewer" style="width:100%;height:80vh;border:1px solid var(--border-color,#ddd);border-radius:8px;overflow:hidden;">
|
|
635
|
+
<iframe style="width:100%;height:100%;border:none;" srcdoc="${escaped}"></iframe></div>`;
|
|
636
|
+
return { html, frontmatter: null, raw };
|
|
637
|
+
} catch {
|
|
638
|
+
return { html: '<p>Error loading HTML file</p>', frontmatter: null, raw: '' };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Read a YAML file and render it as formatted code block.
|
|
644
|
+
*/
|
|
645
|
+
function readYamlSafe(filePath) {
|
|
646
|
+
try {
|
|
647
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
648
|
+
const escaped = raw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
649
|
+
const html = `<pre><code class="language-yaml">${escaped}</code></pre>`;
|
|
650
|
+
return { html, frontmatter: null, raw };
|
|
651
|
+
} catch {
|
|
652
|
+
return { html: '<p>Error loading YAML file</p>', frontmatter: null, raw: '' };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Categorize an artifact based on its path relative to _bmad-output.
|
|
658
|
+
*/
|
|
659
|
+
function categorizeArtifact(filePath, outputPath) {
|
|
660
|
+
const rel = relative(outputPath, filePath).replace(/\\/g, '/');
|
|
661
|
+
|
|
662
|
+
if (rel.startsWith('planning-artifacts/research/')) return 'research';
|
|
663
|
+
if (rel.startsWith('planning-artifacts/')) return 'planning';
|
|
664
|
+
if (rel.startsWith('implementation-artifacts/')) return 'story';
|
|
665
|
+
if (rel.startsWith('analysis/')) return 'analysis';
|
|
666
|
+
if (rel.startsWith('excalidraw-diagrams/')) return 'diagram';
|
|
667
|
+
if (rel.startsWith('bmb-creations/')) return 'bmb-creation';
|
|
668
|
+
|
|
669
|
+
// Root-level files — categorize by filename prefix
|
|
670
|
+
const name = basename(filePath).toLowerCase();
|
|
671
|
+
if (/^(test-design|test-review|atdd-|automation-|traceability-|gate-decision-|nfr-)/.test(name)) return 'test-arch';
|
|
672
|
+
if (/^(design-thinking-|innovation-strategy-|problem-solution-|story-)/.test(name)) return 'cis';
|
|
673
|
+
if (/^brainstorming-/.test(name)) return 'analysis';
|
|
674
|
+
|
|
675
|
+
return 'other';
|
|
676
|
+
}
|
|
677
|
+
|
|
451
678
|
/**
|
|
452
679
|
* Format a kebab-case name to Title Case.
|
|
453
680
|
*/
|
package/src/server/renderer.js
CHANGED