mdboard 1.2.0 → 2.0.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/bin.js +130 -44
- package/index.html +3321 -1195
- package/package.json +10 -11
- package/presets/kanban/api.json +91 -0
- package/presets/kanban/cli.json +69 -0
- package/presets/kanban/docs.json +29 -0
- package/presets/kanban/entities.json +128 -0
- package/presets/kanban/structure.json +15 -0
- package/presets/kanban/ui.json +86 -0
- package/presets/scrum/api.json +98 -0
- package/presets/scrum/cli.json +120 -0
- package/presets/scrum/docs.json +43 -0
- package/presets/scrum/entities.json +268 -0
- package/presets/scrum/structure.json +32 -0
- package/presets/scrum/ui.json +201 -0
- package/presets/shape-up/api.json +40 -0
- package/presets/shape-up/cli.json +44 -0
- package/presets/shape-up/docs.json +32 -0
- package/presets/shape-up/entities.json +140 -0
- package/presets/shape-up/structure.json +28 -0
- package/presets/shape-up/ui.json +114 -0
- package/src/cli/cli.js +338 -0
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +175 -0
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +180 -0
- package/src/core/config.js +551 -0
- package/src/core/history.js +146 -0
- package/src/core/scanner.js +521 -0
- package/{workspace.js → src/core/workspace.js} +0 -15
- package/{yaml.js → src/core/yaml.js} +5 -1
- package/src/server/api.js +616 -0
- package/{server.js → src/server/server.js} +180 -132
- package/{watcher.js → src/server/watcher.js} +40 -9
- package/api.js +0 -752
- package/config.js +0 -73
- package/defaults.json +0 -43
- package/init.js +0 -109
- package/scanner.js +0 -491
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard preset — Create and manage methodology presets
|
|
3
|
+
*
|
|
4
|
+
* Presets define the full config (6 JSON files) for a methodology.
|
|
5
|
+
* Built-in templates: scrum, kanban. Given entities + structure,
|
|
6
|
+
* the other 4 files (cli, api, ui, docs) are auto-derived.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* mdboard preset list List available presets
|
|
10
|
+
* mdboard preset create <name> Create from built-in template
|
|
11
|
+
* mdboard preset create <name> --from X Copy existing preset
|
|
12
|
+
* mdboard preset show <name> Show preset summary
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var fs = require('fs');
|
|
18
|
+
var path = require('path');
|
|
19
|
+
var configEngine = require('../core/config');
|
|
20
|
+
|
|
21
|
+
// ── Built-in templates ──────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
var TEMPLATES = {
|
|
24
|
+
scrum: buildScrum,
|
|
25
|
+
kanban: buildKanban,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function buildScrum() {
|
|
29
|
+
var entities = {
|
|
30
|
+
methodology: 'scrum',
|
|
31
|
+
entities: {
|
|
32
|
+
sprint: {
|
|
33
|
+
singular: 'Sprint',
|
|
34
|
+
plural: 'Sprints',
|
|
35
|
+
prefix: 'SPR',
|
|
36
|
+
file: 'README.md',
|
|
37
|
+
fields: {
|
|
38
|
+
status: {
|
|
39
|
+
type: 'enum',
|
|
40
|
+
values: [
|
|
41
|
+
{ key: 'planned', label: 'Planned', color: '#8B8B93', icon: 'dashed-circle' },
|
|
42
|
+
{ key: 'active', label: 'Active', color: '#5B6EF5', icon: 'half-circle' },
|
|
43
|
+
{ key: 'completed', label: 'Completed', color: '#2EA043', icon: 'check-circle' }
|
|
44
|
+
],
|
|
45
|
+
default: 'planned'
|
|
46
|
+
},
|
|
47
|
+
goal: { type: 'string', label: 'Goal' },
|
|
48
|
+
start_date: { type: 'date', label: 'Start' },
|
|
49
|
+
end_date: { type: 'date', label: 'End' }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
story: {
|
|
53
|
+
singular: 'Story',
|
|
54
|
+
plural: 'Stories',
|
|
55
|
+
prefix: 'US',
|
|
56
|
+
file: 'README.md',
|
|
57
|
+
fields: {
|
|
58
|
+
status: {
|
|
59
|
+
type: 'enum',
|
|
60
|
+
values: [
|
|
61
|
+
{ key: 'backlog', label: 'Backlog', color: '#8B8B93', icon: 'circle' },
|
|
62
|
+
{ key: 'ready', label: 'Ready', color: '#06B6D4', icon: 'dashed-circle' },
|
|
63
|
+
{ key: 'in-progress', label: 'In Progress', color: '#D4A72C', icon: 'half-circle' },
|
|
64
|
+
{ key: 'review', label: 'Review', color: '#8B5CF6', icon: 'three-quarter-circle' },
|
|
65
|
+
{ key: 'done', label: 'Done', color: '#2EA043', icon: 'check-circle' }
|
|
66
|
+
],
|
|
67
|
+
default: 'backlog'
|
|
68
|
+
},
|
|
69
|
+
points: { type: 'number', label: 'Points' },
|
|
70
|
+
assigned: { type: 'list', label: 'Assigned' }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
task: {
|
|
74
|
+
singular: 'Task',
|
|
75
|
+
plural: 'Tasks',
|
|
76
|
+
prefix: 'TASK',
|
|
77
|
+
file: 'PREFIX-NNN.md',
|
|
78
|
+
fields: {
|
|
79
|
+
status: {
|
|
80
|
+
type: 'enum',
|
|
81
|
+
values: [
|
|
82
|
+
{ key: 'todo', label: 'Todo', color: '#8B8B93', icon: 'circle' },
|
|
83
|
+
{ key: 'in-progress', label: 'In Progress', color: '#D4A72C', icon: 'half-circle' },
|
|
84
|
+
{ key: 'done', label: 'Done', color: '#2EA043', icon: 'check-circle' }
|
|
85
|
+
],
|
|
86
|
+
default: 'todo'
|
|
87
|
+
},
|
|
88
|
+
assigned: { type: 'list', label: 'Assigned' },
|
|
89
|
+
points: { type: 'number', label: 'Points' }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
epic: {
|
|
93
|
+
singular: 'Epic',
|
|
94
|
+
plural: 'Epics',
|
|
95
|
+
prefix: 'EPIC',
|
|
96
|
+
file: 'README.md',
|
|
97
|
+
fields: {
|
|
98
|
+
status: {
|
|
99
|
+
type: 'enum',
|
|
100
|
+
values: [
|
|
101
|
+
{ key: 'open', label: 'Open', color: '#5B6EF5', icon: 'circle' },
|
|
102
|
+
{ key: 'closed', label: 'Closed', color: '#2EA043', icon: 'check-circle' }
|
|
103
|
+
],
|
|
104
|
+
default: 'open'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
bug: {
|
|
109
|
+
singular: 'Bug',
|
|
110
|
+
plural: 'Bugs',
|
|
111
|
+
prefix: 'BUG',
|
|
112
|
+
file: 'PREFIX-NNN.md',
|
|
113
|
+
fields: {
|
|
114
|
+
status: {
|
|
115
|
+
type: 'enum',
|
|
116
|
+
values: [
|
|
117
|
+
{ key: 'open', label: 'Open', color: '#DA3633', icon: 'circle' },
|
|
118
|
+
{ key: 'in-progress', label: 'In Progress', color: '#D4A72C', icon: 'half-circle' },
|
|
119
|
+
{ key: 'fixed', label: 'Fixed', color: '#2EA043', icon: 'check-circle' }
|
|
120
|
+
],
|
|
121
|
+
default: 'open'
|
|
122
|
+
},
|
|
123
|
+
severity: {
|
|
124
|
+
type: 'enum',
|
|
125
|
+
values: [
|
|
126
|
+
{ key: 'critical', label: 'Critical', color: '#DA3633' },
|
|
127
|
+
{ key: 'major', label: 'Major', color: '#F97316' },
|
|
128
|
+
{ key: 'minor', label: 'Minor', color: '#D4A72C' }
|
|
129
|
+
],
|
|
130
|
+
default: 'major'
|
|
131
|
+
},
|
|
132
|
+
assigned: { type: 'list', label: 'Assigned' }
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
note: {
|
|
136
|
+
singular: 'Note',
|
|
137
|
+
plural: 'Notes',
|
|
138
|
+
prefix: 'NOTE',
|
|
139
|
+
file: 'PREFIX-NNN.md',
|
|
140
|
+
fields: {
|
|
141
|
+
updated: { type: 'date', label: 'Updated' }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
priorities: [
|
|
146
|
+
{ key: 'urgent', label: 'Urgent', color: '#F97316', bars: 4 },
|
|
147
|
+
{ key: 'high', label: 'High', color: '#F97316', bars: 3 },
|
|
148
|
+
{ key: 'medium', label: 'Medium', color: '#D4A72C', bars: 2 },
|
|
149
|
+
{ key: 'low', label: 'Low', color: '#5B6EF5', bars: 1 }
|
|
150
|
+
],
|
|
151
|
+
completedStatuses: ['done', 'completed', 'fixed', 'closed']
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
var structure = {
|
|
155
|
+
root: 'project',
|
|
156
|
+
hierarchy: {
|
|
157
|
+
sprint: {
|
|
158
|
+
dir: 'sprints',
|
|
159
|
+
children: {
|
|
160
|
+
story: {
|
|
161
|
+
dir: 'stories',
|
|
162
|
+
children: {
|
|
163
|
+
task: { dir: 'tasks' }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
standalone: {
|
|
170
|
+
epic: { dir: 'epics' },
|
|
171
|
+
bug: { dir: 'bugs' },
|
|
172
|
+
note: { dir: 'notes' }
|
|
173
|
+
},
|
|
174
|
+
archive: { dir: 'archive' }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return { entities: entities, structure: structure };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildKanban() {
|
|
181
|
+
var entities = {
|
|
182
|
+
methodology: 'kanban',
|
|
183
|
+
entities: {
|
|
184
|
+
task: {
|
|
185
|
+
singular: 'Task',
|
|
186
|
+
plural: 'Tasks',
|
|
187
|
+
prefix: 'TASK',
|
|
188
|
+
file: 'PREFIX-NNN.md',
|
|
189
|
+
fields: {
|
|
190
|
+
status: {
|
|
191
|
+
type: 'enum',
|
|
192
|
+
values: [
|
|
193
|
+
{ key: 'backlog', label: 'Backlog', color: '#8B8B93', icon: 'circle' },
|
|
194
|
+
{ key: 'todo', label: 'Todo', color: '#06B6D4', icon: 'dashed-circle' },
|
|
195
|
+
{ key: 'in-progress', label: 'In Progress', color: '#D4A72C', icon: 'half-circle' },
|
|
196
|
+
{ key: 'review', label: 'Review', color: '#8B5CF6', icon: 'three-quarter-circle' },
|
|
197
|
+
{ key: 'done', label: 'Done', color: '#2EA043', icon: 'check-circle' }
|
|
198
|
+
],
|
|
199
|
+
default: 'backlog'
|
|
200
|
+
},
|
|
201
|
+
priority: {
|
|
202
|
+
type: 'enum',
|
|
203
|
+
values: [
|
|
204
|
+
{ key: 'urgent', label: 'Urgent', color: '#DA3633' },
|
|
205
|
+
{ key: 'high', label: 'High', color: '#F97316' },
|
|
206
|
+
{ key: 'medium', label: 'Medium', color: '#D4A72C' },
|
|
207
|
+
{ key: 'low', label: 'Low', color: '#5B6EF5' }
|
|
208
|
+
],
|
|
209
|
+
default: 'medium'
|
|
210
|
+
},
|
|
211
|
+
assigned: { type: 'list', label: 'Assigned' },
|
|
212
|
+
points: { type: 'number', label: 'Points' },
|
|
213
|
+
tags: { type: 'list', label: 'Tags' }
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
note: {
|
|
217
|
+
singular: 'Note',
|
|
218
|
+
plural: 'Notes',
|
|
219
|
+
prefix: 'NOTE',
|
|
220
|
+
file: 'PREFIX-NNN.md',
|
|
221
|
+
fields: {
|
|
222
|
+
updated: { type: 'date', label: 'Updated' }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
priorities: [
|
|
227
|
+
{ key: 'urgent', label: 'Urgent', color: '#DA3633', bars: 4 },
|
|
228
|
+
{ key: 'high', label: 'High', color: '#F97316', bars: 3 },
|
|
229
|
+
{ key: 'medium', label: 'Medium', color: '#D4A72C', bars: 2 },
|
|
230
|
+
{ key: 'low', label: 'Low', color: '#5B6EF5', bars: 1 }
|
|
231
|
+
],
|
|
232
|
+
completedStatuses: ['done']
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Kanban: flat, no hierarchy
|
|
236
|
+
var structure = {
|
|
237
|
+
root: 'project',
|
|
238
|
+
hierarchy: {},
|
|
239
|
+
standalone: {
|
|
240
|
+
task: { dir: 'tasks' },
|
|
241
|
+
note: { dir: 'notes' }
|
|
242
|
+
},
|
|
243
|
+
archive: { dir: 'archive' }
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return { entities: entities, structure: structure };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Auto-derivation ─────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function deriveCliConfig(entities, structure) {
|
|
252
|
+
var allTypes = Object.keys(entities.entities);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
commands: {
|
|
256
|
+
create: { entities: allTypes, args: { positional: 'title', flags: 'auto' } },
|
|
257
|
+
update: { entities: allTypes, args: { positional: 'id', flags: 'auto' } },
|
|
258
|
+
delete: { entities: allTypes, action: 'archive' },
|
|
259
|
+
status: {
|
|
260
|
+
output: 'status.md',
|
|
261
|
+
sections: deriveStatusSections(entities, structure)
|
|
262
|
+
},
|
|
263
|
+
sync: {
|
|
264
|
+
checks: ['dangling-refs', 'duplicate-ids', 'missing-parents', 'orphaned-tasks'],
|
|
265
|
+
fix: true
|
|
266
|
+
},
|
|
267
|
+
init: { presets: ['shape-up', 'scrum', 'kanban'], default: entities.methodology },
|
|
268
|
+
theme: { scope: ['global', 'project'] },
|
|
269
|
+
cache: { subcommands: ['list', 'clean'] }
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function deriveStatusSections(entities, structure) {
|
|
275
|
+
var sections = [];
|
|
276
|
+
var leafType = findLeafType(structure);
|
|
277
|
+
var rootType = findRootType(structure);
|
|
278
|
+
|
|
279
|
+
// Summary metrics
|
|
280
|
+
var metrics = [];
|
|
281
|
+
if (rootType) {
|
|
282
|
+
metrics.push({ label: capitalize(entities.entities[rootType].plural), compute: 'count', entity: rootType });
|
|
283
|
+
}
|
|
284
|
+
if (leafType) {
|
|
285
|
+
metrics.push({ label: capitalize(entities.entities[leafType].plural), compute: 'count', entity: leafType, detail: 'done-ratio' });
|
|
286
|
+
}
|
|
287
|
+
sections.push({ type: 'summary', metrics: metrics });
|
|
288
|
+
|
|
289
|
+
// Entity list from root
|
|
290
|
+
if (rootType) {
|
|
291
|
+
var midType = findMidType(structure);
|
|
292
|
+
var listSection = { type: 'entity-list', entity: rootType, fields: ['title', 'status', 'progress'] };
|
|
293
|
+
if (midType) {
|
|
294
|
+
listSection.nest = { entity: midType, fields: ['title', 'status'] };
|
|
295
|
+
}
|
|
296
|
+
sections.push(listSection);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Table of leaf type
|
|
300
|
+
if (leafType) {
|
|
301
|
+
sections.push({ type: 'entity-table', entity: leafType, groupBy: 'status', fields: ['id', 'title', 'status', 'points'] });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return sections;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function deriveApiConfig(entities, structure) {
|
|
308
|
+
var allTypes = Object.keys(entities.entities);
|
|
309
|
+
var activeStatuses = ['active', 'in-progress', 'building'];
|
|
310
|
+
|
|
311
|
+
// Find types that have "active-like" statuses
|
|
312
|
+
var activeEntities = [];
|
|
313
|
+
for (var i = 0; i < allTypes.length; i++) {
|
|
314
|
+
var e = entities.entities[allTypes[i]];
|
|
315
|
+
if (e.fields && e.fields.status) {
|
|
316
|
+
var keys = e.fields.status.values.map(function (v) { return v.key; });
|
|
317
|
+
for (var j = 0; j < activeStatuses.length; j++) {
|
|
318
|
+
if (keys.indexOf(activeStatuses[j]) !== -1) {
|
|
319
|
+
activeEntities.push(allTypes[i]);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
endpoints: {
|
|
328
|
+
crud: {
|
|
329
|
+
entities: allTypes,
|
|
330
|
+
operations: ['list', 'get', 'create', 'update', 'delete'],
|
|
331
|
+
pattern: '/api/:entity',
|
|
332
|
+
sourceScoped: true,
|
|
333
|
+
sourcePattern: '/api/sources/:source/:entity'
|
|
334
|
+
},
|
|
335
|
+
queries: {
|
|
336
|
+
active: {
|
|
337
|
+
pattern: '/api/:entity/active',
|
|
338
|
+
filter: { status: activeStatuses },
|
|
339
|
+
entities: activeEntities
|
|
340
|
+
},
|
|
341
|
+
metrics: {
|
|
342
|
+
pattern: '/api/metrics',
|
|
343
|
+
computes: ['status-breakdown', 'priority-distribution', 'progress-by-parent']
|
|
344
|
+
},
|
|
345
|
+
health: {
|
|
346
|
+
pattern: '/api/health',
|
|
347
|
+
computes: ['total', 'completed', 'in-progress', 'velocity']
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
system: {
|
|
351
|
+
config: { method: 'GET', path: '/api/config' },
|
|
352
|
+
project: { method: 'GET', path: '/api/project' },
|
|
353
|
+
sources: { method: 'GET', path: '/api/sources' },
|
|
354
|
+
sync: { method: 'POST', path: '/api/sources/sync' },
|
|
355
|
+
history: { method: 'GET', path: '/api/history' },
|
|
356
|
+
switch: { method: 'POST', path: '/api/history/switch' },
|
|
357
|
+
theme: { method: 'POST', path: '/api/theme' },
|
|
358
|
+
events: { method: 'GET', path: '/api/events', type: 'sse' }
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
filtering: { strategy: 'query-params', autoFilters: true }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function deriveUiConfig(entities, structure) {
|
|
366
|
+
var tabs = [];
|
|
367
|
+
var hierarchyTypes = flattenHierarchyTypes(structure);
|
|
368
|
+
var standaloneTypes = Object.keys(structure.standalone || {});
|
|
369
|
+
var leafType = hierarchyTypes.length > 0 ? hierarchyTypes[hierarchyTypes.length - 1] : null;
|
|
370
|
+
var rootType = hierarchyTypes.length > 0 ? hierarchyTypes[0] : null;
|
|
371
|
+
|
|
372
|
+
// Leaf hierarchy → board + table + list
|
|
373
|
+
if (leafType) {
|
|
374
|
+
var e = entities.entities[leafType];
|
|
375
|
+
tabs.push({
|
|
376
|
+
id: e.plural.toLowerCase(),
|
|
377
|
+
label: e.plural,
|
|
378
|
+
description: e.plural + ' — leaf work items',
|
|
379
|
+
icon: 'check-square',
|
|
380
|
+
entity: leafType,
|
|
381
|
+
layouts: ['board', 'table', 'list'],
|
|
382
|
+
defaultLayout: 'board',
|
|
383
|
+
board: { groupBy: 'status', cardFields: ['id', 'title', 'status'], dragAction: 'update-status' },
|
|
384
|
+
table: { columns: buildTableColumns(leafType, entities, hierarchyTypes), sortable: true },
|
|
385
|
+
filters: 'auto'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Mid hierarchy → cards + list with expandChildren
|
|
390
|
+
for (var m = hierarchyTypes.length - 2; m >= 1; m--) {
|
|
391
|
+
var midType = hierarchyTypes[m];
|
|
392
|
+
var childType = hierarchyTypes[m + 1];
|
|
393
|
+
var me = entities.entities[midType];
|
|
394
|
+
var ce = entities.entities[childType];
|
|
395
|
+
tabs.push({
|
|
396
|
+
id: me.plural.toLowerCase(),
|
|
397
|
+
label: me.plural,
|
|
398
|
+
description: me.plural,
|
|
399
|
+
icon: 'layers',
|
|
400
|
+
entity: midType,
|
|
401
|
+
layouts: ['cards', 'list'],
|
|
402
|
+
defaultLayout: 'cards',
|
|
403
|
+
cards: {
|
|
404
|
+
fields: ['title', 'status'],
|
|
405
|
+
showProgress: true,
|
|
406
|
+
expandChildren: { entity: childType, fields: ['title', 'status'] }
|
|
407
|
+
},
|
|
408
|
+
list: { fields: ['title', 'status', 'progress'], expandable: true },
|
|
409
|
+
filters: 'auto'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Root hierarchy → list with expandChildren
|
|
414
|
+
if (rootType && hierarchyTypes.length > 1) {
|
|
415
|
+
var re = entities.entities[rootType];
|
|
416
|
+
var firstChild = hierarchyTypes[1];
|
|
417
|
+
tabs.push({
|
|
418
|
+
id: re.plural.toLowerCase(),
|
|
419
|
+
label: re.plural,
|
|
420
|
+
description: re.plural,
|
|
421
|
+
icon: 'refresh-cw',
|
|
422
|
+
entity: rootType,
|
|
423
|
+
layouts: ['list'],
|
|
424
|
+
defaultLayout: 'list',
|
|
425
|
+
list: {
|
|
426
|
+
fields: buildListFields(rootType, entities),
|
|
427
|
+
expandChildren: { entity: firstChild, fields: ['title', 'status'] }
|
|
428
|
+
},
|
|
429
|
+
filters: ['status']
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Standalone with status → board + cards (kanban edge case: promoted to primary)
|
|
434
|
+
for (var s = 0; s < standaloneTypes.length; s++) {
|
|
435
|
+
var st = standaloneTypes[s];
|
|
436
|
+
var se = entities.entities[st];
|
|
437
|
+
if (!se) continue;
|
|
438
|
+
|
|
439
|
+
var hasStatus = se.fields && se.fields.status;
|
|
440
|
+
var isNoteType = st === 'note' || (!hasStatus && !se.fields.priority);
|
|
441
|
+
|
|
442
|
+
if (isNoteType) {
|
|
443
|
+
// Note-like → editor
|
|
444
|
+
tabs.push({
|
|
445
|
+
id: se.plural.toLowerCase(),
|
|
446
|
+
label: se.plural,
|
|
447
|
+
description: se.plural,
|
|
448
|
+
icon: 'file-text',
|
|
449
|
+
entity: st,
|
|
450
|
+
layouts: ['editor'],
|
|
451
|
+
defaultLayout: 'editor'
|
|
452
|
+
});
|
|
453
|
+
} else if (hasStatus && !leafType) {
|
|
454
|
+
// No hierarchy → standalone with status becomes primary board
|
|
455
|
+
tabs.push({
|
|
456
|
+
id: se.plural.toLowerCase(),
|
|
457
|
+
label: se.plural,
|
|
458
|
+
description: se.plural,
|
|
459
|
+
icon: 'check-square',
|
|
460
|
+
entity: st,
|
|
461
|
+
layouts: ['board', 'table', 'list'],
|
|
462
|
+
defaultLayout: 'board',
|
|
463
|
+
board: { groupBy: 'status', cardFields: ['id', 'title', 'status'], dragAction: 'update-status' },
|
|
464
|
+
table: { columns: ['id', 'title', 'status', 'assigned'], sortable: true },
|
|
465
|
+
filters: 'auto'
|
|
466
|
+
});
|
|
467
|
+
} else if (hasStatus) {
|
|
468
|
+
// Standalone with status in hierarchy preset → cards
|
|
469
|
+
tabs.push({
|
|
470
|
+
id: se.plural.toLowerCase(),
|
|
471
|
+
label: se.plural,
|
|
472
|
+
description: se.plural,
|
|
473
|
+
icon: 'alert-circle',
|
|
474
|
+
entity: st,
|
|
475
|
+
layouts: ['cards'],
|
|
476
|
+
defaultLayout: 'cards',
|
|
477
|
+
cards: { fields: ['title', 'status'], showContent: true },
|
|
478
|
+
filters: ['status']
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Metrics tab always last
|
|
484
|
+
var metricsCards = [];
|
|
485
|
+
if (rootType) {
|
|
486
|
+
metricsCards.push({ title: capitalize(entities.entities[rootType].singular) + ' Progress', type: 'progress-by-parent', parent: rootType });
|
|
487
|
+
}
|
|
488
|
+
if (leafType) {
|
|
489
|
+
metricsCards.push({ title: 'Status Breakdown', type: 'status-breakdown', entity: leafType });
|
|
490
|
+
} else if (standaloneTypes.length > 0) {
|
|
491
|
+
var primary = standaloneTypes.find(function (t) { return entities.entities[t] && entities.entities[t].fields && entities.entities[t].fields.status; });
|
|
492
|
+
if (primary) {
|
|
493
|
+
metricsCards.push({ title: 'Status Breakdown', type: 'status-breakdown', entity: primary });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
metricsCards.push({ title: 'Project Health', type: 'health' });
|
|
497
|
+
|
|
498
|
+
tabs.push({
|
|
499
|
+
id: 'metrics',
|
|
500
|
+
label: 'Metrics',
|
|
501
|
+
description: 'Project progress and health overview',
|
|
502
|
+
icon: 'bar-chart',
|
|
503
|
+
layouts: ['metrics'],
|
|
504
|
+
defaultLayout: 'metrics',
|
|
505
|
+
metrics: { cards: metricsCards }
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
tabs: tabs,
|
|
510
|
+
panel: { enabled: true, position: 'right', editableFields: 'auto', sections: ['properties', 'ai', 'content'] },
|
|
511
|
+
sourceRail: { enabled: true, position: 'left', showHome: true, showHistory: true }
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function deriveDocsConfig(entities, structure) {
|
|
516
|
+
var leafType = findLeafType(structure);
|
|
517
|
+
var rootType = findRootType(structure);
|
|
518
|
+
|
|
519
|
+
var metrics = [];
|
|
520
|
+
if (rootType) {
|
|
521
|
+
metrics.push({ label: capitalize(entities.entities[rootType].plural), compute: 'count', entity: rootType });
|
|
522
|
+
}
|
|
523
|
+
if (leafType) {
|
|
524
|
+
metrics.push({ label: capitalize(entities.entities[leafType].plural), compute: 'count', entity: leafType, detail: 'done-ratio' });
|
|
525
|
+
} else {
|
|
526
|
+
// Kanban-style: use standalone with status
|
|
527
|
+
var standalone = Object.keys(structure.standalone || {});
|
|
528
|
+
for (var i = 0; i < standalone.length; i++) {
|
|
529
|
+
var e = entities.entities[standalone[i]];
|
|
530
|
+
if (e && e.fields && e.fields.status) {
|
|
531
|
+
metrics.push({ label: capitalize(e.plural), compute: 'count', entity: standalone[i], detail: 'done-ratio' });
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
var sections = [{ type: 'summary', metrics: metrics }];
|
|
538
|
+
|
|
539
|
+
if (rootType) {
|
|
540
|
+
sections.push({ type: 'entity-list', entity: rootType, fields: ['title', 'status', 'progress'] });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
var tableEntity = leafType || findPrimaryStandalone(entities, structure);
|
|
544
|
+
if (tableEntity) {
|
|
545
|
+
sections.push({ type: 'entity-table', entity: tableEntity, groupBy: 'status', fields: ['id', 'title', 'status'] });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
status: {
|
|
550
|
+
output: 'status.md',
|
|
551
|
+
header: 'Project Status: {{project.name}}',
|
|
552
|
+
sections: sections
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
function flattenHierarchyTypes(structure) {
|
|
560
|
+
var types = [];
|
|
561
|
+
function walk(node) {
|
|
562
|
+
for (var type in node) {
|
|
563
|
+
types.push(type);
|
|
564
|
+
if (node[type].children) walk(node[type].children);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
walk(structure.hierarchy || {});
|
|
568
|
+
return types;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function findLeafType(structure) {
|
|
572
|
+
var types = flattenHierarchyTypes(structure);
|
|
573
|
+
return types.length > 0 ? types[types.length - 1] : null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function findRootType(structure) {
|
|
577
|
+
var types = flattenHierarchyTypes(structure);
|
|
578
|
+
return types.length > 0 ? types[0] : null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function findMidType(structure) {
|
|
582
|
+
var types = flattenHierarchyTypes(structure);
|
|
583
|
+
return types.length > 1 ? types[1] : null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function findPrimaryStandalone(entities, structure) {
|
|
587
|
+
var standalone = Object.keys(structure.standalone || {});
|
|
588
|
+
for (var i = 0; i < standalone.length; i++) {
|
|
589
|
+
var e = entities.entities[standalone[i]];
|
|
590
|
+
if (e && e.fields && e.fields.status) return standalone[i];
|
|
591
|
+
}
|
|
592
|
+
return standalone[0] || null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
596
|
+
|
|
597
|
+
function buildTableColumns(leafType, entities, hierarchyTypes) {
|
|
598
|
+
var cols = ['id', 'title'];
|
|
599
|
+
// Add parent types as columns
|
|
600
|
+
for (var i = hierarchyTypes.length - 2; i >= 0; i--) {
|
|
601
|
+
cols.push(hierarchyTypes[i]);
|
|
602
|
+
}
|
|
603
|
+
cols.push('status');
|
|
604
|
+
var fields = entities.entities[leafType].fields;
|
|
605
|
+
if (fields.points) cols.push('points');
|
|
606
|
+
if (fields.assigned) cols.push('assigned');
|
|
607
|
+
return cols;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function buildListFields(type, entities) {
|
|
611
|
+
var fields = ['title', 'status'];
|
|
612
|
+
var e = entities.entities[type];
|
|
613
|
+
if (e.fields.start_date) fields.push('start_date');
|
|
614
|
+
if (e.fields.end_date) fields.push('end_date');
|
|
615
|
+
fields.push('progress');
|
|
616
|
+
return fields;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── Write preset to disk ────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
function writePreset(presetDir, configs) {
|
|
622
|
+
fs.mkdirSync(presetDir, { recursive: true });
|
|
623
|
+
var files = ['entities', 'structure', 'cli', 'api', 'ui', 'docs'];
|
|
624
|
+
for (var i = 0; i < files.length; i++) {
|
|
625
|
+
fs.writeFileSync(
|
|
626
|
+
path.join(presetDir, files[i] + '.json'),
|
|
627
|
+
JSON.stringify(configs[files[i]], null, 2) + '\n',
|
|
628
|
+
'utf-8'
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── Subcommands ─────────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
function listPresets() {
|
|
636
|
+
var presets = [];
|
|
637
|
+
try {
|
|
638
|
+
presets = fs.readdirSync(configEngine.PRESETS_DIR).filter(function (d) {
|
|
639
|
+
return fs.statSync(path.join(configEngine.PRESETS_DIR, d)).isDirectory();
|
|
640
|
+
});
|
|
641
|
+
} catch (e) { /* */ }
|
|
642
|
+
|
|
643
|
+
var builtIn = Object.keys(TEMPLATES);
|
|
644
|
+
|
|
645
|
+
console.log('\n Available presets:\n');
|
|
646
|
+
for (var i = 0; i < presets.length; i++) {
|
|
647
|
+
var tag = builtIn.indexOf(presets[i]) !== -1 ? '' : '';
|
|
648
|
+
console.log(' ' + presets[i] + tag);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Show templates not yet created
|
|
652
|
+
for (var j = 0; j < builtIn.length; j++) {
|
|
653
|
+
if (presets.indexOf(builtIn[j]) === -1) {
|
|
654
|
+
console.log(' ' + builtIn[j] + ' (template — run `mdboard preset create ' + builtIn[j] + '`)');
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
console.log('');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function createPreset(name, fromPreset) {
|
|
661
|
+
var presetDir = path.join(configEngine.PRESETS_DIR, name);
|
|
662
|
+
|
|
663
|
+
if (fs.existsSync(presetDir)) {
|
|
664
|
+
console.error(' Error: preset "' + name + '" already exists at ' + presetDir);
|
|
665
|
+
console.error(' Delete it first or choose a different name.');
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
var configs;
|
|
670
|
+
|
|
671
|
+
if (fromPreset) {
|
|
672
|
+
// Copy from existing preset
|
|
673
|
+
var sourceDir = path.join(configEngine.PRESETS_DIR, fromPreset);
|
|
674
|
+
if (!fs.existsSync(sourceDir)) {
|
|
675
|
+
console.error(' Error: source preset "' + fromPreset + '" not found.');
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
configs = {};
|
|
679
|
+
var files = ['entities', 'structure', 'cli', 'api', 'ui', 'docs'];
|
|
680
|
+
for (var i = 0; i < files.length; i++) {
|
|
681
|
+
var fp = path.join(sourceDir, files[i] + '.json');
|
|
682
|
+
try {
|
|
683
|
+
configs[files[i]] = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
684
|
+
} catch (e) {
|
|
685
|
+
configs[files[i]] = {};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Update methodology name
|
|
689
|
+
if (configs.entities) configs.entities.methodology = name;
|
|
690
|
+
} else if (TEMPLATES[name]) {
|
|
691
|
+
// Built-in template with auto-derivation
|
|
692
|
+
var template = TEMPLATES[name]();
|
|
693
|
+
configs = {
|
|
694
|
+
entities: template.entities,
|
|
695
|
+
structure: template.structure,
|
|
696
|
+
cli: deriveCliConfig(template.entities, template.structure),
|
|
697
|
+
api: deriveApiConfig(template.entities, template.structure),
|
|
698
|
+
ui: deriveUiConfig(template.entities, template.structure),
|
|
699
|
+
docs: deriveDocsConfig(template.entities, template.structure)
|
|
700
|
+
};
|
|
701
|
+
} else {
|
|
702
|
+
console.error(' Error: no built-in template for "' + name + '".');
|
|
703
|
+
console.error(' Use --from <preset> to copy an existing one.');
|
|
704
|
+
console.error(' Built-in templates: ' + Object.keys(TEMPLATES).join(', '));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
writePreset(presetDir, configs);
|
|
709
|
+
|
|
710
|
+
// Output
|
|
711
|
+
var files = fs.readdirSync(presetDir).filter(function (f) { return f.endsWith('.json'); });
|
|
712
|
+
console.log('\n Preset created: ' + name);
|
|
713
|
+
console.log(' Path: ' + presetDir + '/\n');
|
|
714
|
+
console.log(' Files:');
|
|
715
|
+
for (var j = 0; j < files.length; j++) {
|
|
716
|
+
console.log(' ' + files[j]);
|
|
717
|
+
}
|
|
718
|
+
console.log('\n Edit any file to customize. Core files:');
|
|
719
|
+
console.log(' entities.json Entity types, fields, statuses');
|
|
720
|
+
console.log(' structure.json Directory layout & hierarchy');
|
|
721
|
+
console.log(' Derived (auto-generated from entities + structure):');
|
|
722
|
+
console.log(' cli.json, api.json, ui.json, docs.json\n');
|
|
723
|
+
console.log(' Use this preset:');
|
|
724
|
+
console.log(' mdboard --preset ' + name);
|
|
725
|
+
console.log(' mdboard preset show ' + name);
|
|
726
|
+
console.log('');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function showPreset(name) {
|
|
730
|
+
var presetDir = path.join(configEngine.PRESETS_DIR, name);
|
|
731
|
+
if (!fs.existsSync(presetDir)) {
|
|
732
|
+
console.error(' Error: preset "' + name + '" not found.');
|
|
733
|
+
if (TEMPLATES[name]) {
|
|
734
|
+
console.error(' Run `mdboard preset create ' + name + '` to generate it.');
|
|
735
|
+
}
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
var entitiesFile = path.join(presetDir, 'entities.json');
|
|
740
|
+
var structureFile = path.join(presetDir, 'structure.json');
|
|
741
|
+
var entities, structure;
|
|
742
|
+
|
|
743
|
+
try { entities = JSON.parse(fs.readFileSync(entitiesFile, 'utf-8')); } catch (e) { entities = {}; }
|
|
744
|
+
try { structure = JSON.parse(fs.readFileSync(structureFile, 'utf-8')); } catch (e) { structure = {}; }
|
|
745
|
+
|
|
746
|
+
var hierarchyTypes = flattenHierarchyTypes(structure);
|
|
747
|
+
var standaloneTypes = Object.keys(structure.standalone || {});
|
|
748
|
+
var allTypes = Object.keys(entities.entities || {});
|
|
749
|
+
|
|
750
|
+
console.log('\n Preset: ' + name);
|
|
751
|
+
console.log(' Path: ' + presetDir + '/');
|
|
752
|
+
|
|
753
|
+
// Hierarchy
|
|
754
|
+
if (hierarchyTypes.length > 0) {
|
|
755
|
+
var chain = hierarchyTypes.map(function (t) {
|
|
756
|
+
var e = entities.entities[t];
|
|
757
|
+
return e ? e.singular : t;
|
|
758
|
+
}).join(' > ');
|
|
759
|
+
console.log('\n Hierarchy: ' + chain);
|
|
760
|
+
} else {
|
|
761
|
+
console.log('\n Hierarchy: (flat — no nesting)');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Standalone
|
|
765
|
+
if (standaloneTypes.length > 0) {
|
|
766
|
+
var names = standaloneTypes.map(function (t) {
|
|
767
|
+
var e = entities.entities[t];
|
|
768
|
+
return e ? e.singular : t;
|
|
769
|
+
}).join(', ');
|
|
770
|
+
console.log(' Standalone: ' + names);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Entity details
|
|
774
|
+
console.log('\n Entities (' + allTypes.length + '):\n');
|
|
775
|
+
for (var i = 0; i < allTypes.length; i++) {
|
|
776
|
+
var type = allTypes[i];
|
|
777
|
+
var e = entities.entities[type];
|
|
778
|
+
var statuses = e.fields && e.fields.status
|
|
779
|
+
? e.fields.status.values.map(function (v) { return v.key; }).join(', ')
|
|
780
|
+
: '(no status)';
|
|
781
|
+
var storage = e.file === 'PREFIX-NNN.md' ? 'file' : 'dir';
|
|
782
|
+
console.log(' ' + e.prefix.padEnd(6) + ' ' + e.singular.padEnd(10) + ' [' + storage + '] ' + statuses);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
console.log('');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ── CLI runner ──────────────────────────────────────────────────────
|
|
789
|
+
|
|
790
|
+
function run(args) {
|
|
791
|
+
var subCmd = args[0];
|
|
792
|
+
|
|
793
|
+
if (!subCmd || subCmd === '--help' || subCmd === '-h') {
|
|
794
|
+
printHelp();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (subCmd === 'list') {
|
|
799
|
+
listPresets();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (subCmd === 'create') {
|
|
804
|
+
var name = args[1];
|
|
805
|
+
if (!name) {
|
|
806
|
+
console.error(' Error: preset name required.');
|
|
807
|
+
console.error(' Usage: mdboard preset create <name> [--from <preset>]');
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
var fromPreset = null;
|
|
812
|
+
for (var i = 2; i < args.length; i++) {
|
|
813
|
+
if (args[i] === '--from' && args[i + 1]) {
|
|
814
|
+
fromPreset = args[i + 1];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
createPreset(name, fromPreset);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (subCmd === 'show') {
|
|
823
|
+
var name = args[1];
|
|
824
|
+
if (!name) {
|
|
825
|
+
console.error(' Error: preset name required.');
|
|
826
|
+
console.error(' Usage: mdboard preset show <name>');
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
showPreset(name);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
console.error(' Unknown subcommand: ' + subCmd);
|
|
834
|
+
printHelp();
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function printHelp() {
|
|
839
|
+
console.log('\n mdboard preset — Create and manage methodology presets\n');
|
|
840
|
+
console.log(' Subcommands:');
|
|
841
|
+
console.log(' mdboard preset list List available presets');
|
|
842
|
+
console.log(' mdboard preset create <name> Create from built-in template');
|
|
843
|
+
console.log(' mdboard preset create <name> --from X Copy existing preset');
|
|
844
|
+
console.log(' mdboard preset show <name> Show preset summary\n');
|
|
845
|
+
console.log(' Built-in templates: ' + Object.keys(TEMPLATES).join(', '));
|
|
846
|
+
console.log('');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
module.exports = { run, TEMPLATES, deriveCliConfig, deriveApiConfig, deriveUiConfig, deriveDocsConfig };
|