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.
Files changed (40) hide show
  1. package/bin.js +130 -44
  2. package/index.html +3321 -1195
  3. package/package.json +10 -11
  4. package/presets/kanban/api.json +91 -0
  5. package/presets/kanban/cli.json +69 -0
  6. package/presets/kanban/docs.json +29 -0
  7. package/presets/kanban/entities.json +128 -0
  8. package/presets/kanban/structure.json +15 -0
  9. package/presets/kanban/ui.json +86 -0
  10. package/presets/scrum/api.json +98 -0
  11. package/presets/scrum/cli.json +120 -0
  12. package/presets/scrum/docs.json +43 -0
  13. package/presets/scrum/entities.json +268 -0
  14. package/presets/scrum/structure.json +32 -0
  15. package/presets/scrum/ui.json +201 -0
  16. package/presets/shape-up/api.json +40 -0
  17. package/presets/shape-up/cli.json +44 -0
  18. package/presets/shape-up/docs.json +32 -0
  19. package/presets/shape-up/entities.json +140 -0
  20. package/presets/shape-up/structure.json +28 -0
  21. package/presets/shape-up/ui.json +114 -0
  22. package/src/cli/cli.js +338 -0
  23. package/src/cli/config.js +234 -0
  24. package/src/cli/init.js +175 -0
  25. package/src/cli/preset.js +849 -0
  26. package/src/cli/skill.js +417 -0
  27. package/src/cli/status.js +180 -0
  28. package/src/core/config.js +551 -0
  29. package/src/core/history.js +146 -0
  30. package/src/core/scanner.js +521 -0
  31. package/{workspace.js → src/core/workspace.js} +0 -15
  32. package/{yaml.js → src/core/yaml.js} +5 -1
  33. package/src/server/api.js +616 -0
  34. package/{server.js → src/server/server.js} +180 -132
  35. package/{watcher.js → src/server/watcher.js} +40 -9
  36. package/api.js +0 -752
  37. package/config.js +0 -73
  38. package/defaults.json +0 -43
  39. package/init.js +0 -109
  40. 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 };