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,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
+ };