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.
Files changed (53) hide show
  1. package/bin.js +117 -59
  2. package/index.html +2161 -1579
  3. package/package.json +7 -5
  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 +186 -210
  23. package/src/cli/config.js +234 -0
  24. package/src/cli/init.js +128 -76
  25. package/src/cli/preset.js +849 -0
  26. package/src/cli/skill.js +417 -0
  27. package/src/cli/status.js +126 -96
  28. package/src/core/config.js +491 -38
  29. package/src/core/history.js +17 -1
  30. package/src/core/scanner.js +373 -463
  31. package/src/core/workspace.js +0 -15
  32. package/src/server/api.js +464 -741
  33. package/src/server/server.js +105 -130
  34. package/build.js +0 -44
  35. package/defaults.json +0 -43
  36. package/src/cli/sync.js +0 -194
  37. package/src/cli/theme.js +0 -142
  38. package/src/client/app.js +0 -266
  39. package/src/client/board.js +0 -157
  40. package/src/client/core.js +0 -331
  41. package/src/client/editor.js +0 -318
  42. package/src/client/history.js +0 -137
  43. package/src/client/metrics.js +0 -38
  44. package/src/client/milestones.js +0 -77
  45. package/src/client/notes.js +0 -183
  46. package/src/client/overview.js +0 -104
  47. package/src/client/panel.js +0 -637
  48. package/src/client/styles.css +0 -471
  49. package/src/client/table.js +0 -111
  50. package/src/client/template.html +0 -144
  51. package/src/client/themes.js +0 -261
  52. package/src/client/workspace.js +0 -164
  53. 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
- * Provides buildModel() for shared use by status/sync/CRUD,
5
- * parseFlags() for CLI argument parsing, and
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 { loadConfig } = require('../core/config');
12
- const { loadWorkspace } = require('../core/workspace');
12
+ const configEngine = require('../core/config');
13
13
  const {
14
14
  createModel, scanSource, computeProgress, mergeResults,
15
- updateMarkdownFile, createTask, createMilestone, createEpic,
16
- createSprint, archiveItem,
15
+ createEntity, updateEntity, deleteEntity,
17
16
  } = require('../core/scanner');
18
- const { parseFrontmatter, serializeYaml } = require('../core/yaml');
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, workspace, sources }}
24
+ * @returns {{ model, config, projectPath }}
25
25
  */
