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/core/config.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mdboard — Configuration
|
|
2
|
+
* mdboard — Configuration Engine
|
|
3
|
+
*
|
|
4
|
+
* Loads and resolves the 6 configuration files (entities, structure, cli, api, ui, docs).
|
|
5
|
+
* Provides the public API that the rest of the system consumes.
|
|
3
6
|
*
|
|
4
7
|
* Resolution order (highest priority first):
|
|
5
8
|
* 1. Explicit path via --config / MDBOARD_CONFIG
|
|
6
|
-
* 2. <projectDir>/project
|
|
7
|
-
* 3.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* 2. <projectDir>/project/<file>.json (per-project overrides)
|
|
10
|
+
* 3. preset directory (built-in)
|
|
11
|
+
*
|
|
12
|
+
* The preset is determined by entities.json methodology field, defaulting to "shape-up".
|
|
10
13
|
*/
|
|
11
14
|
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
12
17
|
const fs = require('fs');
|
|
13
18
|
const path = require('path');
|
|
14
19
|
const os = require('os');
|
|
15
20
|
|
|
16
|
-
const
|
|
21
|
+
const PRESETS_DIR = path.join(__dirname, '..', '..', 'presets');
|
|
22
|
+
const CONFIG_FILES = ['entities', 'structure', 'cli', 'api', 'ui', 'docs'];
|
|
23
|
+
const DEFAULT_PRESET = 'shape-up';
|
|
24
|
+
|
|
25
|
+
// --- Utilities ---
|
|
17
26
|
|
|
18
27
|
function deepMerge(target, source) {
|
|
19
28
|
const result = { ...target };
|
|
@@ -31,68 +40,512 @@ function deepMerge(target, source) {
|
|
|
31
40
|
|
|
32
41
|
function tryReadJson(filePath) {
|
|
33
42
|
try {
|
|
34
|
-
|
|
35
|
-
return JSON.parse(raw);
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
36
44
|
} catch {
|
|
37
45
|
return null;
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
// --- Loader ---
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load all configuration files for a project.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectDir - The workspace root directory
|
|
55
|
+
* @param {string} [explicitPath] - Explicit path to a config directory
|
|
56
|
+
* @returns {object} - Merged configuration object with all 6 configs
|
|
57
|
+
*/
|
|
41
58
|
function loadConfig(projectDir, explicitPath) {
|
|
42
|
-
|
|
59
|
+
// Determine which preset to use
|
|
60
|
+
// Priority: project mdboard.json > project entities.json > env var > default
|
|
61
|
+
let preset = process.env.MDBOARD_PRESET || DEFAULT_PRESET;
|
|
43
62
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
63
|
+
if (projectDir) {
|
|
64
|
+
const userMdboard = tryReadJson(path.join(projectDir, 'project', 'mdboard.json'));
|
|
65
|
+
if (userMdboard && userMdboard.preset) {
|
|
66
|
+
preset = userMdboard.preset;
|
|
67
|
+
}
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
const userEntities = projectDir
|
|
71
|
+
? tryReadJson(path.join(projectDir, 'project', 'entities.json'))
|
|
72
|
+
: null;
|
|
73
|
+
if (userEntities && userEntities.methodology) {
|
|
74
|
+
preset = userEntities.methodology;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
const
|
|
54
|
-
|
|
77
|
+
const presetDir = path.join(PRESETS_DIR, preset);
|
|
78
|
+
const config = { _preset: preset, _projectDir: projectDir };
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
for (const name of CONFIG_FILES) {
|
|
81
|
+
// Load preset base
|
|
82
|
+
const presetFile = tryReadJson(path.join(presetDir, name + '.json'));
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
// Load user overrides
|
|
85
|
+
const candidates = [];
|
|
86
|
+
if (explicitPath) {
|
|
87
|
+
candidates.push(path.join(path.resolve(explicitPath), name + '.json'));
|
|
88
|
+
}
|
|
89
|
+
if (projectDir) {
|
|
90
|
+
candidates.push(path.join(projectDir, 'project', name + '.json'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let userFile = null;
|
|
94
|
+
let userPath = null;
|
|
95
|
+
for (const p of candidates) {
|
|
96
|
+
const data = tryReadJson(p);
|
|
97
|
+
if (data) {
|
|
98
|
+
userFile = data;
|
|
99
|
+
userPath = p;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (presetFile && userFile) {
|
|
105
|
+
config[name] = deepMerge(presetFile, userFile);
|
|
106
|
+
} else {
|
|
107
|
+
config[name] = userFile || presetFile || {};
|
|
65
108
|
}
|
|
109
|
+
config['_' + name + 'Path'] = userPath || path.join(presetDir, name + '.json');
|
|
66
110
|
}
|
|
67
111
|
|
|
68
|
-
|
|
69
|
-
config.
|
|
112
|
+
// Also load legacy mdboard.json for theme resolution
|
|
113
|
+
config._theme = resolveTheme(projectDir, explicitPath);
|
|
114
|
+
|
|
70
115
|
return config;
|
|
71
116
|
}
|
|
72
117
|
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
// --- Entity API ---
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get entity definition by type name.
|
|
122
|
+
* @param {object} config - Loaded config
|
|
123
|
+
* @param {string} type - Entity type (e.g. "cycle", "task")
|
|
124
|
+
* @returns {object|null}
|
|
125
|
+
*/
|
|
126
|
+
function getEntity(config, type) {
|
|
127
|
+
return (config.entities && config.entities.entities && config.entities.entities[type]) || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all entity type names.
|
|
132
|
+
* @param {object} config - Loaded config
|
|
133
|
+
* @returns {string[]}
|
|
134
|
+
*/
|
|
135
|
+
function getEntityTypes(config) {
|
|
136
|
+
return config.entities && config.entities.entities
|
|
137
|
+
? Object.keys(config.entities.entities)
|
|
138
|
+
: [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get fields definition for an entity type.
|
|
143
|
+
* @param {object} config - Loaded config
|
|
144
|
+
* @param {string} type - Entity type
|
|
145
|
+
* @returns {object} - Field definitions
|
|
146
|
+
*/
|
|
147
|
+
function getFields(config, type) {
|
|
148
|
+
const entity = getEntity(config, type);
|
|
149
|
+
return entity ? entity.fields || {} : {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get status values for an entity type (if it has a status enum field).
|
|
154
|
+
* @param {object} config - Loaded config
|
|
155
|
+
* @param {string} type - Entity type
|
|
156
|
+
* @returns {object[]|null} - Array of status value objects
|
|
157
|
+
*/
|
|
158
|
+
function getStatuses(config, type) {
|
|
159
|
+
const fields = getFields(config, type);
|
|
160
|
+
return fields.status && fields.status.type === 'enum' ? fields.status.values : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if a status key is considered "completed".
|
|
165
|
+
* @param {object} config - Loaded config
|
|
166
|
+
* @param {string} statusKey - Status key to check
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function isCompletedStatus(config, statusKey) {
|
|
170
|
+
const completed = (config.entities && config.entities.completedStatuses) || [];
|
|
171
|
+
return completed.includes(statusKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get priority definitions.
|
|
176
|
+
* @param {object} config - Loaded config
|
|
177
|
+
* @returns {object[]}
|
|
178
|
+
*/
|
|
179
|
+
function getPriorities(config) {
|
|
180
|
+
return (config.entities && config.entities.priorities) || [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Hierarchy API ---
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the hierarchy tree from structure config.
|
|
187
|
+
* @param {object} config - Loaded config
|
|
188
|
+
* @returns {object} - Hierarchy tree
|
|
189
|
+
*/
|
|
190
|
+
function getHierarchy(config) {
|
|
191
|
+
return (config.structure && config.structure.hierarchy) || {};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the project root directory name.
|
|
196
|
+
* @param {object} config - Loaded config
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
function getRoot(config) {
|
|
200
|
+
return (config.structure && config.structure.root) || 'project';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Flatten the hierarchy into an ordered array of { type, dir, depth, parent }.
|
|
205
|
+
* @param {object} config - Loaded config
|
|
206
|
+
* @returns {object[]}
|
|
207
|
+
*/
|
|
208
|
+
function flattenHierarchy(config) {
|
|
209
|
+
const result = [];
|
|
210
|
+
|
|
211
|
+
function walk(node, depth, parentType) {
|
|
212
|
+
for (const [type, def] of Object.entries(node)) {
|
|
213
|
+
result.push({ type, dir: def.dir, depth, parent: parentType });
|
|
214
|
+
if (def.children) {
|
|
215
|
+
walk(def.children, depth + 1, type);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
walk(getHierarchy(config), 0, null);
|
|
75
221
|
|
|
76
|
-
|
|
77
|
-
|
|
222
|
+
// Also add standalone entities
|
|
223
|
+
const standalone = (config.structure && config.structure.standalone) || {};
|
|
224
|
+
for (const [type, def] of Object.entries(standalone)) {
|
|
225
|
+
result.push({ type, dir: def.dir, depth: 0, parent: null, standalone: true });
|
|
78
226
|
}
|
|
79
227
|
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the parent type for an entity type.
|
|
233
|
+
* @param {object} config - Loaded config
|
|
234
|
+
* @param {string} type - Entity type
|
|
235
|
+
* @returns {string|null}
|
|
236
|
+
*/
|
|
237
|
+
function getParent(config, type) {
|
|
238
|
+
const flat = flattenHierarchy(config);
|
|
239
|
+
const entry = flat.find(e => e.type === type);
|
|
240
|
+
return entry ? entry.parent : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the direct children types for an entity type.
|
|
245
|
+
* @param {object} config - Loaded config
|
|
246
|
+
* @param {string} type - Entity type
|
|
247
|
+
* @returns {string[]}
|
|
248
|
+
*/
|
|
249
|
+
function getChildren(config, type) {
|
|
250
|
+
const flat = flattenHierarchy(config);
|
|
251
|
+
return flat.filter(e => e.parent === type).map(e => e.type);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get all ancestor types for an entity type (from immediate parent to root).
|
|
256
|
+
* @param {object} config - Loaded config
|
|
257
|
+
* @param {string} type - Entity type
|
|
258
|
+
* @returns {string[]}
|
|
259
|
+
*/
|
|
260
|
+
function getAncestors(config, type) {
|
|
261
|
+
const ancestors = [];
|
|
262
|
+
let current = type;
|
|
263
|
+
while (true) {
|
|
264
|
+
const parent = getParent(config, current);
|
|
265
|
+
if (!parent) break;
|
|
266
|
+
ancestors.push(parent);
|
|
267
|
+
current = parent;
|
|
268
|
+
}
|
|
269
|
+
return ancestors;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve the filesystem path for an entity instance.
|
|
274
|
+
*
|
|
275
|
+
* @param {object} config - Loaded config
|
|
276
|
+
* @param {string} type - Entity type to resolve
|
|
277
|
+
* @param {object} ids - Map of entity type → slug/id for each ancestor
|
|
278
|
+
* e.g. { cycle: "cycle-01", bet: "config-engine", scope: "schema" }
|
|
279
|
+
* @returns {string} - Resolved path relative to project root
|
|
280
|
+
*
|
|
281
|
+
* Example for task in shape-up:
|
|
282
|
+
* resolveEntityPath(config, "task", { cycle: "cycle-01", bet: "config-engine", scope: "schema" })
|
|
283
|
+
* → "cycles/cycle-01/bets/config-engine/scopes/schema/tasks"
|
|
284
|
+
*/
|
|
285
|
+
function resolveEntityPath(config, type, ids) {
|
|
286
|
+
ids = ids || {};
|
|
287
|
+
const root = getRoot(config);
|
|
288
|
+
|
|
289
|
+
// Check standalone first
|
|
290
|
+
const standalone = (config.structure && config.structure.standalone) || {};
|
|
291
|
+
if (standalone[type]) {
|
|
292
|
+
return path.join(root, standalone[type].dir);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Walk the hierarchy to build the path
|
|
296
|
+
const segments = [root];
|
|
297
|
+
|
|
298
|
+
function walk(node, targetType) {
|
|
299
|
+
for (const [nodeType, def] of Object.entries(node)) {
|
|
300
|
+
segments.push(def.dir);
|
|
301
|
+
if (ids[nodeType]) {
|
|
302
|
+
segments.push(ids[nodeType]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (nodeType === targetType) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (def.children) {
|
|
310
|
+
if (walk(def.children, targetType)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Backtrack
|
|
316
|
+
if (ids[nodeType]) segments.pop();
|
|
317
|
+
segments.pop();
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
walk(getHierarchy(config), type);
|
|
323
|
+
return segments.join('/');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Resolve the full file path for a new entity instance.
|
|
328
|
+
*
|
|
329
|
+
* @param {object} config - Loaded config
|
|
330
|
+
* @param {string} type - Entity type
|
|
331
|
+
* @param {string} slug - Slug for the new entity
|
|
332
|
+
* @param {object} ids - Ancestor ids map
|
|
333
|
+
* @returns {string} - Full relative file path
|
|
334
|
+
*/
|
|
335
|
+
function resolveEntityFile(config, type, slug, ids) {
|
|
336
|
+
const entity = getEntity(config, type);
|
|
337
|
+
if (!entity) return null;
|
|
338
|
+
|
|
339
|
+
const dirPath = resolveEntityPath(config, type, ids);
|
|
340
|
+
const filePattern = entity.file || 'README.md';
|
|
341
|
+
|
|
342
|
+
if (filePattern === 'README.md') {
|
|
343
|
+
// Directory-based entity: create dir/slug/README.md
|
|
344
|
+
return path.join(dirPath, slug, 'README.md');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (filePattern === 'PREFIX-NNN.md') {
|
|
348
|
+
// File-based entity: create dir/PREFIX-NNN.md (caller resolves NNN)
|
|
349
|
+
return dirPath; // Return the directory; caller adds the filename
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return path.join(dirPath, filePattern);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if an entity type is standalone (not in hierarchy).
|
|
357
|
+
* @param {object} config - Loaded config
|
|
358
|
+
* @param {string} type - Entity type
|
|
359
|
+
* @returns {boolean}
|
|
360
|
+
*/
|
|
361
|
+
function isStandalone(config, type) {
|
|
362
|
+
const standalone = (config.structure && config.structure.standalone) || {};
|
|
363
|
+
return !!standalone[type];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- Ref resolution ---
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Get all ref fields for an entity type (fields that reference other entities).
|
|
370
|
+
* @param {object} config - Loaded config
|
|
371
|
+
* @param {string} type - Entity type
|
|
372
|
+
* @returns {object[]} - Array of { fieldName, targetEntity, multiple }
|
|
373
|
+
*/
|
|
374
|
+
function getRefFields(config, type) {
|
|
375
|
+
const fields = getFields(config, type);
|
|
376
|
+
const refs = [];
|
|
377
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
378
|
+
if (def.type === 'ref') {
|
|
379
|
+
refs.push({ fieldName: name, targetEntity: def.entity, multiple: !!def.multiple });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return refs;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get all filterable fields for an entity type (enums + refs + ancestors).
|
|
387
|
+
* Used by API autoFilters and UI auto filters.
|
|
388
|
+
* @param {object} config - Loaded config
|
|
389
|
+
* @param {string} type - Entity type
|
|
390
|
+
* @returns {string[]} - Field names that can be used as filters
|
|
391
|
+
*/
|
|
392
|
+
function getFilterableFields(config, type) {
|
|
393
|
+
const fields = getFields(config, type);
|
|
394
|
+
const filters = [];
|
|
395
|
+
|
|
396
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
397
|
+
if (def.type === 'enum' || def.type === 'ref') {
|
|
398
|
+
filters.push(name);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Add ancestor types as implicit filters
|
|
403
|
+
const ancestors = getAncestors(config, type);
|
|
404
|
+
for (const ancestor of ancestors) {
|
|
405
|
+
if (!filters.includes(ancestor)) {
|
|
406
|
+
filters.push(ancestor);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return filters;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- CLI helpers ---
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get CLI-available entities for a command.
|
|
417
|
+
* @param {object} config - Loaded config
|
|
418
|
+
* @param {string} command - Command name (create, update, delete)
|
|
419
|
+
* @returns {string[]}
|
|
420
|
+
*/
|
|
421
|
+
function getCliEntities(config, command) {
|
|
422
|
+
const cmd = config.cli && config.cli.commands && config.cli.commands[command];
|
|
423
|
+
return cmd ? cmd.entities || [] : [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Generate CLI flags for an entity type.
|
|
428
|
+
* Returns auto-generated flags from entity fields + ancestor flags from hierarchy.
|
|
429
|
+
* @param {object} config - Loaded config
|
|
430
|
+
* @param {string} type - Entity type
|
|
431
|
+
* @returns {object[]} - Array of { flag, field, type, required, values }
|
|
432
|
+
*/
|
|
433
|
+
function getCliFlags(config, type) {
|
|
434
|
+
const flags = [];
|
|
435
|
+
const fields = getFields(config, type);
|
|
436
|
+
|
|
437
|
+
// Flags from entity fields
|
|
438
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
439
|
+
const flag = '--' + name.replace(/_/g, '-');
|
|
440
|
+
flags.push({
|
|
441
|
+
flag,
|
|
442
|
+
field: name,
|
|
443
|
+
type: def.type,
|
|
444
|
+
label: def.label || name,
|
|
445
|
+
values: def.type === 'enum' ? def.values.map(v => v.key) : undefined
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Flags from parent entities in hierarchy
|
|
450
|
+
const ancestors = getAncestors(config, type);
|
|
451
|
+
for (const ancestor of ancestors) {
|
|
452
|
+
const flag = '--' + ancestor.replace(/_/g, '-');
|
|
453
|
+
flags.push({
|
|
454
|
+
flag,
|
|
455
|
+
field: ancestor,
|
|
456
|
+
type: 'parent-ref',
|
|
457
|
+
label: getEntity(config, ancestor).singular,
|
|
458
|
+
required: true
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return flags;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// --- UI helpers ---
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get UI tab definitions.
|
|
469
|
+
* @param {object} config - Loaded config
|
|
470
|
+
* @returns {object[]}
|
|
471
|
+
*/
|
|
472
|
+
function getUiTabs(config) {
|
|
473
|
+
return (config.ui && config.ui.tabs) || [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get board columns (status values) for a board view entity.
|
|
478
|
+
* @param {object} config - Loaded config
|
|
479
|
+
* @param {string} type - Entity type displayed in board
|
|
480
|
+
* @returns {object[]}
|
|
481
|
+
*/
|
|
482
|
+
function getBoardColumns(config, type) {
|
|
483
|
+
const statuses = getStatuses(config, type);
|
|
484
|
+
if (!statuses) return [];
|
|
485
|
+
return statuses.filter(s => !isCompletedStatus(config, s.key) || s.key === 'done');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- Theme ---
|
|
489
|
+
|
|
490
|
+
function resolveTheme(projectDir, explicitPath) {
|
|
491
|
+
const candidates = [];
|
|
492
|
+
if (explicitPath) candidates.push(path.join(path.resolve(explicitPath), 'mdboard.json'));
|
|
80
493
|
if (projectDir) {
|
|
81
494
|
candidates.push(path.join(projectDir, 'project', 'mdboard.json'));
|
|
82
495
|
candidates.push(path.join(projectDir, 'mdboard.json'));
|
|
83
496
|
}
|
|
84
|
-
|
|
85
|
-
const globalPath = path.join(os.homedir(), '.config', 'mdboard', 'mdboard.json');
|
|
86
|
-
candidates.push(globalPath);
|
|
497
|
+
candidates.push(path.join(os.homedir(), '.config', 'mdboard', 'mdboard.json'));
|
|
87
498
|
|
|
88
499
|
for (const p of candidates) {
|
|
89
500
|
const data = tryReadJson(p);
|
|
90
|
-
if (data && data.theme)
|
|
91
|
-
return data.theme;
|
|
92
|
-
}
|
|
501
|
+
if (data && data.theme) return data.theme;
|
|
93
502
|
}
|
|
94
|
-
|
|
95
503
|
return null;
|
|
96
504
|
}
|
|
97
505
|
|
|
98
|
-
|
|
506
|
+
// --- Export ---
|
|
507
|
+
|
|
508
|
+
module.exports = {
|
|
509
|
+
// Loader
|
|
510
|
+
loadConfig,
|
|
511
|
+
deepMerge,
|
|
512
|
+
|
|
513
|
+
// Entity API
|
|
514
|
+
getEntity,
|
|
515
|
+
getEntityTypes,
|
|
516
|
+
getFields,
|
|
517
|
+
getStatuses,
|
|
518
|
+
isCompletedStatus,
|
|
519
|
+
getPriorities,
|
|
520
|
+
|
|
521
|
+
// Hierarchy API
|
|
522
|
+
getHierarchy,
|
|
523
|
+
getRoot,
|
|
524
|
+
flattenHierarchy,
|
|
525
|
+
getParent,
|
|
526
|
+
getChildren,
|
|
527
|
+
getAncestors,
|
|
528
|
+
resolveEntityPath,
|
|
529
|
+
resolveEntityFile,
|
|
530
|
+
isStandalone,
|
|
531
|
+
|
|
532
|
+
// Ref & filter API
|
|
533
|
+
getRefFields,
|
|
534
|
+
getFilterableFields,
|
|
535
|
+
|
|
536
|
+
// CLI helpers
|
|
537
|
+
getCliEntities,
|
|
538
|
+
getCliFlags,
|
|
539
|
+
|
|
540
|
+
// UI helpers
|
|
541
|
+
getUiTabs,
|
|
542
|
+
getBoardColumns,
|
|
543
|
+
|
|
544
|
+
// Theme
|
|
545
|
+
resolveTheme,
|
|
546
|
+
|
|
547
|
+
// Constants
|
|
548
|
+
PRESETS_DIR,
|
|
549
|
+
CONFIG_FILES,
|
|
550
|
+
DEFAULT_PRESET,
|
|
551
|
+
};
|
package/src/core/history.js
CHANGED
|
@@ -127,4 +127,20 @@ function removeFromHistory(projectPath) {
|
|
|
127
127
|
writeHistory(entries);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Remove history entries whose directories no longer exist on disk.
|
|
132
|
+
* @returns {{ removed: string[], kept: number }}
|
|
133
|
+
*/
|
|
134
|
+
function pruneHistory() {
|
|
135
|
+
const entries = readHistory();
|
|
136
|
+
const removed = [];
|
|
137
|
+
const kept = entries.filter(e => {
|
|
138
|
+
if (fs.existsSync(e.path)) return true;
|
|
139
|
+
removed.push(e.path);
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
if (removed.length > 0) writeHistory(kept);
|
|
143
|
+
return { removed, kept: kept.length };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { readHistory, registerProject, getHistory, isValidProject, removeFromHistory, pruneHistory };
|