bmad-viewer 0.2.0 → 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/src/data/data-model.js +154 -40
package/package.json
CHANGED
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, relative } 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
|
|
@@ -511,6 +604,27 @@ function loadConfig(bmadPath, aggregator) {
|
|
|
511
604
|
return config;
|
|
512
605
|
}
|
|
513
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
|
+
|
|
514
628
|
/**
|
|
515
629
|
* Scan a directory for direct .md files (non-recursive).
|
|
516
630
|
*/
|