@lucasviola/specwiki 1.0.1 → 1.0.2
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/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +1 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/output/html/assets/specwiki.css +48 -0
- package/dist/output/html/nav-grouping.d.ts +20 -26
- package/dist/output/html/nav-grouping.d.ts.map +1 -1
- package/dist/output/html/nav-grouping.js +249 -533
- package/dist/output/html/nav-grouping.js.map +1 -1
- package/dist/output/html/renderer.d.ts +24 -0
- package/dist/output/html/renderer.d.ts.map +1 -1
- package/dist/output/html/renderer.js +49 -18
- package/dist/output/html/renderer.js.map +1 -1
- package/dist/output/html/templates/article.mustache +40 -0
- package/dist/output/html/templates/index.mustache +42 -2
- package/dist/output/wiki.d.ts.map +1 -1
- package/dist/output/wiki.js +10 -2
- package/dist/output/wiki.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const BMAD_HELP_PATH = "_bmad/_config/bmad-help.csv";
|
|
4
1
|
const CATEGORY_PATH_PREFIXES = {
|
|
5
|
-
"agent-skills": ".agents/skills/",
|
|
6
|
-
"cursor-skills": ".cursor/skills/",
|
|
7
2
|
"cursor-rules": ".cursor/rules/",
|
|
3
|
+
"cursor-skills": ".cursor/skills/",
|
|
4
|
+
"agent-skills": ".agents/skills/",
|
|
8
5
|
"bmad-output": "_bmad-output/",
|
|
9
6
|
specs: "specs/",
|
|
10
7
|
spec: "spec/",
|
|
@@ -14,595 +11,314 @@ const CATEGORY_PATH_PREFIXES = {
|
|
|
14
11
|
plans: "docs/plans/",
|
|
15
12
|
requirements: "requirements/",
|
|
16
13
|
github: ".github/",
|
|
14
|
+
root: "",
|
|
17
15
|
};
|
|
18
16
|
const FOLDER_LABELS = {
|
|
17
|
+
planning: "Planning",
|
|
19
18
|
"planning-artifacts": "Planning",
|
|
20
19
|
"implementation-artifacts": "Implementation Stories",
|
|
20
|
+
"implementation-stories": "Implementation Stories",
|
|
21
|
+
"epic-context": "Epic Context",
|
|
21
22
|
discovery: "Discovery",
|
|
22
23
|
research: "Research",
|
|
23
|
-
|
|
24
|
-
architecture: "Architecture",
|
|
25
|
-
readiness: "Readiness",
|
|
26
|
-
epics: "Epics",
|
|
27
|
-
};
|
|
28
|
-
const PHASE_GROUPS = {
|
|
29
|
-
"1-analysis": { key: "phase-analysis", label: "Analysis", sortOrder: 1 },
|
|
30
|
-
"2-planning": { key: "phase-planning", label: "Planning", sortOrder: 2 },
|
|
31
|
-
"3-solutioning": {
|
|
32
|
-
key: "phase-solutioning",
|
|
33
|
-
label: "Solutioning",
|
|
34
|
-
sortOrder: 3,
|
|
35
|
-
},
|
|
36
|
-
"4-implementation": {
|
|
37
|
-
key: "phase-implementation",
|
|
38
|
-
label: "Implementation",
|
|
39
|
-
sortOrder: 4,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
const YOUR_TEAM_GROUP = {
|
|
43
|
-
key: "your-team",
|
|
44
|
-
label: "Your team",
|
|
45
|
-
sortOrder: 0,
|
|
46
|
-
};
|
|
47
|
-
const CORE_UTILITIES_GROUP = {
|
|
48
|
-
key: "core-utilities",
|
|
49
|
-
label: "Core utilities",
|
|
50
|
-
sortOrder: 5,
|
|
51
|
-
};
|
|
52
|
-
const DEPRECATED_GROUP = {
|
|
53
|
-
key: "deprecated",
|
|
54
|
-
label: "Deprecated",
|
|
55
|
-
sortOrder: 6,
|
|
24
|
+
other: "Other",
|
|
56
25
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
const PROJECT_DOCS_GROUP = {
|
|
63
|
-
key: "project-docs",
|
|
64
|
-
label: "Project docs",
|
|
65
|
-
sortOrder: 0,
|
|
66
|
-
};
|
|
67
|
-
export async function loadNavGroupingContext(projectRoot) {
|
|
68
|
-
if (!projectRoot) {
|
|
69
|
-
return { bmadCatalog: null };
|
|
70
|
-
}
|
|
71
|
-
const csvPath = path.join(projectRoot, BMAD_HELP_PATH);
|
|
72
|
-
try {
|
|
73
|
-
const csvContent = await fs.readFile(csvPath, "utf-8");
|
|
74
|
-
const catalog = await parseBmadHelpCsv(csvContent, projectRoot);
|
|
75
|
-
return { bmadCatalog: catalog };
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return { bmadCatalog: null };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
export function buildCategoryNavSubgroups(category, pages, context, activePageSlug) {
|
|
82
|
-
const assignments = pages.map((page) => ({
|
|
83
|
-
page,
|
|
84
|
-
assignment: assignPageGroups(page, category, context),
|
|
85
|
-
}));
|
|
86
|
-
const hasGrouping = assignments.some(({ assignment }) => assignment.segments.length > 0);
|
|
87
|
-
if (!hasGrouping) {
|
|
88
|
-
return {
|
|
89
|
-
subgroups: [],
|
|
90
|
-
pages: sortNavPages(assignments.map(({ page, assignment }) => toNavPage(page, assignment.displayTitle))),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
const tree = buildTreeFromAssignments(assignments);
|
|
94
|
-
const flattened = flattenSingletonSubgroups(tree);
|
|
95
|
-
const promoted = promoteSingletonPages(flattened);
|
|
96
|
-
const withState = applyNavSubgroupState(promoted.subgroups, category, activePageSlug);
|
|
97
|
-
if (withState.length === 0 && promoted.pages.length > 0) {
|
|
98
|
-
return {
|
|
99
|
-
subgroups: [],
|
|
100
|
-
pages: sortNavPages(promoted.pages),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
if (withState.length === 1 && withState[0].pages.length === 0) {
|
|
104
|
-
return {
|
|
105
|
-
subgroups: [],
|
|
106
|
-
pages: sortNavPages([
|
|
107
|
-
...promoted.pages,
|
|
108
|
-
...withState[0].subgroups.flatMap(collectPages),
|
|
109
|
-
]),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
26
|
+
const STORY_FILENAME_PATTERN = /^(\d+)-(\d+)-/;
|
|
27
|
+
function storyNumbers(sourcePath) {
|
|
28
|
+
const filename = sourcePath.split("/").pop() ?? sourcePath;
|
|
29
|
+
const match = filename.match(STORY_FILENAME_PATTERN);
|
|
112
30
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
31
|
+
epic: parseInt(match?.[1] ?? "0", 10),
|
|
32
|
+
story: parseInt(match?.[2] ?? "0", 10),
|
|
115
33
|
};
|
|
116
34
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
return assignment.segments.map((segment) => segment.label).join(" › ");
|
|
35
|
+
let subgroupOrderCounter = 0;
|
|
36
|
+
export async function loadNavGroupingContext(_projectRoot) {
|
|
37
|
+
return { loaded: false };
|
|
123
38
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
39
|
+
export function buildCategoryNavSubgroups(pages, options) {
|
|
40
|
+
subgroupOrderCounter = 0;
|
|
41
|
+
const rootPages = [];
|
|
42
|
+
const rootChildren = new Map();
|
|
43
|
+
for (const page of pages) {
|
|
44
|
+
const navPage = toNavPage(page);
|
|
45
|
+
const segments = resolveSubgroupSegments(page.sourcePath, options.categoryKey);
|
|
46
|
+
if (segments.length === 0) {
|
|
47
|
+
rootPages.push(navPage);
|
|
48
|
+
continue;
|
|
129
49
|
}
|
|
50
|
+
insertIntoTree(rootChildren, segments, navPage);
|
|
130
51
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
if (bmadOutputAssignment) {
|
|
134
|
-
return bmadOutputAssignment;
|
|
135
|
-
}
|
|
52
|
+
if (options.categoryKey === "bmad-output") {
|
|
53
|
+
sortImplementationStoryPages(rootChildren);
|
|
136
54
|
}
|
|
137
|
-
|
|
55
|
+
const subgroups = finalizeSubgroups(rootChildren, options);
|
|
56
|
+
const promotedPages = [...rootPages, ...subgroups.rootPromoted];
|
|
57
|
+
const visibleSubgroups = subgroups.subgroups;
|
|
58
|
+
const hasSubgroups = visibleSubgroups.length > 0;
|
|
59
|
+
return {
|
|
60
|
+
pages: promotedPages,
|
|
61
|
+
subgroups: visibleSubgroups,
|
|
62
|
+
hasSubgroups,
|
|
63
|
+
};
|
|
138
64
|
}
|
|
139
|
-
function
|
|
140
|
-
const skillFolder = extractSkillFolder(page.sourcePath);
|
|
141
|
-
if (!skillFolder) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const agent = catalog.agents.get(skillFolder);
|
|
145
|
-
if (agent) {
|
|
146
|
-
const displayTitle = formatAgentTitle(agent);
|
|
147
|
-
return {
|
|
148
|
-
segments: [YOUR_TEAM_GROUP],
|
|
149
|
-
pageSortKey: `00-${agent.name.toLowerCase()}`,
|
|
150
|
-
displayTitle,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
const entry = catalog.skills.get(skillFolder);
|
|
154
|
-
if (!entry) {
|
|
155
|
-
return {
|
|
156
|
-
segments: [UNCATALOGUED_GROUP],
|
|
157
|
-
pageSortKey: `99-${page.title.toLowerCase()}`,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (entry.deprecated) {
|
|
161
|
-
return {
|
|
162
|
-
segments: [DEPRECATED_GROUP],
|
|
163
|
-
pageSortKey: `98-${entry.displayName.toLowerCase()}`,
|
|
164
|
-
displayTitle: entry.displayName,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const segment = resolveSkillPhaseGroup(entry);
|
|
65
|
+
function toNavPage(page) {
|
|
168
66
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
67
|
+
title: page.title,
|
|
68
|
+
slug: page.slug,
|
|
69
|
+
href: `${page.slug}.html`,
|
|
70
|
+
sourcePath: page.sourcePath,
|
|
172
71
|
};
|
|
173
72
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
73
|
+
function normalizePath(sourcePath) {
|
|
74
|
+
return sourcePath.replace(/\\/g, "/");
|
|
75
|
+
}
|
|
76
|
+
function resolveSubgroupSegments(sourcePath, categoryKey) {
|
|
77
|
+
const normalized = normalizePath(sourcePath);
|
|
78
|
+
if (categoryKey === "bmad-output") {
|
|
79
|
+
const bmadSegments = resolveBmadOutputSegments(normalized);
|
|
80
|
+
if (bmadSegments !== null) {
|
|
81
|
+
return bmadSegments;
|
|
82
|
+
}
|
|
177
83
|
}
|
|
178
|
-
return
|
|
84
|
+
return resolveL0Segments(normalized, categoryKey);
|
|
179
85
|
}
|
|
180
|
-
function
|
|
181
|
-
const normalized = page.sourcePath.replace(/\\/g, "/");
|
|
86
|
+
function resolveBmadOutputSegments(normalized) {
|
|
182
87
|
if (!normalized.startsWith("_bmad-output/")) {
|
|
183
88
|
return null;
|
|
184
89
|
}
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
label: "Planning",
|
|
193
|
-
sortOrder: 0,
|
|
194
|
-
};
|
|
195
|
-
if (dirParts.length <= 1) {
|
|
196
|
-
return {
|
|
197
|
-
segments: [planning],
|
|
198
|
-
pageSortKey: basename,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
const subKey = dirParts.slice(1).join("/");
|
|
202
|
-
const subLabel = labelForPathSegment(dirParts[1]);
|
|
203
|
-
return {
|
|
204
|
-
segments: [
|
|
205
|
-
planning,
|
|
206
|
-
{
|
|
207
|
-
key: `planning-${subKey}`,
|
|
208
|
-
label: subLabel,
|
|
209
|
-
sortOrder: 0,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
pageSortKey: basename,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
if (remainder.startsWith("implementation-artifacts/")) {
|
|
216
|
-
const implementation = {
|
|
217
|
-
key: "implementation",
|
|
218
|
-
label: "Implementation Stories",
|
|
219
|
-
sortOrder: 1,
|
|
220
|
-
};
|
|
221
|
-
if (basename.startsWith("epic-")) {
|
|
222
|
-
return {
|
|
223
|
-
segments: [
|
|
224
|
-
{
|
|
225
|
-
key: "epic-context",
|
|
226
|
-
label: "Epic Context",
|
|
227
|
-
sortOrder: 2,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
pageSortKey: basename,
|
|
231
|
-
};
|
|
90
|
+
const relative = normalized.slice("_bmad-output/".length);
|
|
91
|
+
const dirParts = relative.split("/");
|
|
92
|
+
const filename = dirParts.pop() ?? "";
|
|
93
|
+
if (dirParts[0] === "planning-artifacts" || dirParts[0] === "planning") {
|
|
94
|
+
if (dirParts[0] === "planning-artifacts") {
|
|
95
|
+
const childKey = dirParts[1] ?? "other";
|
|
96
|
+
return ["planning", childKey];
|
|
232
97
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const epicNumber = epicMatch[1];
|
|
236
|
-
return {
|
|
237
|
-
segments: [
|
|
238
|
-
implementation,
|
|
239
|
-
{
|
|
240
|
-
key: `epic-${epicNumber}`,
|
|
241
|
-
label: `Epic ${epicNumber}`,
|
|
242
|
-
sortOrder: Number.parseInt(epicNumber, 10),
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
pageSortKey: basename,
|
|
246
|
-
};
|
|
98
|
+
if (dirParts.length === 1) {
|
|
99
|
+
return ["planning"];
|
|
247
100
|
}
|
|
248
|
-
return
|
|
249
|
-
segments: [
|
|
250
|
-
implementation,
|
|
251
|
-
{
|
|
252
|
-
key: "other-stories",
|
|
253
|
-
label: "Other Stories",
|
|
254
|
-
sortOrder: 999,
|
|
255
|
-
},
|
|
256
|
-
],
|
|
257
|
-
pageSortKey: basename,
|
|
258
|
-
};
|
|
101
|
+
return ["planning", dirParts[1]];
|
|
259
102
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const normalized = page.sourcePath.replace(/\\/g, "/");
|
|
264
|
-
const prefix = CATEGORY_PATH_PREFIXES[category];
|
|
265
|
-
let remainder = normalized;
|
|
266
|
-
if (prefix && normalized.startsWith(prefix)) {
|
|
267
|
-
remainder = normalized.slice(prefix.length);
|
|
268
|
-
}
|
|
269
|
-
const dirname = path.posix.dirname(remainder);
|
|
270
|
-
if (dirname === ".") {
|
|
271
|
-
if (category === "other" && normalized.includes("/")) {
|
|
272
|
-
const topSegment = normalized.split("/")[0];
|
|
273
|
-
return {
|
|
274
|
-
segments: [
|
|
275
|
-
{
|
|
276
|
-
key: topSegment,
|
|
277
|
-
label: labelForPathSegment(topSegment),
|
|
278
|
-
sortOrder: 0,
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
pageSortKey: normalized,
|
|
282
|
-
};
|
|
103
|
+
if (dirParts[0] === "implementation-artifacts") {
|
|
104
|
+
if (dirParts.length === 1 && filename.startsWith("epic-")) {
|
|
105
|
+
return ["epic-context"];
|
|
283
106
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
};
|
|
107
|
+
const storyMatch = filename.match(STORY_FILENAME_PATTERN);
|
|
108
|
+
if (storyMatch) {
|
|
109
|
+
const epicNum = storyMatch[1];
|
|
110
|
+
return ["implementation-stories", `epic-${epicNum}`];
|
|
289
111
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
112
|
+
if (dirParts.length > 1) {
|
|
113
|
+
return resolveL0Segments(normalized, "bmad-output");
|
|
114
|
+
}
|
|
115
|
+
return ["other"];
|
|
294
116
|
}
|
|
295
|
-
|
|
296
|
-
const segments = dirParts.slice(0, 2).map((part, index) => ({
|
|
297
|
-
key: dirParts.slice(0, index + 1).join("/"),
|
|
298
|
-
label: labelForPathSegment(part),
|
|
299
|
-
sortOrder: 0,
|
|
300
|
-
}));
|
|
301
|
-
return {
|
|
302
|
-
segments,
|
|
303
|
-
pageSortKey: normalized,
|
|
304
|
-
};
|
|
117
|
+
return null;
|
|
305
118
|
}
|
|
306
|
-
function
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
root.pages.push(toNavPage(page, assignment.displayTitle));
|
|
313
|
-
roots.set(rootKey, root);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
let level = roots;
|
|
317
|
-
let parent;
|
|
318
|
-
for (const [index, segment] of assignment.segments.entries()) {
|
|
319
|
-
const existing = level.get(segment.key);
|
|
320
|
-
const node = existing ??
|
|
321
|
-
createMutableSubgroup(segment.key, segment.label, segment.sortOrder);
|
|
322
|
-
if (!existing) {
|
|
323
|
-
level.set(segment.key, node);
|
|
324
|
-
if (parent) {
|
|
325
|
-
parent.children.set(segment.key, node);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (index === assignment.segments.length - 1) {
|
|
329
|
-
node.pages.push(toNavPage(page, assignment.displayTitle));
|
|
330
|
-
node.pageSortKeys.push(assignment.pageSortKey);
|
|
331
|
-
}
|
|
332
|
-
parent = node;
|
|
333
|
-
level = node.children;
|
|
119
|
+
function resolveL0Segments(normalized, categoryKey) {
|
|
120
|
+
const prefix = CATEGORY_PATH_PREFIXES[categoryKey];
|
|
121
|
+
if (categoryKey === "other") {
|
|
122
|
+
const parts = normalized.split("/");
|
|
123
|
+
if (parts.length <= 1) {
|
|
124
|
+
return [];
|
|
334
125
|
}
|
|
126
|
+
return parts.slice(0, Math.min(2, parts.length - 1));
|
|
335
127
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const subgroups = sortMutableSubgroups(topLevel).map(finalizeSubgroup);
|
|
339
|
-
if (rootPages.length > 0) {
|
|
340
|
-
return [
|
|
341
|
-
...subgroups,
|
|
342
|
-
{
|
|
343
|
-
key: "ungrouped",
|
|
344
|
-
label: "Ungrouped",
|
|
345
|
-
anchor: "ungrouped",
|
|
346
|
-
pageCount: rootPages.length,
|
|
347
|
-
collapsible: rootPages.length > 1,
|
|
348
|
-
open: false,
|
|
349
|
-
active: false,
|
|
350
|
-
pages: rootPages,
|
|
351
|
-
subgroups: [],
|
|
352
|
-
hasNestedSubgroups: false,
|
|
353
|
-
},
|
|
354
|
-
];
|
|
128
|
+
if (prefix === undefined) {
|
|
129
|
+
return [];
|
|
355
130
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
key,
|
|
361
|
-
label,
|
|
362
|
-
sortOrder,
|
|
363
|
-
pages: [],
|
|
364
|
-
pageSortKeys: [],
|
|
365
|
-
children: new Map(),
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
function finalizeSubgroup(node) {
|
|
369
|
-
const subgroups = sortMutableSubgroups([...node.children.values()]).map(finalizeSubgroup);
|
|
370
|
-
const pages = sortNavPages(node.pages);
|
|
371
|
-
const pageCount = pages.length + subgroups.reduce((sum, child) => sum + child.pageCount, 0);
|
|
372
|
-
return {
|
|
373
|
-
key: node.key,
|
|
374
|
-
label: node.label,
|
|
375
|
-
anchor: slugify(node.key),
|
|
376
|
-
pageCount,
|
|
377
|
-
collapsible: pageCount > 1 || subgroups.length > 0,
|
|
378
|
-
open: false,
|
|
379
|
-
active: false,
|
|
380
|
-
pages,
|
|
381
|
-
subgroups,
|
|
382
|
-
hasNestedSubgroups: subgroups.length > 0,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
function flattenSingletonSubgroups(subgroups) {
|
|
386
|
-
const flattened = [];
|
|
387
|
-
for (const subgroup of subgroups) {
|
|
388
|
-
const nested = flattenSingletonSubgroups(subgroup.subgroups);
|
|
389
|
-
let current = {
|
|
390
|
-
...subgroup,
|
|
391
|
-
subgroups: nested,
|
|
392
|
-
hasNestedSubgroups: nested.length > 0,
|
|
393
|
-
};
|
|
394
|
-
if (current.subgroups.length === 1 &&
|
|
395
|
-
current.pages.length === 0 &&
|
|
396
|
-
current.subgroups[0]) {
|
|
397
|
-
const child = current.subgroups[0];
|
|
398
|
-
current = {
|
|
399
|
-
...child,
|
|
400
|
-
key: `${current.key}-${child.key}`,
|
|
401
|
-
label: `${current.label} › ${child.label}`,
|
|
402
|
-
anchor: slugify(`${current.key}-${child.key}`),
|
|
403
|
-
};
|
|
131
|
+
if (prefix === "") {
|
|
132
|
+
const parts = normalized.split("/");
|
|
133
|
+
if (parts.length <= 1) {
|
|
134
|
+
return [];
|
|
404
135
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
136
|
+
return parts.slice(0, Math.min(2, parts.length - 1));
|
|
137
|
+
}
|
|
138
|
+
if (!normalized.startsWith(prefix)) {
|
|
139
|
+
return [];
|
|
409
140
|
}
|
|
410
|
-
|
|
141
|
+
const relative = normalized.slice(prefix.length);
|
|
142
|
+
const dirParts = relative.split("/");
|
|
143
|
+
dirParts.pop();
|
|
144
|
+
if (dirParts.length === 0) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return dirParts.slice(0, 2);
|
|
411
148
|
}
|
|
412
|
-
function
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
149
|
+
function insertIntoTree(root, segments, page) {
|
|
150
|
+
const capped = segments.slice(0, 2);
|
|
151
|
+
let current = root;
|
|
152
|
+
let path = [];
|
|
153
|
+
for (let i = 0; i < capped.length; i++) {
|
|
154
|
+
const segment = capped[i];
|
|
155
|
+
path = [...path, segment];
|
|
156
|
+
const key = path.join("/");
|
|
157
|
+
if (!current.has(segment)) {
|
|
158
|
+
current.set(segment, {
|
|
159
|
+
key,
|
|
160
|
+
label: humanizeFolderLabel(segment, path),
|
|
161
|
+
pages: [],
|
|
162
|
+
children: new Map(),
|
|
163
|
+
order: subgroupOrderCounter++,
|
|
164
|
+
});
|
|
423
165
|
}
|
|
424
|
-
|
|
425
|
-
|
|
166
|
+
const node = current.get(segment);
|
|
167
|
+
if (i === capped.length - 1) {
|
|
168
|
+
node.pages.push(page);
|
|
426
169
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
pages,
|
|
430
|
-
subgroups: children,
|
|
431
|
-
pageCount,
|
|
432
|
-
collapsible: pageCount > 1 || children.length > 0,
|
|
433
|
-
hasNestedSubgroups: children.length > 0,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
subgroups: kept,
|
|
438
|
-
pages: promotedPages,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
function applyNavSubgroupState(subgroups, category, activePageSlug) {
|
|
442
|
-
return subgroups.map((subgroup) => {
|
|
443
|
-
const nested = applyNavSubgroupState(subgroup.subgroups, category, activePageSlug);
|
|
444
|
-
const containsActivePage = subgroup.pages.some((page) => page.slug === activePageSlug) ||
|
|
445
|
-
nested.some((child) => child.active || child.open);
|
|
446
|
-
return {
|
|
447
|
-
...subgroup,
|
|
448
|
-
subgroups: nested,
|
|
449
|
-
hasNestedSubgroups: nested.length > 0,
|
|
450
|
-
active: containsActivePage,
|
|
451
|
-
open: containsActivePage,
|
|
452
|
-
};
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
function collectPages(subgroup) {
|
|
456
|
-
return [
|
|
457
|
-
...subgroup.pages,
|
|
458
|
-
...subgroup.subgroups.flatMap((child) => collectPages(child)),
|
|
459
|
-
];
|
|
460
|
-
}
|
|
461
|
-
function sortMutableSubgroups(nodes) {
|
|
462
|
-
return [...nodes].sort((left, right) => {
|
|
463
|
-
if (left.sortOrder !== right.sortOrder) {
|
|
464
|
-
return left.sortOrder - right.sortOrder;
|
|
170
|
+
else {
|
|
171
|
+
current = node.children;
|
|
465
172
|
}
|
|
466
|
-
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
function sortNavPages(pages) {
|
|
470
|
-
return [...pages].sort((left, right) => left.title.localeCompare(right.title));
|
|
471
|
-
}
|
|
472
|
-
function compareNavPages(left, right) {
|
|
473
|
-
return left.title.localeCompare(right.title);
|
|
474
|
-
}
|
|
475
|
-
function toNavPage(page, displayTitle) {
|
|
476
|
-
return {
|
|
477
|
-
title: displayTitle ?? page.title,
|
|
478
|
-
slug: page.slug,
|
|
479
|
-
href: `${page.slug}.html`,
|
|
480
|
-
sourcePath: page.sourcePath,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function extractSkillFolder(sourcePath) {
|
|
484
|
-
const normalized = sourcePath.replace(/\\/g, "/");
|
|
485
|
-
const match = normalized.match(/^\.agents\/skills\/([^/]+)\/SKILL\.md$/);
|
|
486
|
-
return match?.[1] ?? null;
|
|
173
|
+
}
|
|
487
174
|
}
|
|
488
|
-
function
|
|
175
|
+
function humanizeFolderLabel(segment, path) {
|
|
176
|
+
if (segment.startsWith("epic-")) {
|
|
177
|
+
const num = segment.slice("epic-".length);
|
|
178
|
+
return `Epic ${num}`;
|
|
179
|
+
}
|
|
489
180
|
if (FOLDER_LABELS[segment]) {
|
|
490
181
|
return FOLDER_LABELS[segment];
|
|
491
182
|
}
|
|
183
|
+
if (path[0] === "planning" && path.length === 2) {
|
|
184
|
+
return (FOLDER_LABELS[segment] ??
|
|
185
|
+
segment
|
|
186
|
+
.split("-")
|
|
187
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
188
|
+
.join(" "));
|
|
189
|
+
}
|
|
492
190
|
return segment
|
|
493
191
|
.split("-")
|
|
494
|
-
.map((
|
|
192
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
495
193
|
.join(" ");
|
|
496
194
|
}
|
|
497
|
-
function
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
return
|
|
195
|
+
function sortImplementationStoryPages(root) {
|
|
196
|
+
const implNode = root.get("implementation-stories");
|
|
197
|
+
if (!implNode) {
|
|
198
|
+
return;
|
|
501
199
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
508
|
-
.replace(/^-+|-+$/g, "");
|
|
509
|
-
}
|
|
510
|
-
async function parseBmadHelpCsv(csvContent, projectRoot) {
|
|
511
|
-
const skills = new Map();
|
|
512
|
-
const lines = csvContent.split(/\r?\n/).filter(Boolean);
|
|
513
|
-
if (lines.length <= 1) {
|
|
514
|
-
return { skills, agents: new Map() };
|
|
515
|
-
}
|
|
516
|
-
for (const line of lines.slice(1)) {
|
|
517
|
-
const row = parseCsvLine(line);
|
|
518
|
-
if (row.length < 8) {
|
|
519
|
-
continue;
|
|
200
|
+
const epicEntries = [...implNode.children.entries()].sort(([, a], [, b]) => {
|
|
201
|
+
const epicA = parseInt(a.key.match(/epic-(\d+)/)?.[1] ?? "0", 10);
|
|
202
|
+
const epicB = parseInt(b.key.match(/epic-(\d+)/)?.[1] ?? "0", 10);
|
|
203
|
+
if (epicA !== epicB) {
|
|
204
|
+
return epicA - epicB;
|
|
520
205
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
206
|
+
return a.order - b.order;
|
|
207
|
+
});
|
|
208
|
+
implNode.children = new Map(epicEntries);
|
|
209
|
+
for (const [, epicNode] of epicEntries) {
|
|
210
|
+
epicNode.pages.sort((a, b) => {
|
|
211
|
+
const numsA = storyNumbers(a.sourcePath);
|
|
212
|
+
const numsB = storyNumbers(b.sourcePath);
|
|
213
|
+
if (numsA.epic !== numsB.epic) {
|
|
214
|
+
return numsA.epic - numsB.epic;
|
|
215
|
+
}
|
|
216
|
+
return numsA.story - numsB.story;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function finalizeSubgroups(nodes, options) {
|
|
221
|
+
const result = [];
|
|
222
|
+
const rootPromoted = [];
|
|
223
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => a.order - b.order);
|
|
224
|
+
for (const node of sortedNodes) {
|
|
225
|
+
const finalized = finalizeNode(node, options);
|
|
226
|
+
rootPromoted.push(...finalized.promoted);
|
|
227
|
+
if (finalized.subgroup) {
|
|
228
|
+
result.push(finalized.subgroup);
|
|
528
229
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
230
|
+
}
|
|
231
|
+
return { subgroups: result, rootPromoted };
|
|
232
|
+
}
|
|
233
|
+
function finalizeNode(node, options) {
|
|
234
|
+
const promoted = [...node.pages];
|
|
235
|
+
const childSubgroups = [];
|
|
236
|
+
const sortedChildren = [...node.children.values()].sort((a, b) => a.order - b.order);
|
|
237
|
+
for (const child of sortedChildren) {
|
|
238
|
+
const finalizedChild = finalizeNode(child, options);
|
|
239
|
+
promoted.push(...finalizedChild.promoted);
|
|
240
|
+
if (finalizedChild.subgroup) {
|
|
241
|
+
childSubgroups.push(finalizedChild.subgroup);
|
|
537
242
|
}
|
|
538
243
|
}
|
|
539
|
-
const
|
|
540
|
-
|
|
244
|
+
const totalPageCount = promoted.length + childSubgroups.reduce((sum, sg) => sum + sg.pageCount, 0);
|
|
245
|
+
if (totalPageCount === 0) {
|
|
246
|
+
return { promoted: [] };
|
|
247
|
+
}
|
|
248
|
+
if (totalPageCount === 1 && promoted.length === 1) {
|
|
249
|
+
return { promoted };
|
|
250
|
+
}
|
|
251
|
+
const containsActive = nodeOrChildrenContainActive(promoted, childSubgroups, options);
|
|
252
|
+
const collapsible = totalPageCount > 1;
|
|
253
|
+
const subgroup = {
|
|
254
|
+
key: node.key,
|
|
255
|
+
label: node.label,
|
|
256
|
+
pageCount: totalPageCount,
|
|
257
|
+
collapsible,
|
|
258
|
+
open: containsActive,
|
|
259
|
+
pages: promoted,
|
|
260
|
+
};
|
|
261
|
+
if (childSubgroups.length > 0) {
|
|
262
|
+
subgroup.subgroups = childSubgroups;
|
|
263
|
+
applyIndexOpenState(subgroup, options);
|
|
264
|
+
}
|
|
265
|
+
if (options.indexBuild && collapsible && !containsActive) {
|
|
266
|
+
subgroup.open = false;
|
|
267
|
+
}
|
|
268
|
+
return { promoted: [], subgroup };
|
|
541
269
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
270
|
+
function applyIndexOpenState(subgroup, options) {
|
|
271
|
+
if (!options.indexBuild) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
for (const child of subgroup.subgroups ?? []) {
|
|
275
|
+
const childActive = subgroupContainsActiveInChild(child, options);
|
|
276
|
+
if (child.collapsible && !childActive) {
|
|
277
|
+
child.open = false;
|
|
547
278
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
279
|
+
applyIndexOpenState(child, options);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function nodeOrChildrenContainActive(pages, childSubgroups, options) {
|
|
283
|
+
if (!options.activePageSlug && !options.activeSourcePath) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
for (const page of pages) {
|
|
287
|
+
if (page.slug === options.activePageSlug ||
|
|
288
|
+
page.sourcePath === options.activeSourcePath) {
|
|
289
|
+
return true;
|
|
555
290
|
}
|
|
556
|
-
|
|
557
|
-
|
|
291
|
+
}
|
|
292
|
+
for (const child of childSubgroups) {
|
|
293
|
+
if (subgroupContainsActiveInChild(child, options)) {
|
|
294
|
+
return true;
|
|
558
295
|
}
|
|
559
296
|
}
|
|
560
|
-
return
|
|
297
|
+
return false;
|
|
561
298
|
}
|
|
562
|
-
function
|
|
563
|
-
if (
|
|
564
|
-
return
|
|
299
|
+
function subgroupContainsActiveInChild(subgroup, options) {
|
|
300
|
+
if (!options.activePageSlug && !options.activeSourcePath) {
|
|
301
|
+
return false;
|
|
565
302
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return { skill: skillFolder, name, title, icon };
|
|
571
|
-
}
|
|
572
|
-
function extractTomlSection(content, section) {
|
|
573
|
-
const pattern = new RegExp(`^\\[${section}\\][\\s\\S]*?(?=^\\[[^\\]]+\\]|$)`, "m");
|
|
574
|
-
const match = content.match(pattern);
|
|
575
|
-
return match?.[0] ?? "";
|
|
576
|
-
}
|
|
577
|
-
function readTomlString(section, key) {
|
|
578
|
-
const pattern = new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m");
|
|
579
|
-
const match = section.match(pattern);
|
|
580
|
-
return match?.[1];
|
|
581
|
-
}
|
|
582
|
-
function parseCsvLine(line) {
|
|
583
|
-
const values = [];
|
|
584
|
-
let current = "";
|
|
585
|
-
let inQuotes = false;
|
|
586
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
587
|
-
const char = line[index];
|
|
588
|
-
if (char === '"') {
|
|
589
|
-
if (inQuotes && line[index + 1] === '"') {
|
|
590
|
-
current += '"';
|
|
591
|
-
index += 1;
|
|
592
|
-
}
|
|
593
|
-
else {
|
|
594
|
-
inQuotes = !inQuotes;
|
|
595
|
-
}
|
|
596
|
-
continue;
|
|
303
|
+
for (const page of subgroup.pages) {
|
|
304
|
+
if (page.slug === options.activePageSlug ||
|
|
305
|
+
page.sourcePath === options.activeSourcePath) {
|
|
306
|
+
return true;
|
|
597
307
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
308
|
+
}
|
|
309
|
+
for (const child of subgroup.subgroups ?? []) {
|
|
310
|
+
if (subgroupContainsActiveInChild(child, options)) {
|
|
311
|
+
return true;
|
|
602
312
|
}
|
|
603
|
-
current += char;
|
|
604
313
|
}
|
|
605
|
-
|
|
606
|
-
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
/** Exported for unit tests — maps category keys to path strip prefixes. */
|
|
317
|
+
export function categoryPathPrefix(categoryKey) {
|
|
318
|
+
return CATEGORY_PATH_PREFIXES[categoryKey];
|
|
319
|
+
}
|
|
320
|
+
/** Exported for unit tests — humanizes a folder segment. */
|
|
321
|
+
export function humanizeSegment(segment) {
|
|
322
|
+
return humanizeFolderLabel(segment, [segment]);
|
|
607
323
|
}
|
|
608
324
|
//# sourceMappingURL=nav-grouping.js.map
|