mdboard 1.3.0 → 2.1.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 +117 -59
- package/index.html +2161 -1579
- package/package.json +7 -5
- 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 +186 -210
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +128 -76
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +126 -96
- package/src/core/config.js +491 -38
- package/src/core/history.js +17 -1
- package/src/core/scanner.js +373 -463
- package/src/core/workspace.js +0 -15
- package/src/server/api.js +464 -741
- package/src/server/server.js +105 -130
- package/build.js +0 -44
- package/defaults.json +0 -43
- package/src/cli/sync.js +0 -194
- package/src/cli/theme.js +0 -142
- package/src/client/app.js +0 -266
- package/src/client/board.js +0 -157
- package/src/client/core.js +0 -331
- package/src/client/editor.js +0 -318
- package/src/client/history.js +0 -137
- package/src/client/metrics.js +0 -38
- package/src/client/milestones.js +0 -77
- package/src/client/notes.js +0 -183
- package/src/client/overview.js +0 -104
- package/src/client/panel.js +0 -637
- package/src/client/styles.css +0 -471
- package/src/client/table.js +0 -111
- package/src/client/template.html +0 -144
- package/src/client/themes.js +0 -261
- package/src/client/workspace.js +0 -164
- package/src/core/agent-scanner.js +0 -260
package/src/cli/cli.js
CHANGED
|
@@ -1,70 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mdboard — CLI utilities + CRUD handlers
|
|
2
|
+
* mdboard — Dynamic CLI utilities + CRUD handlers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* create/update/delete handlers with reference cleanup.
|
|
4
|
+
* All entity types, flags, and paths are resolved from config.
|
|
5
|
+
* No hardcoded entity names.
|
|
7
6
|
*/
|
|
8
7
|
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
9
10
|
const fs = require('fs');
|
|
10
11
|
const path = require('path');
|
|
11
|
-
const
|
|
12
|
-
const { loadWorkspace } = require('../core/workspace');
|
|
12
|
+
const configEngine = require('../core/config');
|
|
13
13
|
const {
|
|
14
14
|
createModel, scanSource, computeProgress, mergeResults,
|
|
15
|
-
|
|
16
|
-
createSprint, archiveItem,
|
|
15
|
+
createEntity, updateEntity, deleteEntity,
|
|
17
16
|
} = require('../core/scanner');
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
// --- Build model ---
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Build the full project model from disk.
|
|
22
22
|
*
|
|
23
23
|
* @param {string} projectDir - Workspace root directory
|
|
24
|
-
* @returns {{ model, config, projectPath
|
|
24
|
+
* @returns {{ model, config, projectPath }}
|
|
25
25
|
*/
|
|
26
26
|
function buildModel(projectDir) {
|
|
27
27
|
const projectPath = path.join(projectDir, 'project');
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const workspace = loadWorkspace(projectDir, wsPath);
|
|
31
|
-
const sources = workspace ? workspace.sources : [];
|
|
32
|
-
|
|
33
|
-
const model = createModel();
|
|
34
|
-
|
|
35
|
-
if (workspace && sources.length > 0) {
|
|
36
|
-
const results = [];
|
|
37
|
-
for (const source of sources) {
|
|
38
|
-
const sourcePath = source._resolvedPath;
|
|
39
|
-
if (!sourcePath) continue;
|
|
40
|
-
const readonly = source.readonly != null ? source.readonly : (source.type === 'remote');
|
|
41
|
-
const meta = {
|
|
42
|
-
name: source.name,
|
|
43
|
-
label: source.label || source.name,
|
|
44
|
-
color: source.color || null,
|
|
45
|
-
type: source.type,
|
|
46
|
-
readonly,
|
|
47
|
-
};
|
|
48
|
-
results.push(scanSource(sourcePath, config, meta));
|
|
49
|
-
}
|
|
50
|
-
mergeResults(model, results);
|
|
51
|
-
} else {
|
|
52
|
-
mergeResults(model, [scanSource(projectPath, config, {})]);
|
|
53
|
-
}
|
|
28
|
+
const cfg = configEngine.loadConfig(projectDir, process.env.MDBOARD_CONFIG);
|
|
29
|
+
const model = createModel(cfg);
|
|
54
30
|
|
|
55
|
-
|
|
31
|
+
mergeResults(model, [scanSource(projectPath, cfg, {})]);
|
|
32
|
+
computeProgress(model, cfg);
|
|
56
33
|
|
|
57
|
-
return { model, config, projectPath
|
|
34
|
+
return { model, config: cfg, projectPath };
|
|
58
35
|
}
|
|
59
36
|
|
|
37
|
+
// --- Flag parsing ---
|
|
38
|
+
|
|
39
|
+
const GLOBAL_FLAGS = new Set(['project', 'config', 'workspace', 'preset']);
|
|
40
|
+
|
|
60
41
|
/**
|
|
61
42
|
* Parse CLI flags and positional arguments.
|
|
62
|
-
*
|
|
63
|
-
* @param {string[]} args - Raw CLI arguments
|
|
64
|
-
* @returns {{ flags: object, positional: string[] }}
|
|
65
43
|
*/
|
|
66
|
-
const GLOBAL_FLAGS = new Set(['project', 'config', 'workspace']);
|
|
67
|
-
|
|
68
44
|
function parseFlags(args) {
|
|
69
45
|
const flags = {};
|
|
70
46
|
const positional = [];
|
|
@@ -74,7 +50,6 @@ function parseFlags(args) {
|
|
|
74
50
|
const key = args[i].slice(2);
|
|
75
51
|
const next = args[i + 1];
|
|
76
52
|
if (GLOBAL_FLAGS.has(key)) {
|
|
77
|
-
// Skip global flags (consumed by resolveProjectDir / bin.js)
|
|
78
53
|
if (next && !next.startsWith('--')) i++;
|
|
79
54
|
continue;
|
|
80
55
|
}
|
|
@@ -94,9 +69,6 @@ function parseFlags(args) {
|
|
|
94
69
|
|
|
95
70
|
/**
|
|
96
71
|
* Resolve the project directory from args.
|
|
97
|
-
*
|
|
98
|
-
* @param {string[]} args - Raw CLI arguments
|
|
99
|
-
* @returns {string}
|
|
100
72
|
*/
|
|
101
73
|
function resolveProjectDir(args) {
|
|
102
74
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -107,85 +79,82 @@ function resolveProjectDir(args) {
|
|
|
107
79
|
return process.cwd();
|
|
108
80
|
}
|
|
109
81
|
|
|
110
|
-
//
|
|
111
|
-
// CRUD handlers
|
|
112
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
82
|
+
// --- Dynamic CRUD ---
|
|
113
83
|
|
|
114
84
|
/**
|
|
115
|
-
* mdboard create <entity> <title> [flags]
|
|
85
|
+
* mdboard create <entity> <title> [--flags]
|
|
86
|
+
*
|
|
87
|
+
* Entities and flags are resolved dynamically from config.
|
|
116
88
|
*/
|
|
117
89
|
function handleCreate(projectDir, args) {
|
|
118
90
|
const { flags, positional } = parseFlags(args);
|
|
119
|
-
const
|
|
91
|
+
const entityType = positional[0];
|
|
120
92
|
const title = positional.slice(1).join(' ');
|
|
121
93
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
mdboard create — Create a new entity
|
|
94
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
95
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
125
96
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
mdboard create
|
|
130
|
-
|
|
131
|
-
|
|
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');
|
|
132
104
|
process.exit(1);
|
|
133
105
|
}
|
|
134
106
|
|
|
135
|
-
|
|
107
|
+
if (!availableTypes.includes(entityType)) {
|
|
108
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
136
111
|
|
|
137
|
-
|
|
138
|
-
case 'milestone': {
|
|
139
|
-
if (!title) { console.error(' Error: title is required'); process.exit(1); }
|
|
140
|
-
const result = createMilestone(projectPath, config, { title }, model.milestones);
|
|
141
|
-
console.log(` Created milestone ${result.id}: ${result.file}`);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
112
|
+
const entityDef = configEngine.getEntity(cfg, entityType);
|
|
144
113
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
124
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!flags.milestone) { console.error(' Error: --milestone is required'); process.exit(1); }
|
|
159
|
-
if (!flags.epic) { console.error(' Error: --epic is required'); process.exit(1); }
|
|
160
|
-
const data = {
|
|
161
|
-
title,
|
|
162
|
-
milestone: flags.milestone,
|
|
163
|
-
epic: flags.epic,
|
|
164
|
-
};
|
|
165
|
-
if (flags.priority) data.priority = flags.priority;
|
|
166
|
-
if (flags.points) data.points = parseInt(flags.points, 10);
|
|
167
|
-
if (flags.sprint) data.sprint = flags.sprint;
|
|
168
|
-
if (flags.assigned) data.assigned = flags.assigned;
|
|
169
|
-
if (flags.status) data.status = flags.status;
|
|
170
|
-
const result = createTask(projectPath, config, data, model.tasks);
|
|
171
|
-
console.log(` Created task ${result.id}: ${result.file}`);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
125
|
+
// Build data from flags + title
|
|
126
|
+
const data = { title };
|
|
174
127
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
break;
|
|
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);
|
|
183
135
|
}
|
|
136
|
+
data[anc] = flags[flagName] || flags[anc];
|
|
137
|
+
}
|
|
184
138
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
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);
|
|
189
158
|
}
|
|
190
159
|
|
|
191
160
|
/**
|
|
@@ -193,42 +162,27 @@ function handleCreate(projectDir, args) {
|
|
|
193
162
|
*/
|
|
194
163
|
function handleUpdate(projectDir, args) {
|
|
195
164
|
const { flags, positional } = parseFlags(args);
|
|
196
|
-
const
|
|
165
|
+
const entityType = positional[0];
|
|
197
166
|
const id = positional[1];
|
|
198
167
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
mdboard update — Update an entity's frontmatter
|
|
168
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
169
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
202
170
|
|
|
203
|
-
|
|
204
|
-
mdboard update
|
|
205
|
-
mdboard update milestone MS-001 --status active
|
|
206
|
-
mdboard update epic EPIC-002 --priority high
|
|
207
|
-
mdboard update sprint SP-003 --status active --goal "New goal"
|
|
208
|
-
`);
|
|
171
|
+
if (!entityType || !id) {
|
|
172
|
+
console.log('\n mdboard update — Update an entity\n\n Usage:\n mdboard update <entity> <id> --field value\n');
|
|
209
173
|
process.exit(1);
|
|
210
174
|
}
|
|
211
175
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const collectionMap = {
|
|
215
|
-
task: 'tasks',
|
|
216
|
-
milestone: 'milestones',
|
|
217
|
-
epic: 'epics',
|
|
218
|
-
sprint: 'sprints',
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const collection = collectionMap[entity];
|
|
222
|
-
if (!collection) {
|
|
223
|
-
console.error(` Error: unknown entity "${entity}". Use: task, milestone, epic, sprint`);
|
|
176
|
+
if (!availableTypes.includes(entityType)) {
|
|
177
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
224
178
|
process.exit(1);
|
|
225
179
|
}
|
|
226
180
|
|
|
227
|
-
const items = model[
|
|
181
|
+
const items = model.entities[entityType] || [];
|
|
228
182
|
const item = items.find(x => x.id === id) || items.find(x => x._originalId === id);
|
|
229
183
|
|
|
230
184
|
if (!item) {
|
|
231
|
-
console.error(
|
|
185
|
+
console.error(' Error: ' + entityType + ' "' + id + '" not found');
|
|
232
186
|
process.exit(1);
|
|
233
187
|
}
|
|
234
188
|
|
|
@@ -237,13 +191,25 @@ function handleUpdate(projectDir, args) {
|
|
|
237
191
|
process.exit(1);
|
|
238
192
|
}
|
|
239
193
|
|
|
240
|
-
// Convert
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|
|
244
210
|
|
|
245
|
-
|
|
246
|
-
console.log(
|
|
211
|
+
updateEntity(projectPath, item._file, updates);
|
|
212
|
+
console.log(' Updated ' + entityType + ' ' + id + ': ' + item._file);
|
|
247
213
|
}
|
|
248
214
|
|
|
249
215
|
/**
|
|
@@ -251,112 +217,122 @@ function handleUpdate(projectDir, args) {
|
|
|
251
217
|
*/
|
|
252
218
|
function handleDelete(projectDir, args) {
|
|
253
219
|
const { positional } = parseFlags(args);
|
|
254
|
-
const
|
|
220
|
+
const entityType = positional[0];
|
|
255
221
|
const id = positional[1];
|
|
256
222
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
mdboard delete — Delete an entity
|
|
223
|
+
const { model, config: cfg, projectPath } = buildModel(projectDir);
|
|
224
|
+
const availableTypes = configEngine.getEntityTypes(cfg);
|
|
260
225
|
|
|
261
|
-
|
|
262
|
-
mdboard delete
|
|
263
|
-
mdboard delete sprint SP-003
|
|
264
|
-
mdboard delete epic EPIC-002
|
|
265
|
-
mdboard delete milestone MS-001
|
|
266
|
-
`);
|
|
226
|
+
if (!entityType || !id) {
|
|
227
|
+
console.log('\n mdboard delete — Delete an entity\n\n Usage:\n mdboard delete <entity> <id>\n');
|
|
267
228
|
process.exit(1);
|
|
268
229
|
}
|
|
269
230
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const collectionMap = {
|
|
273
|
-
task: 'tasks',
|
|
274
|
-
milestone: 'milestones',
|
|
275
|
-
epic: 'epics',
|
|
276
|
-
sprint: 'sprints',
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const collection = collectionMap[entity];
|
|
280
|
-
if (!collection) {
|
|
281
|
-
console.error(` Error: unknown entity "${entity}". Use: task, milestone, epic, sprint`);
|
|
231
|
+
if (!availableTypes.includes(entityType)) {
|
|
232
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
282
233
|
process.exit(1);
|
|
283
234
|
}
|
|
284
235
|
|
|
285
|
-
const items = model[
|
|
236
|
+
const items = model.entities[entityType] || [];
|
|
286
237
|
const item = items.find(x => x.id === id) || items.find(x => x._originalId === id);
|
|
287
238
|
|
|
288
239
|
if (!item) {
|
|
289
|
-
console.error(
|
|
240
|
+
console.error(' Error: ' + entityType + ' "' + id + '" not found');
|
|
290
241
|
process.exit(1);
|
|
291
242
|
}
|
|
292
243
|
|
|
293
|
-
|
|
294
|
-
console.log(
|
|
244
|
+
deleteEntity(projectPath, item);
|
|
245
|
+
console.log(' Deleted ' + entityType + ' ' + id + ': ' + item._file);
|
|
295
246
|
|
|
296
|
-
// Clean
|
|
297
|
-
const cleaned = cleanReferences(projectPath, model,
|
|
247
|
+
// Clean ref fields pointing to this entity
|
|
248
|
+
const cleaned = cleanReferences(projectPath, model, cfg, entityType, item);
|
|
298
249
|
if (cleaned > 0) {
|
|
299
|
-
console.log(
|
|
250
|
+
console.log(' Cleaned ' + cleaned + ' reference' + (cleaned > 1 ? 's' : '') + '.');
|
|
300
251
|
}
|
|
301
252
|
}
|
|
302
253
|
|
|
303
254
|
/**
|
|
304
255
|
* Clean up dangling references after deleting an entity.
|
|
305
|
-
*
|
|
306
|
-
* @param {string} projectPath - Absolute path to project/ dir
|
|
307
|
-
* @param {object} model - Current model
|
|
308
|
-
* @param {object} config - Config object
|
|
309
|
-
* @param {string} entity - Entity type deleted (task, sprint, epic)
|
|
310
|
-
* @param {object} deletedItem - The deleted item
|
|
311
|
-
* @returns {number} - Number of references cleaned
|
|
256
|
+
* Dynamically checks all ref fields across all entity types.
|
|
312
257
|
*/
|
|
313
|
-
function cleanReferences(projectPath, model,
|
|
258
|
+
function cleanReferences(projectPath, model, cfg, deletedType, deletedItem) {
|
|
314
259
|
let cleaned = 0;
|
|
315
|
-
const deletedId = deletedItem.id;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
for (const
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
280
|
}
|
|
325
281
|
}
|
|
326
282
|
}
|
|
327
283
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
300
|
}
|
|
337
301
|
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (epic.dependencies && Array.isArray(epic.dependencies) && epic.dependencies.includes(deletedId)) {
|
|
342
|
-
const newDeps = epic.dependencies.filter(d => d !== deletedId);
|
|
343
|
-
updateMarkdownFile(projectPath, epic._file, { dependencies: newDeps });
|
|
344
|
-
cleaned++;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// Warn about orphaned tasks
|
|
348
|
-
const orphanedTasks = model.tasks.filter(t =>
|
|
349
|
-
t._epic === deletedItem._dir && t._milestone === deletedItem._milestone
|
|
350
|
-
);
|
|
351
|
-
if (orphanedTasks.length > 0) {
|
|
352
|
-
console.log(` Warning: ${orphanedTasks.length} task(s) under deleted epic may be orphaned.`);
|
|
353
|
-
}
|
|
302
|
+
if (!availableTypes.includes(entityType)) {
|
|
303
|
+
console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
|
|
304
|
+
process.exit(1);
|
|
354
305
|
}
|
|
355
306
|
|
|
356
|
-
|
|
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('');
|
|
357
333
|
}
|
|
358
334
|
|
|
359
335
|
module.exports = {
|
|
360
336
|
buildModel, parseFlags, resolveProjectDir,
|
|
361
|
-
handleCreate, handleUpdate, handleDelete, cleanReferences,
|
|
337
|
+
handleCreate, handleUpdate, handleDelete, handleList, cleanReferences,
|
|
362
338
|
};
|