bmad-viewer 0.1.8 → 0.3.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 +1 -1
- 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 +388 -47
- package/src/server/renderer.js +1 -0
package/package.json
CHANGED
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, dirname } 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';
|
|
@@ -24,65 +24,146 @@ export function buildDataModel(bmadDir) {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Build wiki catalog data by scanning _bmad directory structure.
|
|
27
|
-
*
|
|
27
|
+
* Dynamically discovers modules and supports both:
|
|
28
|
+
* - New structure: SKILL.md as entry point in skill directories
|
|
29
|
+
* - Old structure: agents/ and workflows/ subdirectories with direct .md files
|
|
28
30
|
*/
|
|
29
31
|
function buildWikiData(bmadPath, aggregator) {
|
|
30
32
|
const modules = [];
|
|
31
33
|
const allItems = [];
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
// Dynamically discover module directories (skip underscore-prefixed like _config)
|
|
36
|
+
let moduleDirs = [];
|
|
37
|
+
try {
|
|
38
|
+
const entries = readdirSync(bmadPath, { withFileTypes: true });
|
|
39
|
+
moduleDirs = entries
|
|
40
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('_'))
|
|
41
|
+
.map(e => e.name)
|
|
42
|
+
.sort();
|
|
43
|
+
} catch {
|
|
44
|
+
return { modules, allItems };
|
|
45
|
+
}
|
|
34
46
|
|
|
35
|
-
for (const modName of
|
|
47
|
+
for (const modName of moduleDirs) {
|
|
36
48
|
const modPath = join(bmadPath, modName);
|
|
37
|
-
if (!existsSync(modPath) || !statSync(modPath).isDirectory()) continue;
|
|
38
|
-
|
|
39
49
|
const moduleData = { id: modName, name: modName.toUpperCase(), groups: [] };
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
const groupMap = {};
|
|
51
|
+
|
|
52
|
+
// --- New structure: find SKILL.md files recursively ---
|
|
53
|
+
const skillFiles = findNamedFilesRecursive(modPath, 'SKILL.md');
|
|
54
|
+
const skillDirs = new Set(skillFiles.map(f => dirname(f)));
|
|
55
|
+
|
|
56
|
+
for (const filePath of skillFiles) {
|
|
57
|
+
const rel = relative(modPath, filePath).replace(/\\/g, '/');
|
|
58
|
+
const parts = rel.split('/'); // e.g. ["skill-name","SKILL.md"] or ["category","skill-name","SKILL.md"]
|
|
59
|
+
const skillDirName = parts[parts.length - 2];
|
|
60
|
+
const groupName = parts.length > 2 ? parts[0] : null;
|
|
61
|
+
const groupKey = groupName ?? '__root__';
|
|
62
|
+
const displayGroup = groupName ? formatName(groupName) : 'Skills';
|
|
63
|
+
|
|
64
|
+
const id = `${modName}/${rel.replace('/SKILL.md', '')}`;
|
|
65
|
+
const content = readMarkdownSafe(filePath, aggregator);
|
|
66
|
+
const type = inferSkillType(groupName, skillDirName);
|
|
67
|
+
const item = { id, name: formatName(skillDirName), type, path: filePath, ...content };
|
|
68
|
+
|
|
69
|
+
if (!groupMap[groupKey]) {
|
|
70
|
+
groupMap[groupKey] = { name: displayGroup, type: groupKey === '__root__' ? 'skill' : groupKey, items: [], _sortKey: groupName ?? '' };
|
|
54
71
|
}
|
|
72
|
+
groupMap[groupKey].items.push(item);
|
|
73
|
+
allItems.push(item);
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
// --- New structure: workflow.md files in dirs without SKILL.md ---
|
|
77
|
+
const workflowFiles = findNamedFilesRecursive(modPath, 'workflow.md')
|
|
78
|
+
.filter(f => !skillDirs.has(dirname(f)));
|
|
79
|
+
|
|
80
|
+
for (const filePath of workflowFiles) {
|
|
81
|
+
const rel = relative(modPath, filePath).replace(/\\/g, '/');
|
|
82
|
+
const parts = rel.split('/');
|
|
83
|
+
const skillDirName = parts[parts.length - 2];
|
|
84
|
+
const groupName = parts.length > 2 ? parts[0] : null;
|
|
85
|
+
// Use same groupKey as SKILL.md items so they merge into the same group
|
|
86
|
+
const groupKey = groupName ?? '__root__';
|
|
87
|
+
const displayGroup = groupName ? formatName(groupName) : 'Workflows';
|
|
88
|
+
|
|
89
|
+
const id = `${modName}/${rel.replace('/workflow.md', '')}`;
|
|
90
|
+
const content = readMarkdownSafe(filePath, aggregator);
|
|
91
|
+
const item = { id, name: formatName(skillDirName), type: 'workflow', path: filePath, ...content };
|
|
92
|
+
|
|
93
|
+
if (!groupMap[groupKey]) {
|
|
94
|
+
groupMap[groupKey] = { name: displayGroup, type: 'workflows', items: [], _sortKey: groupName ?? '' };
|
|
64
95
|
}
|
|
96
|
+
groupMap[groupKey].items.push(item);
|
|
97
|
+
allItems.push(item);
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
// --- Old structure (backward compat): only used if new-style SKILL.md scan found nothing ---
|
|
101
|
+
const newStyleFound = skillFiles.length > 0 || workflowFiles.length > 0;
|
|
102
|
+
if (!newStyleFound) {
|
|
103
|
+
// agents/ with direct .md files
|
|
104
|
+
const agentsPath = join(modPath, 'agents');
|
|
105
|
+
if (existsSync(agentsPath) && statSync(agentsPath).isDirectory()) {
|
|
106
|
+
const agentFiles = scanDirectMarkdownFiles(agentsPath);
|
|
107
|
+
if (agentFiles.length > 0) {
|
|
108
|
+
const groupKey = '__legacy_agents__';
|
|
109
|
+
if (!groupMap[groupKey]) {
|
|
110
|
+
groupMap[groupKey] = { name: 'Agents', type: 'agents', items: [], _sortKey: 'agents' };
|
|
111
|
+
}
|
|
112
|
+
for (const filePath of agentFiles) {
|
|
75
113
|
const name = basename(filePath, '.md');
|
|
76
|
-
const id = `${modName}/${
|
|
114
|
+
const id = `${modName}/agents/${name}`;
|
|
77
115
|
const content = readMarkdownSafe(filePath, aggregator);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
116
|
+
const item = { id, name: formatName(name), type: 'agent', path: filePath, ...content };
|
|
117
|
+
groupMap[groupKey].items.push(item);
|
|
118
|
+
allItems.push(item);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// workflows/ directory
|
|
124
|
+
const workflowsPath = join(modPath, 'workflows');
|
|
125
|
+
if (existsSync(workflowsPath) && statSync(workflowsPath).isDirectory()) {
|
|
126
|
+
const workflowItems = scanWorkflows(workflowsPath, modName, aggregator);
|
|
127
|
+
if (workflowItems.length > 0) {
|
|
128
|
+
const groupKey = '__legacy_workflows__';
|
|
129
|
+
if (!groupMap[groupKey]) {
|
|
130
|
+
groupMap[groupKey] = { name: 'Workflows', type: 'workflows', items: [], _sortKey: 'workflows' };
|
|
131
|
+
}
|
|
132
|
+
groupMap[groupKey].items.push(...workflowItems);
|
|
133
|
+
allItems.push(...workflowItems);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// other resource dirs
|
|
138
|
+
const otherDirs = ['tasks', 'resources', 'data', 'teams', 'testarch'];
|
|
139
|
+
for (const dirName of otherDirs) {
|
|
140
|
+
const dirPath = join(modPath, dirName);
|
|
141
|
+
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
|
|
142
|
+
const files = scanDirectMarkdownFiles(dirPath);
|
|
143
|
+
if (files.length > 0) {
|
|
144
|
+
const groupKey = `__legacy_${dirName}__`;
|
|
145
|
+
if (!groupMap[groupKey]) {
|
|
146
|
+
groupMap[groupKey] = { name: formatName(dirName), type: dirName, items: [], _sortKey: dirName };
|
|
147
|
+
}
|
|
148
|
+
for (const filePath of files) {
|
|
149
|
+
const name = basename(filePath, '.md');
|
|
150
|
+
const id = `${modName}/${dirName}/${name}`;
|
|
151
|
+
const content = readMarkdownSafe(filePath, aggregator);
|
|
152
|
+
const item = { id, name: formatName(name), type: dirName, path: filePath, ...content };
|
|
153
|
+
groupMap[groupKey].items.push(item);
|
|
154
|
+
allItems.push(item);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
82
157
|
}
|
|
83
158
|
}
|
|
84
159
|
}
|
|
85
160
|
|
|
161
|
+
// Sort groups alphabetically and attach to module
|
|
162
|
+
moduleData.groups = Object.values(groupMap)
|
|
163
|
+
.filter(g => g.items.length > 0)
|
|
164
|
+
.sort((a, b) => a._sortKey.localeCompare(b._sortKey))
|
|
165
|
+
.map(({ _sortKey, ...g }) => g);
|
|
166
|
+
|
|
86
167
|
if (moduleData.groups.length > 0) {
|
|
87
168
|
modules.push(moduleData);
|
|
88
169
|
}
|
|
@@ -91,6 +172,18 @@ function buildWikiData(bmadPath, aggregator) {
|
|
|
91
172
|
return { modules, allItems };
|
|
92
173
|
}
|
|
93
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Infer the type of a skill based on its group/category name and skill name.
|
|
177
|
+
*/
|
|
178
|
+
function inferSkillType(groupName, skillName) {
|
|
179
|
+
const g = (groupName ?? '').toLowerCase();
|
|
180
|
+
const s = skillName.toLowerCase();
|
|
181
|
+
if (g.includes('agent') || s.includes('agent')) return 'agent';
|
|
182
|
+
if (g.includes('workflow') || g.includes('workflows')) return 'workflow';
|
|
183
|
+
if (g.includes('skill') || g.includes('skills')) return 'skill';
|
|
184
|
+
return 'skill';
|
|
185
|
+
}
|
|
186
|
+
|
|
94
187
|
/**
|
|
95
188
|
* Scan workflows directory. Workflows can be:
|
|
96
189
|
* - Direct .md files
|
|
@@ -257,22 +350,27 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
257
350
|
}
|
|
258
351
|
}
|
|
259
352
|
|
|
260
|
-
// Scan planning artifacts
|
|
353
|
+
// Scan planning artifacts (recursive to catch research/ subdir, and include .html files)
|
|
261
354
|
const planningDir = join(outputPath, 'planning-artifacts');
|
|
262
355
|
if (existsSync(planningDir)) {
|
|
263
|
-
const files =
|
|
356
|
+
const files = scanFilesRecursive(planningDir, ['.md', '.html']);
|
|
264
357
|
for (const file of files) {
|
|
265
|
-
const
|
|
266
|
-
const
|
|
358
|
+
const ext = extname(file).toLowerCase();
|
|
359
|
+
const name = basename(file, ext);
|
|
360
|
+
const content = ext === '.html'
|
|
361
|
+
? readHtmlSafe(file)
|
|
362
|
+
: readMarkdownSafe(file, aggregator);
|
|
363
|
+
const type = categorizeArtifact(file, outputPath);
|
|
267
364
|
project.artifacts.push({
|
|
268
365
|
id: `artifact/${name}`,
|
|
269
366
|
name: formatName(name),
|
|
270
367
|
path: file,
|
|
368
|
+
type,
|
|
271
369
|
...content,
|
|
272
370
|
});
|
|
273
371
|
|
|
274
372
|
// Parse stories from epics.md
|
|
275
|
-
if (name === 'epics' && content.raw) {
|
|
373
|
+
if (name === 'epics' && ext === '.md' && content.raw) {
|
|
276
374
|
const storyContents = parseStoriesFromEpics(content.raw, aggregator);
|
|
277
375
|
project.storyContents = storyContents;
|
|
278
376
|
}
|
|
@@ -282,7 +380,6 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
282
380
|
// Scan implementation artifact story files (direct .md files in impl dir + stories/ subdir)
|
|
283
381
|
const implDir = join(outputPath, 'implementation-artifacts');
|
|
284
382
|
if (existsSync(implDir)) {
|
|
285
|
-
// Scan direct .md files in implementation-artifacts (story files live here)
|
|
286
383
|
const implFiles = scanDirectMarkdownFiles(implDir);
|
|
287
384
|
for (const file of implFiles) {
|
|
288
385
|
const name = basename(file, '.md');
|
|
@@ -296,7 +393,6 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
296
393
|
});
|
|
297
394
|
}
|
|
298
395
|
|
|
299
|
-
// Also check stories/ subdirectory if it exists
|
|
300
396
|
const storyDir = join(implDir, 'stories');
|
|
301
397
|
if (existsSync(storyDir)) {
|
|
302
398
|
const files = scanDirectMarkdownFiles(storyDir);
|
|
@@ -314,6 +410,84 @@ function buildProjectData(outputPath, aggregator) {
|
|
|
314
410
|
}
|
|
315
411
|
}
|
|
316
412
|
|
|
413
|
+
// Scan analysis/ directory
|
|
414
|
+
const analysisDir = join(outputPath, 'analysis');
|
|
415
|
+
if (existsSync(analysisDir)) {
|
|
416
|
+
const files = scanDirectFiles(analysisDir, ['.md']);
|
|
417
|
+
for (const file of files) {
|
|
418
|
+
const name = basename(file, '.md');
|
|
419
|
+
const content = readMarkdownSafe(file, aggregator);
|
|
420
|
+
project.artifacts.push({
|
|
421
|
+
id: `artifact/${name}`,
|
|
422
|
+
name: formatName(name),
|
|
423
|
+
path: file,
|
|
424
|
+
type: 'analysis',
|
|
425
|
+
...content,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Scan excalidraw-diagrams/ directory
|
|
431
|
+
const excalidrawDir = join(outputPath, 'excalidraw-diagrams');
|
|
432
|
+
if (existsSync(excalidrawDir)) {
|
|
433
|
+
const files = scanDirectFiles(excalidrawDir, ['.excalidraw']);
|
|
434
|
+
for (const file of files) {
|
|
435
|
+
const name = basename(file, '.excalidraw');
|
|
436
|
+
const content = readExcalidrawSafe(file);
|
|
437
|
+
project.artifacts.push({
|
|
438
|
+
id: `artifact/${name}`,
|
|
439
|
+
name: formatName(name),
|
|
440
|
+
path: file,
|
|
441
|
+
type: 'diagram',
|
|
442
|
+
...content,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Scan bmb-creations/ directory (recursive, .md + .yaml)
|
|
448
|
+
const bmbDir = join(outputPath, 'bmb-creations');
|
|
449
|
+
if (existsSync(bmbDir)) {
|
|
450
|
+
const files = scanFilesRecursive(bmbDir, ['.md', '.yaml']);
|
|
451
|
+
for (const file of files) {
|
|
452
|
+
const ext = extname(file).toLowerCase();
|
|
453
|
+
const name = basename(file, ext);
|
|
454
|
+
const content = ext === '.yaml'
|
|
455
|
+
? readYamlSafe(file)
|
|
456
|
+
: readMarkdownSafe(file, aggregator);
|
|
457
|
+
project.artifacts.push({
|
|
458
|
+
id: `artifact/${name}`,
|
|
459
|
+
name: formatName(name),
|
|
460
|
+
path: file,
|
|
461
|
+
type: 'bmb-creation',
|
|
462
|
+
...content,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Scan root-level files in _bmad-output/ (CIS sessions, test-arch outputs, etc.)
|
|
468
|
+
const rootFiles = scanDirectFiles(outputPath, ['.md']);
|
|
469
|
+
for (const file of rootFiles) {
|
|
470
|
+
const name = basename(file, '.md');
|
|
471
|
+
const content = readMarkdownSafe(file, aggregator);
|
|
472
|
+
const type = categorizeArtifact(file, outputPath);
|
|
473
|
+
project.artifacts.push({
|
|
474
|
+
id: `artifact/${name}`,
|
|
475
|
+
name: formatName(name),
|
|
476
|
+
path: file,
|
|
477
|
+
type,
|
|
478
|
+
...content,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Build artifactGroups by category (excluding stories which are shown under epics)
|
|
483
|
+
project.artifactGroups = {};
|
|
484
|
+
for (const art of project.artifacts) {
|
|
485
|
+
if (art.type === 'story') continue;
|
|
486
|
+
const cat = art.type || 'other';
|
|
487
|
+
if (!project.artifactGroups[cat]) project.artifactGroups[cat] = [];
|
|
488
|
+
project.artifactGroups[cat].push(art);
|
|
489
|
+
}
|
|
490
|
+
|
|
317
491
|
return project;
|
|
318
492
|
}
|
|
319
493
|
|
|
@@ -430,6 +604,27 @@ function loadConfig(bmadPath, aggregator) {
|
|
|
430
604
|
return config;
|
|
431
605
|
}
|
|
432
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Recursively find all files with a specific filename within a directory.
|
|
609
|
+
*/
|
|
610
|
+
function findNamedFilesRecursive(dir, targetName) {
|
|
611
|
+
const found = [];
|
|
612
|
+
try {
|
|
613
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
614
|
+
for (const entry of entries) {
|
|
615
|
+
const fullPath = join(dir, entry.name);
|
|
616
|
+
if (entry.isDirectory()) {
|
|
617
|
+
found.push(...findNamedFilesRecursive(fullPath, targetName));
|
|
618
|
+
} else if (entry.isFile() && entry.name === targetName) {
|
|
619
|
+
found.push(fullPath);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch {
|
|
623
|
+
// Ignore access errors
|
|
624
|
+
}
|
|
625
|
+
return found.sort();
|
|
626
|
+
}
|
|
627
|
+
|
|
433
628
|
/**
|
|
434
629
|
* Scan a directory for direct .md files (non-recursive).
|
|
435
630
|
*/
|
|
@@ -448,6 +643,152 @@ function scanDirectMarkdownFiles(dir) {
|
|
|
448
643
|
return files.sort();
|
|
449
644
|
}
|
|
450
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Scan a directory for direct files matching given extensions (non-recursive).
|
|
648
|
+
*/
|
|
649
|
+
function scanDirectFiles(dir, extensions) {
|
|
650
|
+
const files = [];
|
|
651
|
+
try {
|
|
652
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
653
|
+
for (const entry of entries) {
|
|
654
|
+
if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
655
|
+
files.push(join(dir, entry.name));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
// Ignore
|
|
660
|
+
}
|
|
661
|
+
return files.sort();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Recursively scan a directory for files matching given extensions.
|
|
666
|
+
*/
|
|
667
|
+
function scanFilesRecursive(dir, extensions) {
|
|
668
|
+
const files = [];
|
|
669
|
+
try {
|
|
670
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
671
|
+
for (const entry of entries) {
|
|
672
|
+
const fullPath = join(dir, entry.name);
|
|
673
|
+
if (entry.isDirectory()) {
|
|
674
|
+
files.push(...scanFilesRecursive(fullPath, extensions));
|
|
675
|
+
} else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
|
|
676
|
+
files.push(fullPath);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
// Ignore
|
|
681
|
+
}
|
|
682
|
+
return files.sort();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Read an Excalidraw file and produce an HTML viewer using the Excalidraw React component via CDN.
|
|
687
|
+
*/
|
|
688
|
+
function readExcalidrawSafe(filePath) {
|
|
689
|
+
try {
|
|
690
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
691
|
+
const sceneData = JSON.parse(raw);
|
|
692
|
+
|
|
693
|
+
// Escape the JSON for safe embedding in HTML
|
|
694
|
+
const escapedJson = JSON.stringify(sceneData)
|
|
695
|
+
.replace(/&/g, '&')
|
|
696
|
+
.replace(/</g, '<')
|
|
697
|
+
.replace(/>/g, '>')
|
|
698
|
+
.replace(/"/g, '"');
|
|
699
|
+
|
|
700
|
+
const html = `<div class="excalidraw-viewer" style="width:100%;height:70vh;border:1px solid var(--border-color,#ddd);border-radius:8px;overflow:hidden;">
|
|
701
|
+
<iframe style="width:100%;height:100%;border:none;" srcdoc="<!DOCTYPE html>
|
|
702
|
+
<html>
|
|
703
|
+
<head>
|
|
704
|
+
<meta charset='utf-8'>
|
|
705
|
+
<style>
|
|
706
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
707
|
+
html, body, #root { width:100%; height:100%; }
|
|
708
|
+
.excalidraw .App-menu_top .buttonList { display:none; }
|
|
709
|
+
</style>
|
|
710
|
+
</head>
|
|
711
|
+
<body>
|
|
712
|
+
<div id='root'></div>
|
|
713
|
+
<script src='https://unpkg.com/react@18/umd/react.production.min.js'><\/script>
|
|
714
|
+
<script src='https://unpkg.com/react-dom@18/umd/react-dom.production.min.js'><\/script>
|
|
715
|
+
<script src='https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js'><\/script>
|
|
716
|
+
<script>
|
|
717
|
+
var scene = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(sceneData))}"));
|
|
718
|
+
var App = function() {
|
|
719
|
+
return React.createElement(ExcalidrawLib.Excalidraw, {
|
|
720
|
+
initialData: { elements: scene.elements || [], appState: { viewBackgroundColor: scene.appState?.viewBackgroundColor || '#ffffff', theme: 'light' }, files: scene.files || {} },
|
|
721
|
+
viewModeEnabled: true,
|
|
722
|
+
zenModeEnabled: true,
|
|
723
|
+
gridModeEnabled: false,
|
|
724
|
+
UIOptions: { canvasActions: { changeViewBackgroundColor: false, clearCanvas: false, export: false, loadScene: false, saveToActiveFile: false, toggleTheme: false, saveAsImage: false } }
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
|
|
728
|
+
<\/script>
|
|
729
|
+
</body>
|
|
730
|
+
</html>"></iframe></div>`;
|
|
731
|
+
|
|
732
|
+
return { html, frontmatter: null, raw };
|
|
733
|
+
} catch {
|
|
734
|
+
return { html: '<p>Error loading Excalidraw file</p>', frontmatter: null, raw: '' };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Read an HTML file and embed it in an isolated iframe.
|
|
740
|
+
*/
|
|
741
|
+
function readHtmlSafe(filePath) {
|
|
742
|
+
try {
|
|
743
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
744
|
+
// Escape for srcdoc attribute
|
|
745
|
+
const escaped = raw
|
|
746
|
+
.replace(/&/g, '&')
|
|
747
|
+
.replace(/"/g, '"');
|
|
748
|
+
const html = `<div class="html-artifact-viewer" style="width:100%;height:80vh;border:1px solid var(--border-color,#ddd);border-radius:8px;overflow:hidden;">
|
|
749
|
+
<iframe style="width:100%;height:100%;border:none;" srcdoc="${escaped}"></iframe></div>`;
|
|
750
|
+
return { html, frontmatter: null, raw };
|
|
751
|
+
} catch {
|
|
752
|
+
return { html: '<p>Error loading HTML file</p>', frontmatter: null, raw: '' };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Read a YAML file and render it as formatted code block.
|
|
758
|
+
*/
|
|
759
|
+
function readYamlSafe(filePath) {
|
|
760
|
+
try {
|
|
761
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
762
|
+
const escaped = raw.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
763
|
+
const html = `<pre><code class="language-yaml">${escaped}</code></pre>`;
|
|
764
|
+
return { html, frontmatter: null, raw };
|
|
765
|
+
} catch {
|
|
766
|
+
return { html: '<p>Error loading YAML file</p>', frontmatter: null, raw: '' };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Categorize an artifact based on its path relative to _bmad-output.
|
|
772
|
+
*/
|
|
773
|
+
function categorizeArtifact(filePath, outputPath) {
|
|
774
|
+
const rel = relative(outputPath, filePath).replace(/\\/g, '/');
|
|
775
|
+
|
|
776
|
+
if (rel.startsWith('planning-artifacts/research/')) return 'research';
|
|
777
|
+
if (rel.startsWith('planning-artifacts/')) return 'planning';
|
|
778
|
+
if (rel.startsWith('implementation-artifacts/')) return 'story';
|
|
779
|
+
if (rel.startsWith('analysis/')) return 'analysis';
|
|
780
|
+
if (rel.startsWith('excalidraw-diagrams/')) return 'diagram';
|
|
781
|
+
if (rel.startsWith('bmb-creations/')) return 'bmb-creation';
|
|
782
|
+
|
|
783
|
+
// Root-level files — categorize by filename prefix
|
|
784
|
+
const name = basename(filePath).toLowerCase();
|
|
785
|
+
if (/^(test-design|test-review|atdd-|automation-|traceability-|gate-decision-|nfr-)/.test(name)) return 'test-arch';
|
|
786
|
+
if (/^(design-thinking-|innovation-strategy-|problem-solution-|story-)/.test(name)) return 'cis';
|
|
787
|
+
if (/^brainstorming-/.test(name)) return 'analysis';
|
|
788
|
+
|
|
789
|
+
return 'other';
|
|
790
|
+
}
|
|
791
|
+
|
|
451
792
|
/**
|
|
452
793
|
* Format a kebab-case name to Title Case.
|
|
453
794
|
*/
|
package/src/server/renderer.js
CHANGED