26
26
  function buildModel(projectDir) {
27
27
  const projectPath = path.join(projectDir, 'project');
28
- const config = loadConfig(projectDir, process.env.MDBOARD_CONFIG);
29
- const wsPath = process.env.MDBOARD_WORKSPACE || null;
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
- computeProgress(model, config.completedStatus);
31
+ mergeResults(model, [scanSource(projectPath, cfg, {})]);
32
+ computeProgress(model, cfg);
56
33
 
57
- return { model, config, projectPath, workspace, sources };
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 entity = positional[0];
91
+ const entityType = positional[0];
120
92
  const title = positional.slice(1).join(' ');
121
93
 
122
- if (!entity) {
123
- console.log(`
124
- mdboard create — Create a new entity
94
+ const { model, config: cfg, projectPath } = buildModel(projectDir);
95
+ const availableTypes = configEngine.getEntityTypes(cfg);
125
96
 
126
- Usage:
127
- mdboard create milestone <title>
128
- mdboard create epic <title> --milestone <slug>
129
- mdboard create task <title> --milestone <slug> --epic <slug> [--priority p] [--points n] [--sprint id]
130
- mdboard create sprint --milestone <slug> [--goal "text"]
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
- const { model, config, projectPath } = buildModel(projectDir);
107
+ if (!availableTypes.includes(entityType)) {
108
+ console.error(' Error: unknown entity "' + entityType + '". Available: ' + availableTypes.join(', '));
109
+ process.exit(1);
110
+ }
136
111
 
137
- switch (entity) {
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
- case 'epic': {
146
- if (!title) { console.error(' Error: title is required'); process.exit(1); }
147
- if (!flags.milestone) { console.error(' Error: --milestone is required'); process.exit(1); }
148
- const result = createEpic(projectPath, config, {
149
- title,
150
- milestone: flags.milestone,
151
- }, model.epics);
152
- console.log(` Created epic ${result.id}: ${result.file}`);
153
- break;
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
- case 'task': {
157
- if (!title) { console.error(' Error: title is required'); process.exit(1); }
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
- case 'sprint': {
176
- if (!flags.milestone) { console.error(' Error: --milestone is required'); process.exit(1); }
177
- const data = { milestone: flags.milestone };
178
- if (flags.goal) data.goal = flags.goal;
179
- if (flags.status) data.status = flags.status;
180
- const result = createSprint(projectPath, config, data, model.sprints);
181
- console.log(` Created sprint ${result.id}: ${result.file}`);
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
- default:
186
- console.error(` Error: unknown entity "${entity}". Use: milestone, epic, task, sprint`);
187
- process.exit(1);
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 entity = positional[0];
165
+ const entityType = positional[0];
197
166
  const id = positional[1];
198
167
 
199
- if (!entity || !id) {
200
- console.log(`
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
- Usage:
204
- mdboard update task TASK-005 --status in-progress --assigned john
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
- const { model, projectPath } = buildModel(projectDir);
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[collection];
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(` Error: ${entity} "${id}" not found`);
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 numeric fields
241
- const updates = { ...flags };
242
- if (updates.points) updates.points = parseInt(updates.points, 10);
243
- if (updates.planned_points) updates.planned_points = parseInt(updates.planned_points, 10);
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
- updateMarkdownFile(projectPath, item._file, updates);
246
- console.log(` Updated ${entity} ${id}: ${item._file}`);
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 entity = positional[0];
220
+ const entityType = positional[0];
255
221
  const id = positional[1];
256
222
 
257
- if (!entity || !id) {
258
- console.log(`
259
- mdboard delete — Delete an entity
223
+ const { model, config: cfg, projectPath } = buildModel(projectDir);
224
+ const availableTypes = configEngine.getEntityTypes(cfg);
260
225
 
261
- Usage:
262
- mdboard delete task TASK-005
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
- const { model, config, projectPath } = buildModel(projectDir);
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[collection];
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(` Error: ${entity} "${id}" not found`);
240
+ console.error(' Error: ' + entityType + ' "' + id + '" not found');
290
241
  process.exit(1);
291
242
  }
292
243
 
293
- archiveItem(projectPath, item);
294
- console.log(` Deleted ${entity} ${id}: ${item._file}`);
244
+ deleteEntity(projectPath, item);
245
+ console.log(' Deleted ' + entityType + ' ' + id + ': ' + item._file);
295
246
 
296
- // Clean up references
297
- const cleaned = cleanReferences(projectPath, model, config, entity, item);
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(` Cleaned ${cleaned} reference${cleaned > 1 ? 's' : ''}.`);
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, config, entity, deletedItem) {
258
+ function cleanReferences(projectPath, model, cfg, deletedType, deletedItem) {
314
259
  let cleaned = 0;
315
- const deletedId = deletedItem.id;
316
-
317
- if (entity === 'task') {
318
- // Remove task from sprint.features[]
319
- for (const sprint of model.sprints) {
320
- if (sprint.features && Array.isArray(sprint.features) && sprint.features.includes(deletedId)) {
321
- const newFeatures = sprint.features.filter(f => f !== deletedId);
322
- updateMarkdownFile(projectPath, sprint._file, { features: newFeatures });
323
- cleaned++;
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
- if (entity === 'sprint') {
329
- // Clear task.sprint references
330
- for (const task of model.tasks) {
331
- if (task.sprint === deletedId) {
332
- updateMarkdownFile(projectPath, task._file, { sprint: null });
333
- cleaned++;
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 (entity === 'epic') {
339
- // Clean epic.dependencies[] references
340
- for (const epic of model.epics) {
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
- return cleaned;
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
  };