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/server/server.js
CHANGED
|
@@ -2,23 +2,21 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* mdboard — Project Dashboard Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Supports multi-repo workspaces via workspace.json.
|
|
5
|
+
* Config-driven server that reads markdown project management files
|
|
6
|
+
* and serves a JSON API. No hardcoded entity names.
|
|
9
7
|
*
|
|
10
8
|
* Usage:
|
|
11
9
|
* mdboard --project /path/to/workspace --port 3333
|
|
12
10
|
*/
|
|
13
11
|
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
14
|
const http = require('http');
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const { loadConfig, deepMerge } = require('../core/config');
|
|
19
|
-
const { createModel, scanSource, computeProgress,
|
|
20
|
-
createTask, createMilestone, createEpic, createSprint, createNote, archiveItem } = require('../core/scanner');
|
|
21
|
-
const { scanAgentConfigs } = require('../core/agent-scanner');
|
|
19
|
+
const { createModel, scanSource, computeProgress, updateEntity, createEntity, deleteEntity, mergeResults } = require('../core/scanner');
|
|
22
20
|
const { setupWatchers, closeWatchers } = require('./watcher');
|
|
23
21
|
const { handleApi } = require('./api');
|
|
24
22
|
const { loadWorkspace, syncAllRemotes, getGitAuthor } = require('../core/workspace');
|
|
@@ -58,7 +56,7 @@ let projectPath = path.join(projectDir, 'project');
|
|
|
58
56
|
const boardDir = path.resolve(__dirname, '..', '..');
|
|
59
57
|
|
|
60
58
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// Load configuration
|
|
59
|
+
// Load configuration (new config engine)
|
|
62
60
|
// ---------------------------------------------------------------------------
|
|
63
61
|
let config = loadConfig(projectDir, process.env.MDBOARD_CONFIG);
|
|
64
62
|
|
|
@@ -88,17 +86,10 @@ function broadcast(data) {
|
|
|
88
86
|
// ---------------------------------------------------------------------------
|
|
89
87
|
// Model & scanning — sourceStates tracks per-source state
|
|
90
88
|
// ---------------------------------------------------------------------------
|
|
91
|
-
let model = createModel();
|
|
89
|
+
let model = createModel(config);
|
|
92
90
|
const sourceStates = new Map();
|
|
93
|
-
let aiSuggestions = null;
|
|
94
|
-
|
|
95
|
-
function refreshAiSuggestions() {
|
|
96
|
-
try { aiSuggestions = scanAgentConfigs(projectDir); }
|
|
97
|
-
catch { aiSuggestions = { skills: [], agents: [], mcps: [], commands: [], context: [] }; }
|
|
98
|
-
}
|
|
99
91
|
|
|
100
92
|
function loadSourceConfig(source) {
|
|
101
|
-
// Try to load per-source mdboard.json
|
|
102
93
|
if (!source._repoRoot) return config;
|
|
103
94
|
const candidates = [
|
|
104
95
|
path.join(source._resolvedPath, 'mdboard.json'),
|
|
@@ -115,11 +106,10 @@ function loadSourceConfig(source) {
|
|
|
115
106
|
}
|
|
116
107
|
|
|
117
108
|
function scanAll() {
|
|
118
|
-
model = createModel();
|
|
109
|
+
model = createModel(config);
|
|
119
110
|
sourceStates.clear();
|
|
120
111
|
|
|
121
112
|
if (workspace && sources.length > 0) {
|
|
122
|
-
// Multi-source mode
|
|
123
113
|
const results = [];
|
|
124
114
|
for (const source of sources) {
|
|
125
115
|
const sourcePath = source._resolvedPath;
|
|
@@ -134,21 +124,21 @@ function scanAll() {
|
|
|
134
124
|
readonly,
|
|
135
125
|
};
|
|
136
126
|
|
|
137
|
-
// Load per-source config
|
|
138
127
|
const srcConfig = loadSourceConfig(source);
|
|
139
128
|
const result = scanSource(sourcePath, srcConfig, meta);
|
|
140
129
|
|
|
141
|
-
// Add author info if showAuthor is enabled
|
|
142
130
|
if (workspace.settings && workspace.settings.showAuthor) {
|
|
143
131
|
const repoRoot = source._repoRoot;
|
|
144
132
|
if (repoRoot) {
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
const leafType = getLeafType(srcConfig);
|
|
134
|
+
if (leafType && result.entities[leafType]) {
|
|
135
|
+
for (const item of result.entities[leafType]) {
|
|
136
|
+
item._author = getGitAuthor(repoRoot, item._file);
|
|
137
|
+
}
|
|
147
138
|
}
|
|
148
139
|
}
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
// Store per-source state
|
|
152
142
|
sourceStates.set(source.name, {
|
|
153
143
|
model: result,
|
|
154
144
|
config: srcConfig,
|
|
@@ -161,27 +151,59 @@ function scanAll() {
|
|
|
161
151
|
}
|
|
162
152
|
mergeResults(model, results);
|
|
163
153
|
} else {
|
|
164
|
-
// Legacy single-source mode
|
|
165
154
|
const result = scanSource(projectPath, config, {});
|
|
166
155
|
mergeResults(model, [result]);
|
|
167
156
|
}
|
|
168
157
|
|
|
169
|
-
computeProgress(model, config
|
|
170
|
-
|
|
171
|
-
|
|
158
|
+
computeProgress(model, config);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getLeafType(cfg) {
|
|
162
|
+
const { flattenHierarchy } = require('../core/config');
|
|
163
|
+
const flat = flattenHierarchy(cfg);
|
|
164
|
+
const h = flat.filter(e => !e.standalone);
|
|
165
|
+
return h.length > 0 ? h[h.length - 1].type : null;
|
|
172
166
|
}
|
|
173
167
|
|
|
174
168
|
// ---------------------------------------------------------------------------
|
|
175
|
-
// File update helper
|
|
169
|
+
// File update helper
|
|
176
170
|
// ---------------------------------------------------------------------------
|
|
177
171
|
function updateFile(sourceName, relFile, updates) {
|
|
172
|
+
let sourcePath = projectPath;
|
|
173
|
+
if (sourceName && sources.length > 0) {
|
|
174
|
+
const source = sources.find(s => s.name === sourceName);
|
|
175
|
+
if (source && source._resolvedPath) sourcePath = source._resolvedPath;
|
|
176
|
+
}
|
|
177
|
+
return updateEntity(sourcePath, relFile, updates);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// CRUD helpers — dynamic, uses entityType not collection name
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
function createItemFn(sourceName, entityType, data) {
|
|
184
|
+
let sourcePath = projectPath;
|
|
185
|
+
let cfg = config;
|
|
186
|
+
|
|
178
187
|
if (sourceName && sources.length > 0) {
|
|
179
188
|
const source = sources.find(s => s.name === sourceName);
|
|
180
189
|
if (source && source._resolvedPath) {
|
|
181
|
-
|
|
190
|
+
sourcePath = source._resolvedPath;
|
|
191
|
+
const state = sourceStates.get(sourceName);
|
|
192
|
+
if (state) cfg = state.config;
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
|
-
|
|
195
|
+
|
|
196
|
+
const existingItems = model.entities[entityType] || [];
|
|
197
|
+
return createEntity(sourcePath, cfg, entityType, data, existingItems);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function deleteItemFn(sourceName, item) {
|
|
201
|
+
let sourcePath = projectPath;
|
|
202
|
+
if (sourceName && sources.length > 0) {
|
|
203
|
+
const source = sources.find(s => s.name === sourceName);
|
|
204
|
+
if (source && source._resolvedPath) sourcePath = source._resolvedPath;
|
|
205
|
+
}
|
|
206
|
+
return deleteEntity(sourcePath, item);
|
|
185
207
|
}
|
|
186
208
|
|
|
187
209
|
// ---------------------------------------------------------------------------
|
|
@@ -189,7 +211,6 @@ function updateFile(sourceName, relFile, updates) {
|
|
|
189
211
|
// ---------------------------------------------------------------------------
|
|
190
212
|
async function doSyncRemotes() {
|
|
191
213
|
if (!workspace || sources.length === 0) return;
|
|
192
|
-
|
|
193
214
|
const remoteSources = sources.filter(s => s.type === 'remote');
|
|
194
215
|
if (remoteSources.length === 0) return;
|
|
195
216
|
|
|
@@ -235,6 +256,15 @@ function findCustomCss() {
|
|
|
235
256
|
return null;
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
// Cache index.html at startup
|
|
260
|
+
let _indexHtmlCache = null;
|
|
261
|
+
function getIndexHtml() {
|
|
262
|
+
if (!_indexHtmlCache) {
|
|
263
|
+
try { _indexHtmlCache = fs.readFileSync(path.join(boardDir, 'index.html')); } catch { /* */ }
|
|
264
|
+
}
|
|
265
|
+
return _indexHtmlCache;
|
|
266
|
+
}
|
|
267
|
+
|
|
238
268
|
function serveStatic(req, res) {
|
|
239
269
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
240
270
|
|
|
@@ -253,94 +283,19 @@ function serveStatic(req, res) {
|
|
|
253
283
|
return;
|
|
254
284
|
}
|
|
255
285
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (logoPath.startsWith(projectDir) && fs.existsSync(logoPath)) {
|
|
260
|
-
try {
|
|
261
|
-
const logoContent = fs.readFileSync(logoPath);
|
|
262
|
-
const logoExt = path.extname(logoPath);
|
|
263
|
-
const logoMime = MIME_TYPES[logoExt] || 'application/octet-stream';
|
|
264
|
-
res.writeHead(200, { 'Content-Type': logoMime });
|
|
265
|
-
res.end(logoContent);
|
|
266
|
-
return;
|
|
267
|
-
} catch { /* fall through */ }
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
res.writeHead(404);
|
|
271
|
-
res.end('Not found');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let filePath = path.join(boardDir, 'index.html');
|
|
276
|
-
if (!filePath.startsWith(boardDir)) {
|
|
277
|
-
res.writeHead(403);
|
|
278
|
-
res.end('Forbidden');
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const ext = path.extname(filePath);
|
|
283
|
-
const contentType = MIME_TYPES[ext] || 'text/plain';
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const content = fs.readFileSync(filePath);
|
|
287
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
286
|
+
const content = getIndexHtml();
|
|
287
|
+
if (content) {
|
|
288
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
288
289
|
res.end(content);
|
|
289
|
-
}
|
|
290
|
+
} else {
|
|
290
291
|
res.writeHead(404);
|
|
291
292
|
res.end('Not found');
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
// ---------------------------------------------------------------------------
|
|
296
|
-
//
|
|
297
|
-
// ---------------------------------------------------------------------------
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// CRUD helpers — resolve source path and delegate to scanner
|
|
297
|
+
// API context
|
|
300
298
|
// ---------------------------------------------------------------------------
|
|
301
|
-
function createItemFn(sourceName, collection, data) {
|
|
302
|
-
let sourcePath = projectPath;
|
|
303
|
-
let cfg = config;
|
|
304
|
-
|
|
305
|
-
if (sourceName && sources.length > 0) {
|
|
306
|
-
const source = sources.find(s => s.name === sourceName);
|
|
307
|
-
if (source && source._resolvedPath) {
|
|
308
|
-
sourcePath = source._resolvedPath;
|
|
309
|
-
const state = sourceStates.get(sourceName);
|
|
310
|
-
if (state) cfg = state.config;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Use ALL items across sources to compute next ID — prevents cross-source collisions
|
|
315
|
-
switch (collection) {
|
|
316
|
-
case 'tasks':
|
|
317
|
-
return createTask(sourcePath, cfg, data, model.tasks);
|
|
318
|
-
case 'milestones':
|
|
319
|
-
return createMilestone(sourcePath, cfg, data, model.milestones);
|
|
320
|
-
case 'epics':
|
|
321
|
-
return createEpic(sourcePath, cfg, data, model.epics);
|
|
322
|
-
case 'sprints':
|
|
323
|
-
return createSprint(sourcePath, cfg, data, model.sprints);
|
|
324
|
-
case 'notes':
|
|
325
|
-
return createNote(sourcePath, data, model.notes);
|
|
326
|
-
default:
|
|
327
|
-
throw new Error('Unknown collection: ' + collection);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function archiveItemFn(sourceName, item) {
|
|
332
|
-
let sourcePath = projectPath;
|
|
333
|
-
|
|
334
|
-
if (sourceName && sources.length > 0) {
|
|
335
|
-
const source = sources.find(s => s.name === sourceName);
|
|
336
|
-
if (source && source._resolvedPath) {
|
|
337
|
-
sourcePath = source._resolvedPath;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return archiveItem(sourcePath, item);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
299
|
const apiCtx = {
|
|
345
300
|
get model() { return model; },
|
|
346
301
|
get config() { return config; },
|
|
@@ -352,18 +307,33 @@ const apiCtx = {
|
|
|
352
307
|
broadcast,
|
|
353
308
|
updateFile,
|
|
354
309
|
rescanAll: () => scanAll(),
|
|
310
|
+
reloadConfig: () => reloadConfig(),
|
|
355
311
|
syncRemotes: doSyncRemotes,
|
|
356
312
|
createItem: createItemFn,
|
|
357
|
-
|
|
358
|
-
getAiSuggestions: () => aiSuggestions || { skills: [], agents: [], mcps: [], commands: [], context: [] },
|
|
313
|
+
deleteItem: deleteItemFn,
|
|
359
314
|
get hasProject() {
|
|
360
315
|
return fs.existsSync(path.join(projectDir, 'project')) ||
|
|
361
316
|
(workspace && sources.length > 0);
|
|
362
317
|
},
|
|
363
318
|
loadProject: loadProject,
|
|
364
319
|
getProjectHistory: () => getHistory(projectDir),
|
|
320
|
+
get workspacePath() {
|
|
321
|
+
return workspace ? workspace._path : (workspacePath || path.join(projectDir, 'workspace.json'));
|
|
322
|
+
},
|
|
323
|
+
onWorkspaceChange() {
|
|
324
|
+
workspace = loadWorkspace(projectDir, workspacePath);
|
|
325
|
+
sources = workspace ? workspace.sources : [];
|
|
326
|
+
closeWatchers(activeWatchers);
|
|
327
|
+
scanAll();
|
|
328
|
+
activeWatchers = setupWatchers(buildWatcherOpts());
|
|
329
|
+
doSyncRemotes().catch(() => {});
|
|
330
|
+
broadcast({ type: 'update', timestamp: new Date().toISOString() });
|
|
331
|
+
},
|
|
365
332
|
};
|
|
366
333
|
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// HTTP server
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
367
337
|
const server = http.createServer(async (req, res) => {
|
|
368
338
|
try {
|
|
369
339
|
if (req.url.startsWith('/api/')) {
|
|
@@ -428,44 +398,31 @@ if (isValidProject(projectDir)) {
|
|
|
428
398
|
|
|
429
399
|
/**
|
|
430
400
|
* Load a different project directory dynamically.
|
|
431
|
-
*
|
|
432
|
-
* @param {string} newProjectDir - Absolute path to the new project
|
|
433
401
|
*/
|
|
434
402
|
function loadProject(newProjectDir) {
|
|
435
|
-
// 1. Close existing watchers
|
|
436
403
|
closeWatchers(activeWatchers);
|
|
437
|
-
|
|
438
|
-
// 2. Stop sync interval
|
|
439
404
|
if (syncInterval) clearInterval(syncInterval);
|
|
440
405
|
syncInterval = null;
|
|
441
406
|
|
|
442
|
-
// 3. Update paths
|
|
443
407
|
projectDir = path.resolve(newProjectDir);
|
|
444
408
|
projectPath = path.join(projectDir, 'project');
|
|
445
409
|
workspacePath = null;
|
|
446
410
|
|
|
447
|
-
//
|
|
448
|
-
|
|
411
|
+
// Clear startup preset so each project resolves its own
|
|
412
|
+
delete process.env.MDBOARD_PRESET;
|
|
449
413
|
|
|
450
|
-
|
|
414
|
+
config = loadConfig(projectDir);
|
|
451
415
|
workspace = loadWorkspace(projectDir, null);
|
|
452
416
|
sources = workspace ? workspace.sources : [];
|
|
453
417
|
|
|
454
|
-
// 6. Rescan
|
|
455
418
|
scanAll();
|
|
456
|
-
|
|
457
|
-
// 7. Setup new watchers
|
|
458
419
|
activeWatchers = setupWatchers(buildWatcherOpts());
|
|
459
420
|
|
|
460
|
-
// 8. Restart remote sync
|
|
461
421
|
doSyncRemotes().catch(() => {});
|
|
462
422
|
startSyncInterval();
|
|
463
423
|
|
|
464
|
-
// 9. Register in history
|
|
465
424
|
const projName = (model.project && model.project.name) || path.basename(projectDir);
|
|
466
425
|
registerProject(projectDir, projName);
|
|
467
|
-
|
|
468
|
-
// 10. Broadcast to SSE clients
|
|
469
426
|
broadcast({ type: 'project-changed', projectDir });
|
|
470
427
|
}
|
|
471
428
|
|
|
@@ -478,11 +435,26 @@ server.listen(port, () => {
|
|
|
478
435
|
? `\n Sources: ${sources.map(s => s.label || s.name).join(', ')}`
|
|
479
436
|
: '';
|
|
480
437
|
const watchCount = activeWatchers.length;
|
|
438
|
+
|
|
439
|
+
// Resolve leaf type for tips
|
|
440
|
+
const { flattenHierarchy } = require('../core/config');
|
|
441
|
+
const flat = flattenHierarchy(config);
|
|
442
|
+
const hierarchy = flat.filter(e => !e.standalone);
|
|
443
|
+
const leafType = hierarchy.length > 0 ? hierarchy[hierarchy.length - 1].type : null;
|
|
444
|
+
|
|
481
445
|
console.log(`
|
|
482
446
|
mdboard — Project Dashboard
|
|
483
447
|
Project: ${projectDir}
|
|
484
|
-
Server: http://localhost:${port}${config.
|
|
448
|
+
Server: http://localhost:${port}${config._entitiesPath ? '\n Config: ' + config._entitiesPath : ''}${sourceInfo}
|
|
485
449
|
${watchCount > 0 ? '\n Watching ' + watchCount + ' director' + (watchCount === 1 ? 'y' : 'ies') + ' for changes...' : ''}
|
|
450
|
+
Open in browser:
|
|
451
|
+
http://localhost:${port}
|
|
452
|
+
|
|
453
|
+
Quick start:
|
|
454
|
+
mdboard config Setup preferences & AI skill
|
|
455
|
+
mdboard preset list Switch methodology (scrum, kanban, ...)
|
|
456
|
+
mdboard skill Add AI skill to your IDE${leafType ? '\n mdboard create ' + leafType + ' "title" Create a new ' + leafType : ''}
|
|
457
|
+
mdboard --help Show all commands
|
|
486
458
|
`);
|
|
487
459
|
});
|
|
488
460
|
|
|
@@ -509,3 +481,6 @@ process.on('SIGTERM', () => {
|
|
|
509
481
|
closeWatchers(activeWatchers);
|
|
510
482
|
server.close(() => process.exit(0));
|
|
511
483
|
});
|
|
484
|
+
|
|
485
|
+
// Export for programmatic use in tests
|
|
486
|
+
module.exports = { server, scanAll, apiCtx };
|
package/build.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
const CLIENT_DIR = path.join(__dirname, 'src', 'client');
|
|
8
|
-
const OUT_FILE = path.join(__dirname, 'index.html');
|
|
9
|
-
|
|
10
|
-
const JS_FILES = [
|
|
11
|
-
'themes.js',
|
|
12
|
-
'core.js',
|
|
13
|
-
'workspace.js',
|
|
14
|
-
'editor.js',
|
|
15
|
-
'board.js',
|
|
16
|
-
'table.js',
|
|
17
|
-
'milestones.js',
|
|
18
|
-
'metrics.js',
|
|
19
|
-
'overview.js',
|
|
20
|
-
'notes.js',
|
|
21
|
-
'panel.js',
|
|
22
|
-
'history.js',
|
|
23
|
-
'app.js',
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
// Read template
|
|
27
|
-
const template = fs.readFileSync(path.join(CLIENT_DIR, 'template.html'), 'utf8');
|
|
28
|
-
|
|
29
|
-
// Read CSS
|
|
30
|
-
const css = fs.readFileSync(path.join(CLIENT_DIR, 'styles.css'), 'utf8');
|
|
31
|
-
|
|
32
|
-
// Read and concatenate JS files
|
|
33
|
-
const js = JS_FILES
|
|
34
|
-
.map(f => fs.readFileSync(path.join(CLIENT_DIR, f), 'utf8'))
|
|
35
|
-
.join('\n\n');
|
|
36
|
-
|
|
37
|
-
// Replace placeholders
|
|
38
|
-
let output = template.replace('<!-- STYLES -->', css);
|
|
39
|
-
output = output.replace('<!-- SCRIPTS -->', js);
|
|
40
|
-
|
|
41
|
-
fs.writeFileSync(OUT_FILE, output, 'utf8');
|
|
42
|
-
|
|
43
|
-
const lines = output.split('\n').length;
|
|
44
|
-
console.log(`Built index.html (${lines} lines, ${Buffer.byteLength(output)} bytes)`);
|
package/defaults.json
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"entities": {
|
|
3
|
-
"milestone": { "singular": "Milestone", "plural": "Milestones", "dir": "milestones" },
|
|
4
|
-
"epic": { "singular": "Epic", "plural": "Epics", "dir": "epics" },
|
|
5
|
-
"task": { "singular": "Task", "plural": "Tasks", "dir": "backlog", "prefix": "TASK", "legacyPrefixes": ["FEAT"] },
|
|
6
|
-
"sprint": { "singular": "Sprint", "plural": "Sprints", "dir": "sprints" }
|
|
7
|
-
},
|
|
8
|
-
"statuses": {
|
|
9
|
-
"task": [
|
|
10
|
-
{ "key": "backlog", "label": "Backlog", "color": "#5A5A63", "icon": "dashed-circle" },
|
|
11
|
-
{ "key": "todo", "label": "Todo", "color": "#8B8B93", "icon": "circle" },
|
|
12
|
-
{ "key": "in-progress", "label": "In Progress", "color": "#D4A72C", "icon": "half-circle" },
|
|
13
|
-
{ "key": "in-review", "label": "In Review", "color": "#8B5CF6", "icon": "three-quarter-circle" },
|
|
14
|
-
{ "key": "done", "label": "Done", "color": "#2EA043", "icon": "check-circle" },
|
|
15
|
-
{ "key": "blocked", "label": "Blocked", "color": "#DA3633", "icon": "x-circle" },
|
|
16
|
-
{ "key": "cancelled", "label": "Cancelled", "color": "#5A5A63", "icon": "slash-circle" }
|
|
17
|
-
],
|
|
18
|
-
"milestone": [
|
|
19
|
-
{ "key": "planned", "label": "Planned", "color": "#8B8B93" },
|
|
20
|
-
{ "key": "active", "label": "Active", "color": "#5B6EF5" },
|
|
21
|
-
{ "key": "completed", "label": "Completed", "color": "#2EA043" }
|
|
22
|
-
],
|
|
23
|
-
"epic": [
|
|
24
|
-
{ "key": "active", "label": "Active", "color": "#5B6EF5" },
|
|
25
|
-
{ "key": "completed", "label": "Completed", "color": "#2EA043" },
|
|
26
|
-
{ "key": "blocked", "label": "Blocked", "color": "#DA3633" }
|
|
27
|
-
],
|
|
28
|
-
"sprint": [
|
|
29
|
-
{ "key": "planned", "label": "Planned", "color": "#8B8B93" },
|
|
30
|
-
{ "key": "active", "label": "Active", "color": "#5B6EF5" },
|
|
31
|
-
{ "key": "completed", "label": "Completed", "color": "#2EA043" },
|
|
32
|
-
{ "key": "cancelled", "label": "Cancelled", "color": "#DA3633" }
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
"boardColumns": ["backlog", "todo", "in-progress", "in-review", "done"],
|
|
36
|
-
"priorities": [
|
|
37
|
-
{ "key": "urgent", "label": "Urgent", "color": "#F97316", "bars": 4 },
|
|
38
|
-
{ "key": "high", "label": "High", "color": "#F97316", "bars": 3 },
|
|
39
|
-
{ "key": "medium", "label": "Medium", "color": "#D4A72C", "bars": 2 },
|
|
40
|
-
{ "key": "low", "label": "Low", "color": "#5B6EF5", "bars": 1 }
|
|
41
|
-
],
|
|
42
|
-
"completedStatus": "done"
|
|
43
|
-
}
|
package/src/cli/sync.js
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mdboard — Consistency checker
|
|
3
|
-
*
|
|
4
|
-
* Detects dangling references and inconsistencies in the project model.
|
|
5
|
-
* With --fix, auto-corrects fixable issues.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* mdboard sync Report issues
|
|
9
|
-
* mdboard sync --fix Auto-fix fixable issues
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const { buildModel } = require('./cli');
|
|
15
|
-
const { updateMarkdownFile } = require('../core/scanner');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Run consistency checks on the project model.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} projectDir - Workspace root directory
|
|
21
|
-
* @param {{ fix?: boolean }} opts - Options
|
|
22
|
-
*/
|
|
23
|
-
function runSync(projectDir, opts) {
|
|
24
|
-
const fix = opts && opts.fix;
|
|
25
|
-
const { model, config, projectPath } = buildModel(projectDir);
|
|
26
|
-
|
|
27
|
-
const issues = [];
|
|
28
|
-
|
|
29
|
-
const taskIds = new Set(model.tasks.map(t => t.id));
|
|
30
|
-
const sprintIds = new Set(model.sprints.map(s => s.id));
|
|
31
|
-
const epicIds = new Set(model.epics.map(e => e.id));
|
|
32
|
-
const milestoneIds = new Set(model.milestones.map(m => m.id));
|
|
33
|
-
|
|
34
|
-
// 1. sprint.features[] references tasks that don't exist
|
|
35
|
-
for (const sprint of model.sprints) {
|
|
36
|
-
if (!sprint.features || !Array.isArray(sprint.features)) continue;
|
|
37
|
-
const missing = sprint.features.filter(f => !taskIds.has(f));
|
|
38
|
-
if (missing.length > 0) {
|
|
39
|
-
issues.push({
|
|
40
|
-
type: 'error',
|
|
41
|
-
fixable: true,
|
|
42
|
-
message: `Sprint ${sprint.id}: features[] references missing tasks: ${missing.join(', ')}`,
|
|
43
|
-
fix: () => {
|
|
44
|
-
const newFeatures = sprint.features.filter(f => taskIds.has(f));
|
|
45
|
-
updateMarkdownFile(projectPath, sprint._file, { features: newFeatures });
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 2. task.sprint references sprint that doesn't exist
|
|
52
|
-
for (const task of model.tasks) {
|
|
53
|
-
if (!task.sprint) continue;
|
|
54
|
-
if (!sprintIds.has(task.sprint)) {
|
|
55
|
-
issues.push({
|
|
56
|
-
type: 'error',
|
|
57
|
-
fixable: true,
|
|
58
|
-
message: `Task ${task.id}: sprint "${task.sprint}" does not exist`,
|
|
59
|
-
fix: () => {
|
|
60
|
-
updateMarkdownFile(projectPath, task._file, { sprint: null });
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 3. epic.dependencies[] references epics that don't exist
|
|
67
|
-
for (const epic of model.epics) {
|
|
68
|
-
if (!epic.dependencies || !Array.isArray(epic.dependencies)) continue;
|
|
69
|
-
const missing = epic.dependencies.filter(d => !epicIds.has(d));
|
|
70
|
-
if (missing.length > 0) {
|
|
71
|
-
issues.push({
|
|
72
|
-
type: 'error',
|
|
73
|
-
fixable: true,
|
|
74
|
-
message: `Epic ${epic.id}: dependencies[] references missing epics: ${missing.join(', ')}`,
|
|
75
|
-
fix: () => {
|
|
76
|
-
const newDeps = epic.dependencies.filter(d => epicIds.has(d));
|
|
77
|
-
updateMarkdownFile(projectPath, epic._file, { dependencies: newDeps });
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 4. milestone.tracks[] references milestones that don't exist
|
|
84
|
-
for (const ms of model.milestones) {
|
|
85
|
-
if (!ms.tracks || !Array.isArray(ms.tracks)) continue;
|
|
86
|
-
const missing = ms.tracks.filter(t => {
|
|
87
|
-
// tracks can reference source:MS-ID format
|
|
88
|
-
if (String(t).includes(':')) {
|
|
89
|
-
const parts = String(t).split(':');
|
|
90
|
-
const refId = parts[1];
|
|
91
|
-
return !model.milestones.some(m => m.id === t || m._originalId === refId || m.id === refId);
|
|
92
|
-
}
|
|
93
|
-
return !milestoneIds.has(t);
|
|
94
|
-
});
|
|
95
|
-
if (missing.length > 0) {
|
|
96
|
-
issues.push({
|
|
97
|
-
type: 'warning',
|
|
98
|
-
fixable: false,
|
|
99
|
-
message: `Milestone ${ms.id}: tracks[] references missing milestones: ${missing.join(', ')}`,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 5. Duplicate IDs in the same collection
|
|
105
|
-
const collections = [
|
|
106
|
-
{ name: 'tasks', items: model.tasks },
|
|
107
|
-
{ name: 'milestones', items: model.milestones },
|
|
108
|
-
{ name: 'epics', items: model.epics },
|
|
109
|
-
{ name: 'sprints', items: model.sprints },
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
for (const col of collections) {
|
|
113
|
-
const seen = new Map();
|
|
114
|
-
for (const item of col.items) {
|
|
115
|
-
if (!item.id) continue;
|
|
116
|
-
if (seen.has(item.id)) {
|
|
117
|
-
issues.push({
|
|
118
|
-
type: 'error',
|
|
119
|
-
fixable: false,
|
|
120
|
-
message: `Duplicate ${col.name.slice(0, -1)} ID "${item.id}": ${seen.get(item.id)} and ${item._file}`,
|
|
121
|
-
});
|
|
122
|
-
} else {
|
|
123
|
-
seen.set(item.id, item._file);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 6. Tasks under epic directory without epic README.md
|
|
129
|
-
const msDir = config.entities.milestone.dir;
|
|
130
|
-
const epicDir = config.entities.epic.dir;
|
|
131
|
-
const taskDir = config.entities.task.dir;
|
|
132
|
-
|
|
133
|
-
for (const task of model.tasks) {
|
|
134
|
-
if (!task._epic || !task._milestone) continue;
|
|
135
|
-
const epicReadmePath = path.join(projectPath, msDir, task._milestone, epicDir, task._epic, 'README.md');
|
|
136
|
-
if (!fs.existsSync(epicReadmePath)) {
|
|
137
|
-
issues.push({
|
|
138
|
-
type: 'warning',
|
|
139
|
-
fixable: false,
|
|
140
|
-
message: `Task ${task.id}: parent epic directory "${task._epic}" has no README.md`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 7. Sprints under milestone without milestone README.md
|
|
146
|
-
for (const sprint of model.sprints) {
|
|
147
|
-
if (!sprint._milestone) continue;
|
|
148
|
-
const msReadmePath = path.join(projectPath, msDir, sprint._milestone, 'README.md');
|
|
149
|
-
if (!fs.existsSync(msReadmePath)) {
|
|
150
|
-
issues.push({
|
|
151
|
-
type: 'warning',
|
|
152
|
-
fixable: false,
|
|
153
|
-
message: `Sprint ${sprint.id}: parent milestone directory "${sprint._milestone}" has no README.md`,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Report
|
|
159
|
-
if (issues.length === 0) {
|
|
160
|
-
console.log('\n mdboard sync — No issues found.\n');
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const errors = issues.filter(i => i.type === 'error');
|
|
165
|
-
const warnings = issues.filter(i => i.type === 'warning');
|
|
166
|
-
const fixable = issues.filter(i => i.fixable);
|
|
167
|
-
|
|
168
|
-
console.log('');
|
|
169
|
-
for (const issue of issues) {
|
|
170
|
-
const prefix = issue.type === 'error' ? ' ERROR' : ' WARN ';
|
|
171
|
-
const suffix = issue.fixable ? ' [fixable]' : '';
|
|
172
|
-
console.log(`${prefix}: ${issue.message}${suffix}`);
|
|
173
|
-
}
|
|
174
|
-
console.log('');
|
|
175
|
-
|
|
176
|
-
if (fix && fixable.length > 0) {
|
|
177
|
-
for (const issue of fixable) {
|
|
178
|
-
issue.fix();
|
|
179
|
-
}
|
|
180
|
-
console.log(` mdboard sync --fix — Fixed ${fixable.length} issue${fixable.length > 1 ? 's' : ''}.`);
|
|
181
|
-
if (warnings.length > 0) {
|
|
182
|
-
console.log(` ${warnings.length} warning${warnings.length > 1 ? 's' : ''} remain (not auto-fixable).`);
|
|
183
|
-
}
|
|
184
|
-
console.log('');
|
|
185
|
-
} else {
|
|
186
|
-
console.log(` mdboard sync — ${issues.length} issue${issues.length > 1 ? 's' : ''} (${errors.length} error${errors.length > 1 ? 's' : ''}, ${warnings.length} warning${warnings.length > 1 ? 's' : ''}).${fixable.length > 0 ? ` ${fixable.length} fixable.` : ''}`);
|
|
187
|
-
if (fixable.length > 0) {
|
|
188
|
-
console.log(' Run `mdboard sync --fix` to auto-correct.');
|
|
189
|
-
}
|
|
190
|
-
console.log('');
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = { runSync };
|