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,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"methodology": "shape-up",
|
|
3
|
+
"entities": {
|
|
4
|
+
"cycle": {
|
|
5
|
+
"singular": "Cycle",
|
|
6
|
+
"plural": "Cycles",
|
|
7
|
+
"prefix": "CYC",
|
|
8
|
+
"file": "README.md",
|
|
9
|
+
"fields": {
|
|
10
|
+
"status": {
|
|
11
|
+
"type": "enum",
|
|
12
|
+
"values": [
|
|
13
|
+
{ "key": "planned", "label": "Planned", "color": "#8B8B93", "icon": "dashed-circle" },
|
|
14
|
+
{ "key": "active", "label": "Active", "color": "#5B6EF5", "icon": "half-circle" },
|
|
15
|
+
{ "key": "cooldown", "label": "Cooldown", "color": "#D4A72C", "icon": "pause-circle" },
|
|
16
|
+
{ "key": "completed", "label": "Completed", "color": "#2EA043", "icon": "check-circle" }
|
|
17
|
+
],
|
|
18
|
+
"default": "planned"
|
|
19
|
+
},
|
|
20
|
+
"goal": { "type": "string", "label": "Goal" },
|
|
21
|
+
"duration": { "type": "number", "label": "Weeks", "default": 6 },
|
|
22
|
+
"start_date": { "type": "date", "label": "Start" },
|
|
23
|
+
"end_date": { "type": "date", "label": "End" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bet": {
|
|
27
|
+
"singular": "Bet",
|
|
28
|
+
"plural": "Bets",
|
|
29
|
+
"prefix": "BET",
|
|
30
|
+
"file": "README.md",
|
|
31
|
+
"fields": {
|
|
32
|
+
"status": {
|
|
33
|
+
"type": "enum",
|
|
34
|
+
"values": [
|
|
35
|
+
{ "key": "shaping", "label": "Shaping", "color": "#8B8B93", "icon": "dashed-circle" },
|
|
36
|
+
{ "key": "ready", "label": "Ready", "color": "#06B6D4", "icon": "circle" },
|
|
37
|
+
{ "key": "building", "label": "Building", "color": "#D4A72C", "icon": "half-circle" },
|
|
38
|
+
{ "key": "shipped", "label": "Shipped", "color": "#2EA043", "icon": "check-circle" },
|
|
39
|
+
{ "key": "dropped", "label": "Dropped", "color": "#5A5A63", "icon": "slash-circle" }
|
|
40
|
+
],
|
|
41
|
+
"default": "shaping"
|
|
42
|
+
},
|
|
43
|
+
"appetite": {
|
|
44
|
+
"type": "enum",
|
|
45
|
+
"values": [
|
|
46
|
+
{ "key": "small", "label": "Small Batch", "color": "#06B6D4" },
|
|
47
|
+
{ "key": "big", "label": "Big Batch", "color": "#8B5CF6" }
|
|
48
|
+
],
|
|
49
|
+
"default": "big"
|
|
50
|
+
},
|
|
51
|
+
"problem": { "type": "text", "label": "Problem" },
|
|
52
|
+
"solution": { "type": "text", "label": "Solution" },
|
|
53
|
+
"no_gos": { "type": "list", "label": "No-gos" }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scope": {
|
|
57
|
+
"singular": "Scope",
|
|
58
|
+
"plural": "Scopes",
|
|
59
|
+
"prefix": "SCP",
|
|
60
|
+
"file": "README.md",
|
|
61
|
+
"fields": {
|
|
62
|
+
"hill": {
|
|
63
|
+
"type": "enum",
|
|
64
|
+
"values": [
|
|
65
|
+
{ "key": "unknown", "label": "Unknown", "color": "#5A5A63", "icon": "circle" },
|
|
66
|
+
{ "key": "figuring-out", "label": "Figuring it out", "color": "#F97316", "icon": "half-circle" },
|
|
67
|
+
{ "key": "making-it-happen", "label": "Making it happen", "color": "#5B6EF5", "icon": "three-quarter-circle" },
|
|
68
|
+
{ "key": "done", "label": "Done", "color": "#2EA043", "icon": "check-circle" }
|
|
69
|
+
],
|
|
70
|
+
"default": "unknown"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"task": {
|
|
75
|
+
"singular": "Task",
|
|
76
|
+
"plural": "Tasks",
|
|
77
|
+
"prefix": "TASK",
|
|
78
|
+
"file": "PREFIX-NNN.md",
|
|
79
|
+
"fields": {
|
|
80
|
+
"status": {
|
|
81
|
+
"type": "enum",
|
|
82
|
+
"values": [
|
|
83
|
+
{ "key": "todo", "label": "Todo", "color": "#8B8B93", "icon": "circle" },
|
|
84
|
+
{ "key": "in-progress", "label": "In Progress", "color": "#D4A72C", "icon": "half-circle" },
|
|
85
|
+
{ "key": "done", "label": "Done", "color": "#2EA043", "icon": "check-circle" }
|
|
86
|
+
],
|
|
87
|
+
"default": "todo"
|
|
88
|
+
},
|
|
89
|
+
"assigned": { "type": "list", "label": "Assigned" },
|
|
90
|
+
"points": { "type": "number", "label": "Points" },
|
|
91
|
+
"linked": { "type": "ref", "label": "Linked", "entity": "task", "multiple": true }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"pitch": {
|
|
95
|
+
"singular": "Pitch",
|
|
96
|
+
"plural": "Pitches",
|
|
97
|
+
"prefix": "PITCH",
|
|
98
|
+
"file": "PREFIX-NNN.md",
|
|
99
|
+
"fields": {
|
|
100
|
+
"status": {
|
|
101
|
+
"type": "enum",
|
|
102
|
+
"values": [
|
|
103
|
+
{ "key": "draft", "label": "Draft", "color": "#5A5A63", "icon": "dashed-circle" },
|
|
104
|
+
{ "key": "ready", "label": "Ready", "color": "#06B6D4", "icon": "circle" },
|
|
105
|
+
{ "key": "accepted", "label": "Accepted", "color": "#2EA043", "icon": "check-circle" },
|
|
106
|
+
{ "key": "rejected", "label": "Rejected", "color": "#DA3633", "icon": "x-circle" }
|
|
107
|
+
],
|
|
108
|
+
"default": "draft"
|
|
109
|
+
},
|
|
110
|
+
"appetite": {
|
|
111
|
+
"type": "enum",
|
|
112
|
+
"values": [
|
|
113
|
+
{ "key": "small", "label": "Small Batch", "color": "#06B6D4" },
|
|
114
|
+
{ "key": "big", "label": "Big Batch", "color": "#8B5CF6" }
|
|
115
|
+
],
|
|
116
|
+
"default": "big"
|
|
117
|
+
},
|
|
118
|
+
"problem": { "type": "text", "label": "Problem" },
|
|
119
|
+
"solution": { "type": "text", "label": "Solution" },
|
|
120
|
+
"no_gos": { "type": "list", "label": "No-gos" }
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"note": {
|
|
124
|
+
"singular": "Note",
|
|
125
|
+
"plural": "Notes",
|
|
126
|
+
"prefix": "NOTE",
|
|
127
|
+
"file": "PREFIX-NNN.md",
|
|
128
|
+
"fields": {
|
|
129
|
+
"updated": { "type": "date", "label": "Updated" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"priorities": [
|
|
134
|
+
{ "key": "urgent", "label": "Urgent", "color": "#F97316", "bars": 4 },
|
|
135
|
+
{ "key": "high", "label": "High", "color": "#F97316", "bars": 3 },
|
|
136
|
+
{ "key": "medium", "label": "Medium", "color": "#D4A72C", "bars": 2 },
|
|
137
|
+
{ "key": "low", "label": "Low", "color": "#5B6EF5", "bars": 1 }
|
|
138
|
+
],
|
|
139
|
+
"completedStatuses": ["done", "shipped", "completed"]
|
|
140
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": "project",
|
|
3
|
+
"hierarchy": {
|
|
4
|
+
"cycle": {
|
|
5
|
+
"dir": "cycles",
|
|
6
|
+
"children": {
|
|
7
|
+
"bet": {
|
|
8
|
+
"dir": "bets",
|
|
9
|
+
"children": {
|
|
10
|
+
"scope": {
|
|
11
|
+
"dir": "scopes",
|
|
12
|
+
"children": {
|
|
13
|
+
"task": { "dir": "tasks" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"standalone": {
|
|
22
|
+
"pitch": { "dir": "pitches" },
|
|
23
|
+
"note": { "dir": "notes" }
|
|
24
|
+
},
|
|
25
|
+
"archive": {
|
|
26
|
+
"dir": "archive"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tabs": [
|
|
3
|
+
{
|
|
4
|
+
"id": "tasks",
|
|
5
|
+
"label": "Tasks",
|
|
6
|
+
"description": "Individual work items to be completed",
|
|
7
|
+
"icon": "check-square",
|
|
8
|
+
"entity": "task",
|
|
9
|
+
"layouts": ["list", "board", "table"],
|
|
10
|
+
"defaultLayout": "board",
|
|
11
|
+
"board": {
|
|
12
|
+
"groupBy": "status",
|
|
13
|
+
"cardFields": ["id", "title", "points", "assigned"],
|
|
14
|
+
"dragAction": "update-status"
|
|
15
|
+
},
|
|
16
|
+
"table": {
|
|
17
|
+
"columns": ["id", "title", "scope", "bet", "cycle", "status", "points", "assigned"],
|
|
18
|
+
"sortable": true
|
|
19
|
+
},
|
|
20
|
+
"filters": "auto"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "bets",
|
|
24
|
+
"label": "Bets",
|
|
25
|
+
"description": "Shaped projects with a fixed time appetite",
|
|
26
|
+
"icon": "target",
|
|
27
|
+
"entity": "bet",
|
|
28
|
+
"layouts": ["cards", "list"],
|
|
29
|
+
"defaultLayout": "cards",
|
|
30
|
+
"cards": {
|
|
31
|
+
"fields": ["title", "status", "appetite"],
|
|
32
|
+
"showProgress": true,
|
|
33
|
+
"expandChildren": {
|
|
34
|
+
"entity": "scope",
|
|
35
|
+
"fields": ["title", "hill"],
|
|
36
|
+
"showProgress": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"list": {
|
|
40
|
+
"fields": ["title", "status", "appetite", "progress"],
|
|
41
|
+
"expandable": true
|
|
42
|
+
},
|
|
43
|
+
"filters": ["cycle", "status", "appetite"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "cycles",
|
|
47
|
+
"label": "Cycles",
|
|
48
|
+
"description": "Fixed time periods for betting and building",
|
|
49
|
+
"icon": "refresh-cw",
|
|
50
|
+
"entity": "cycle",
|
|
51
|
+
"layouts": ["list"],
|
|
52
|
+
"defaultLayout": "list",
|
|
53
|
+
"list": {
|
|
54
|
+
"fields": ["title", "status", "duration", "start_date", "end_date", "progress"],
|
|
55
|
+
"expandChildren": {
|
|
56
|
+
"entity": "bet",
|
|
57
|
+
"fields": ["title", "status", "appetite"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"filters": ["status"]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "pitches",
|
|
64
|
+
"label": "Pitches",
|
|
65
|
+
"description": "Ideas shaped into concrete proposals",
|
|
66
|
+
"icon": "lightbulb",
|
|
67
|
+
"entity": "pitch",
|
|
68
|
+
"layouts": ["cards"],
|
|
69
|
+
"defaultLayout": "cards",
|
|
70
|
+
"cards": {
|
|
71
|
+
"fields": ["title", "status", "appetite"],
|
|
72
|
+
"showContent": true
|
|
73
|
+
},
|
|
74
|
+
"filters": ["status", "appetite"]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "notes",
|
|
78
|
+
"label": "Notes",
|
|
79
|
+
"description": "Freeform notes and documentation",
|
|
80
|
+
"icon": "file-text",
|
|
81
|
+
"entity": "note",
|
|
82
|
+
"layouts": ["editor"],
|
|
83
|
+
"defaultLayout": "editor"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "metrics",
|
|
87
|
+
"label": "Metrics",
|
|
88
|
+
"description": "Project progress, health, and status overview",
|
|
89
|
+
"icon": "bar-chart",
|
|
90
|
+
"layouts": ["metrics"],
|
|
91
|
+
"defaultLayout": "metrics",
|
|
92
|
+
"metrics": {
|
|
93
|
+
"cards": [
|
|
94
|
+
{ "title": "Cycle Progress", "type": "progress-by-parent", "parent": "cycle" },
|
|
95
|
+
{ "title": "Hill Chart", "type": "hill-chart", "entity": "scope" },
|
|
96
|
+
{ "title": "Status Breakdown", "type": "status-breakdown", "entity": "task" },
|
|
97
|
+
{ "title": "Project Health", "type": "health" }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"panel": {
|
|
103
|
+
"enabled": true,
|
|
104
|
+
"position": "right",
|
|
105
|
+
"editableFields": "auto",
|
|
106
|
+
"sections": ["properties", "ai", "content"]
|
|
107
|
+
},
|
|
108
|
+
"sourceRail": {
|
|
109
|
+
"enabled": true,
|
|
110
|
+
"position": "left",
|
|
111
|
+
"showHome": true,
|
|
112
|
+
"showHistory": true
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/cli/cli.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard — Dynamic CLI utilities + CRUD handlers
|
|
3
|
+
*
|
|
4
|
+
* All entity types, flags, and paths are resolved from config.
|
|
5
|
+
* No hardcoded entity names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const configEngine = require('../core/config');
|
|
13
|
+
const {
|
|
14
|
+
createModel, scanSource, computeProgress, mergeResults,
|
|
15
|
+
createEntity, updateEntity, deleteEntity,
|
|
16
|
+
} = require('../core/scanner');
|
|
17
|
+
|
|
18
|
+
// --- Build model ---
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the full project model from disk.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} projectDir - Workspace root directory
|
|
24
|
+
* @returns {{ model, config, projectPath }}
|
|
25
|
+
*/
|
|
26
|
+
function buildModel(projectDir) {
|
|
27
|
+
const projectPath = path.join(projectDir, 'project');
|
|
28
|
+
const cfg = configEngine.loadConfig(projectDir, process.env.MDBOARD_CONFIG);
|
|
29
|
+
const model = createModel(cfg);
|
|
30
|
+
|
|
31
|
+
mergeResults(model, [scanSource(projectPath, cfg, {})]);
|
|
32
|
+
computeProgress(model, cfg);
|
|
33
|
+
|
|
34
|
+
return { model, config: cfg, projectPath };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Flag parsing ---
|
|
38
|
+
|
|
39
|
+
const GLOBAL_FLAGS = new Set(['project', 'config', 'workspace', 'preset']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse CLI flags and positional arguments.
|
|
43
|
+
*/
|
|
44
|
+
function parseFlags(args) {
|
|
45
|
+
const flags = {};
|
|
46
|
+
const positional = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
if (args[i].startsWith('--')) {
|
|
50
|
+
const key = args[i].slice(2);
|
|
51
|
+
const next = args[i + 1];
|
|
52
|
+
if (GLOBAL_FLAGS.has(key)) {
|
|
53
|
+
if (next && !next.startsWith('--')) i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (next && !next.startsWith('--')) {
|
|
57
|
+
flags[key] = next;
|
|
58
|
+
i++;
|
|
59
|
+
} else {
|
|
60
|
+
flags[key] = true;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
positional.push(args[i]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { flags, positional };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the project directory from args.
|
|
72
|
+
*/
|
|
73
|
+
function resolveProjectDir(args) {
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
if (args[i] === '--project' && args[i + 1]) {
|
|
76
|
+
return path.resolve(args[i + 1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return process.cwd();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Dynamic CRUD ---
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* mdboard create <entity> <title> [--flags]
|
|
86
|
+
*
|
|
87
|
+
* Entities and flags are resolved dynamically from config.
|
|
88
|
+
*/
|
|
89
|
+
function handleCreate(projectDir, args) {
|
|
90
|
+
const { flags, positional } = parseFlags(args);
|
|
91
|
+
const entityType = positional[0];
|
|
92
|
+
const title = positional.slice(1).join(' ');
|
|
93
|
+
|
|
94
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
95
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
96
|
+
|
|
97
|
+
if (!entityType) {
|
|
98
|
+
const entityList = availableTypes.map(t => {
|
|
99
|
+
const e = configEngine.getEntity(cfg, t);
|
|
100
|
+
return ' mdboard create ' + t + ' "<title>"';
|
|
101
|
+
}).join('\n');
|
|
102
|
+
|
|
103
|
+
console.log('\n mdboard create — Create a new entity\n\n Usage:\n' + entityList + '\n');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!availableTypes.includes(entityType)) {
|
|
108
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const entityDef = configEngine.getEntity(cfg, entityType);
|
|
113
|
+
|
|
114
|
+
// Check required title
|
|
115
|
+
if (!title && entityDef.file !== 'README.md') {
|
|
116
|
+
// File entities always need a title
|
|
117
|
+
console.error(' Error: title is required');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (!title) {
|
|
121
|
+
console.error(' Error: title is required');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build data from flags + title
|
|
126
|
+
const data = { title };
|
|
127
|
+
|
|
128
|
+
// Add ancestor flags (required parent references)
|
|
129
|
+
const ancestors = configEngine.getAncestors(cfg, entityType);
|
|
130
|
+
for (const anc of ancestors) {
|
|
131
|
+
const flagName = anc.replace(/_/g, '-');
|
|
132
|
+
if (!flags[flagName] && !flags[anc]) {
|
|
133
|
+
console.error(' Error: --' + flagName + ' is required');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
data[anc] = flags[flagName] || flags[anc];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add field flags
|
|
140
|
+
const fields = configEngine.getFields(cfg, entityType);
|
|
141
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
142
|
+
const flagName = name.replace(/_/g, '-');
|
|
143
|
+
const val = flags[flagName] || flags[name];
|
|
144
|
+
if (val !== undefined) {
|
|
145
|
+
if (def.type === 'number') {
|
|
146
|
+
data[name] = parseInt(val, 10);
|
|
147
|
+
} else if (def.type === 'list') {
|
|
148
|
+
data[name] = typeof val === 'string' ? val.split(',') : val;
|
|
149
|
+
} else {
|
|
150
|
+
data[name] = val;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existing = model.entities[entityType] || [];
|
|
156
|
+
const result = createEntity(projectPath, cfg, entityType, data, existing);
|
|
157
|
+
console.log(' Created ' + entityDef.singular.toLowerCase() + ' ' + result.id + ': ' + result.file);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* mdboard update <entity> <id> [--key value ...]
|
|
162
|
+
*/
|
|
163
|
+
function handleUpdate(projectDir, args) {
|
|
164
|
+
const { flags, positional } = parseFlags(args);
|
|
165
|
+
const entityType = positional[0];
|
|
166
|
+
const id = positional[1];
|
|
167
|
+
|
|
168
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
169
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
170
|
+
|
|
171
|
+
if (!entityType || !id) {
|
|
172
|
+
console.log('\n mdboard update — Update an entity\n\n Usage:\n mdboard update <entity> <id> --field value\n');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!availableTypes.includes(entityType)) {
|
|
177
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const items = model.entities[entityType] || [];
|
|
182
|
+
const item = items.find(x => x.id === id) || items.find(x => x._originalId === id);
|
|
183
|
+
|
|
184
|
+
if (!item) {
|
|
185
|
+
console.error(' Error: ' + entityType + ' "' + id + '" not found');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (Object.keys(flags).length === 0) {
|
|
190
|
+
console.error(' Error: no fields to update. Use --key value flags.');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Convert types based on field definitions
|
|
195
|
+
const entityDef = configEngine.getEntity(cfg, entityType);
|
|
196
|
+
const fields = entityDef.fields || {};
|
|
197
|
+
const updates = {};
|
|
198
|
+
|
|
199
|
+
for (const [key, val] of Object.entries(flags)) {
|
|
200
|
+
const fieldName = key.replace(/-/g, '_');
|
|
201
|
+
const fieldDef = fields[fieldName];
|
|
202
|
+
if (fieldDef && fieldDef.type === 'number') {
|
|
203
|
+
updates[fieldName] = parseInt(val, 10);
|
|
204
|
+
} else if (fieldDef && fieldDef.type === 'list') {
|
|
205
|
+
updates[fieldName] = typeof val === 'string' ? val.split(',') : val;
|
|
206
|
+
} else {
|
|
207
|
+
updates[fieldName] = val;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
updateEntity(projectPath, item._file, updates);
|
|
212
|
+
console.log(' Updated ' + entityType + ' ' + id + ': ' + item._file);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* mdboard delete <entity> <id>
|
|
217
|
+
*/
|
|
218
|
+
function handleDelete(projectDir, args) {
|
|
219
|
+
const { positional } = parseFlags(args);
|
|
220
|
+
const entityType = positional[0];
|
|
221
|
+
const id = positional[1];
|
|
222
|
+
|
|
223
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
224
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
225
|
+
|
|
226
|
+
if (!entityType || !id) {
|
|
227
|
+
console.log('\n mdboard delete — Delete an entity\n\n Usage:\n mdboard delete <entity> <id>\n');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!availableTypes.includes(entityType)) {
|
|
232
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const items = model.entities[entityType] || [];
|
|
237
|
+
const item = items.find(x => x.id === id) || items.find(x => x._originalId === id);
|
|
238
|
+
|
|
239
|
+
if (!item) {
|
|
240
|
+
console.error(' Error: ' + entityType + ' "' + id + '" not found');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
deleteEntity(projectPath, item);
|
|
245
|
+
console.log(' Deleted ' + entityType + ' ' + id + ': ' + item._file);
|
|
246
|
+
|
|
247
|
+
// Clean ref fields pointing to this entity
|
|
248
|
+
const cleaned = cleanReferences(projectPath, model, cfg, entityType, item);
|
|
249
|
+
if (cleaned > 0) {
|
|
250
|
+
console.log(' Cleaned ' + cleaned + ' reference' + (cleaned > 1 ? 's' : '') + '.');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clean up dangling references after deleting an entity.
|
|
256
|
+
* Dynamically checks all ref fields across all entity types.
|
|
257
|
+
*/
|
|
258
|
+
function cleanReferences(projectPath, model, cfg, deletedType, deletedItem) {
|
|
259
|
+
let cleaned = 0;
|
|
260
|
+
const deletedId = deletedItem._originalId || deletedItem.id;
|
|
261
|
+
|
|
262
|
+
for (const type of configEngine.getEntityTypes(cfg)) {
|
|
263
|
+
const refs = configEngine.getRefFields(cfg, type);
|
|
264
|
+
for (const ref of refs) {
|
|
265
|
+
if (ref.targetEntity !== deletedType) continue;
|
|
266
|
+
|
|
267
|
+
const items = model.entities[type] || [];
|
|
268
|
+
for (const item of items) {
|
|
269
|
+
const val = item[ref.fieldName];
|
|
270
|
+
if (!val) continue;
|
|
271
|
+
|
|
272
|
+
if (ref.multiple && Array.isArray(val) && val.includes(deletedId)) {
|
|
273
|
+
const newVal = val.filter(v => v !== deletedId);
|
|
274
|
+
updateEntity(projectPath, item._file, { [ref.fieldName]: newVal });
|
|
275
|
+
cleaned++;
|
|
276
|
+
} else if (!ref.multiple && val === deletedId) {
|
|
277
|
+
updateEntity(projectPath, item._file, { [ref.fieldName]: null });
|
|
278
|
+
cleaned++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return cleaned;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* mdboard list <entity> [--status xxx] [--cycle xxx]
|
|
289
|
+
*/
|
|
290
|
+
function handleList(projectDir, args) {
|
|
291
|
+
const { flags, positional } = parseFlags(args);
|
|
292
|
+
const entityType = positional[0];
|
|
293
|
+
|
|
294
|
+
const { model, config: cfg } = buildModel(projectDir);
|
|
295
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
296
|
+
|
|
297
|
+
if (!entityType) {
|
|
298
|
+
console.log('\n mdboard list — List entities\n\n Available types: ' + availableTypes.join(', ') + '\n');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!availableTypes.includes(entityType)) {
|
|
303
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let items = model.entities[entityType] || [];
|
|
308
|
+
|
|
309
|
+
// Apply filters from flags
|
|
310
|
+
for (const [key, val] of Object.entries(flags)) {
|
|
311
|
+
const fieldName = key.replace(/-/g, '_');
|
|
312
|
+
items = items.filter(item => {
|
|
313
|
+
const itemVal = item[fieldName] || item['_' + fieldName];
|
|
314
|
+
if (Array.isArray(itemVal)) return itemVal.includes(val);
|
|
315
|
+
return itemVal === val;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const entityDef = configEngine.getEntity(cfg, entityType);
|
|
320
|
+
|
|
321
|
+
if (items.length === 0) {
|
|
322
|
+
console.log('\n No ' + entityDef.plural.toLowerCase() + ' found.\n');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log('\n ' + entityDef.plural + ' (' + items.length + '):\n');
|
|
327
|
+
for (const item of items) {
|
|
328
|
+
const status = item.status ? ' [' + item.status + ']' : '';
|
|
329
|
+
const title = item.title || item.id || '(untitled)';
|
|
330
|
+
console.log(' ' + (item.id || '-') + ' ' + title + status);
|
|
331
|
+
}
|
|
332
|
+
console.log('');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
buildModel, parseFlags, resolveProjectDir,
|
|
337
|
+
handleCreate, handleUpdate, handleDelete, handleList, cleanReferences,
|
|
338
|
+
};
|