mrmd-server 0.2.4 → 0.2.6
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/package.json +1 -1
- package/src/api/file.js +137 -0
- package/src/api/index.js +1 -0
- package/src/api/languagetool.js +119 -0
- package/src/api/project.js +95 -108
- package/src/api/runtime.js +204 -64
- package/src/api/settings.js +4 -4
- package/src/api/voice.js +406 -0
- package/src/cloud-seed.js +359 -0
- package/src/cloud-session-service.js +0 -85
- package/src/relay-bridge.js +301 -0
- package/src/runtime-tunnel-client.js +773 -0
- package/src/server.js +284 -1
- package/src/services.js +12 -0
- package/src/sync-manager.js +91 -0
- package/static/http-shim.js +112 -84
package/package.json
CHANGED
package/src/api/file.js
CHANGED
|
@@ -325,6 +325,143 @@ export function createFileRoutes(ctx) {
|
|
|
325
325
|
}
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* GET /api/browse?path=...&type=all|dir|file&show_hidden=true
|
|
330
|
+
* Browse the filesystem for the file picker.
|
|
331
|
+
* Returns { path, parent, entries: [{name, path, type, size?, modified?}] }
|
|
332
|
+
*/
|
|
333
|
+
router.get('/browse', async (req, res) => {
|
|
334
|
+
try {
|
|
335
|
+
const os = await import('os');
|
|
336
|
+
const fs = await import('fs/promises');
|
|
337
|
+
|
|
338
|
+
let browsePath = req.query.path || '~';
|
|
339
|
+
if (browsePath === '~') {
|
|
340
|
+
browsePath = ctx.projectDir || os.default.homedir();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const resolvedPath = path.resolve(browsePath);
|
|
344
|
+
const typeFilter = req.query.type || 'all'; // 'all', 'dir', 'file'
|
|
345
|
+
const showHidden = req.query.show_hidden === 'true';
|
|
346
|
+
|
|
347
|
+
let dirEntries;
|
|
348
|
+
try {
|
|
349
|
+
dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (err.code === 'ENOENT') {
|
|
352
|
+
return res.status(404).json({ error: 'Directory not found', path: resolvedPath });
|
|
353
|
+
}
|
|
354
|
+
if (err.code === 'EACCES') {
|
|
355
|
+
return res.status(403).json({ error: 'Permission denied', path: resolvedPath });
|
|
356
|
+
}
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const entries = [];
|
|
361
|
+
for (const entry of dirEntries) {
|
|
362
|
+
// Skip hidden files unless requested
|
|
363
|
+
if (!showHidden && entry.name.startsWith('.')) continue;
|
|
364
|
+
// Skip common uninteresting directories
|
|
365
|
+
if (entry.name === 'node_modules' || entry.name === '__pycache__' || entry.name === '.git') continue;
|
|
366
|
+
|
|
367
|
+
const isDir = entry.isDirectory();
|
|
368
|
+
const isFile = entry.isFile();
|
|
369
|
+
if (!isDir && !isFile) continue;
|
|
370
|
+
if (typeFilter === 'dir' && !isDir) continue;
|
|
371
|
+
if (typeFilter === 'file' && !isFile) continue;
|
|
372
|
+
|
|
373
|
+
const entryPath = path.join(resolvedPath, entry.name);
|
|
374
|
+
const item = {
|
|
375
|
+
name: entry.name,
|
|
376
|
+
path: entryPath,
|
|
377
|
+
type: isDir ? 'directory' : 'file',
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Add file metadata (best-effort, don't fail if stat errors)
|
|
381
|
+
if (isFile) {
|
|
382
|
+
try {
|
|
383
|
+
const stat = await fs.stat(entryPath);
|
|
384
|
+
item.size = stat.size;
|
|
385
|
+
item.modified = stat.mtime.toISOString();
|
|
386
|
+
} catch { /* ignore stat errors */ }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
entries.push(item);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Sort: directories first, then alphabetical
|
|
393
|
+
entries.sort((a, b) => {
|
|
394
|
+
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
395
|
+
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
396
|
+
return a.name.localeCompare(b.name);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const parent = path.dirname(resolvedPath);
|
|
400
|
+
res.json({
|
|
401
|
+
path: resolvedPath,
|
|
402
|
+
parent: parent !== resolvedPath ? parent : null,
|
|
403
|
+
entries,
|
|
404
|
+
});
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error('[file:browse]', err);
|
|
407
|
+
res.status(500).json({ error: err.message });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* POST /api/file/write-bytes
|
|
413
|
+
* Write raw bytes to a file (for drag-drop uploads from browser).
|
|
414
|
+
* Body: { filePath, bytes: number[] }
|
|
415
|
+
*/
|
|
416
|
+
router.post('/write-bytes', async (req, res) => {
|
|
417
|
+
try {
|
|
418
|
+
const { filePath, bytes } = req.body;
|
|
419
|
+
if (!filePath || !bytes) {
|
|
420
|
+
return res.status(400).json({ error: 'filePath and bytes required' });
|
|
421
|
+
}
|
|
422
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
423
|
+
await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
424
|
+
await fsPromises.writeFile(fullPath, Buffer.from(bytes));
|
|
425
|
+
|
|
426
|
+
ctx.eventBus.projectChanged(ctx.projectDir);
|
|
427
|
+
res.json({ success: true, path: fullPath });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error('[file:write-bytes]', err);
|
|
430
|
+
res.status(500).json({ error: err.message });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* POST /api/file/copy
|
|
436
|
+
* Copy a file or directory.
|
|
437
|
+
* Body: { fromPath, toPath }
|
|
438
|
+
*/
|
|
439
|
+
router.post('/copy', async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const { fromPath, toPath } = req.body;
|
|
442
|
+
if (!fromPath || !toPath) {
|
|
443
|
+
return res.status(400).json({ error: 'fromPath and toPath required' });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const fullFrom = resolvePath(ctx.projectDir, fromPath);
|
|
447
|
+
const fullTo = resolvePath(ctx.projectDir, toPath);
|
|
448
|
+
const stat = await fsPromises.stat(fullFrom);
|
|
449
|
+
|
|
450
|
+
if (stat.isDirectory()) {
|
|
451
|
+
await fsPromises.cp(fullFrom, fullTo, { recursive: true });
|
|
452
|
+
} else {
|
|
453
|
+
await fsPromises.mkdir(path.dirname(fullTo), { recursive: true });
|
|
454
|
+
await fsPromises.copyFile(fullFrom, fullTo);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
ctx.eventBus.projectChanged(ctx.projectDir);
|
|
458
|
+
res.json({ success: true, path: fullTo });
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error('[file:copy]', err);
|
|
461
|
+
res.status(500).json({ error: err.message });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
328
465
|
return router;
|
|
329
466
|
}
|
|
330
467
|
|
package/src/api/index.js
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LanguageTool API routes
|
|
3
|
+
*
|
|
4
|
+
* Mirrors electronAPI.languagetool.*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
|
|
9
|
+
export function createLanguageToolRoutes(ctx) {
|
|
10
|
+
const router = Router();
|
|
11
|
+
const { languageToolService, languageToolPreferencesService } = ctx;
|
|
12
|
+
|
|
13
|
+
router.get('/status', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
res.json(await languageToolService.status());
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error('[languagetool:status]', err);
|
|
18
|
+
res.status(500).json({ error: err.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
router.get('/languages', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
res.json(await languageToolService.languages());
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[languagetool:languages]', err);
|
|
27
|
+
res.status(500).json({ error: err.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
router.post('/check', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
res.json(await languageToolService.check(req.body || {}));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[languagetool:check]', err);
|
|
36
|
+
res.status(500).json({ error: err.message });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
router.get('/prefs', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const { documentPath, projectRoot } = req.query;
|
|
43
|
+
res.json(await languageToolPreferencesService.getForDocument({ documentPath, projectRoot }));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[languagetool:getPrefs]', err);
|
|
46
|
+
res.status(500).json({ error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.post('/prefs', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const { documentPath, patch = {}, projectRoot = null } = req.body || {};
|
|
53
|
+
res.json(await languageToolPreferencesService.setForDocument({ documentPath, patch, projectRoot }));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[languagetool:setPrefs]', err);
|
|
56
|
+
res.status(500).json({ error: err.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.post('/prefs/clear', async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { documentPath, projectRoot = null } = req.body || {};
|
|
63
|
+
res.json(await languageToolPreferencesService.clearDocumentOverrides({ documentPath, projectRoot }));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('[languagetool:clearPrefs]', err);
|
|
66
|
+
res.status(500).json({ error: err.message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
router.get('/dictionary', (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
res.json(languageToolPreferencesService.getDictionary());
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('[languagetool:getDictionary]', err);
|
|
75
|
+
res.status(500).json({ error: err.message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
router.post('/dictionary/add', (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { word } = req.body || {};
|
|
82
|
+
res.json(languageToolPreferencesService.addToDictionary(word));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error('[languagetool:addToDictionary]', err);
|
|
85
|
+
res.status(500).json({ error: err.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
router.post('/dictionary/remove', (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const { word } = req.body || {};
|
|
92
|
+
res.json(languageToolPreferencesService.removeFromDictionary(word));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error('[languagetool:removeFromDictionary]', err);
|
|
95
|
+
res.status(500).json({ error: err.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
router.get('/defaults', (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
res.json(languageToolPreferencesService.getDefaults());
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('[languagetool:getDefaults]', err);
|
|
104
|
+
res.status(500).json({ error: err.message });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
router.post('/defaults', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { patch = {} } = req.body || {};
|
|
111
|
+
res.json(languageToolPreferencesService.setDefaults(patch));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('[languagetool:setDefaults]', err);
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return router;
|
|
119
|
+
}
|
package/src/api/project.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
import { Router } from 'express';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
|
-
import { existsSync
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
12
|
import { watch } from 'chokidar';
|
|
13
|
-
import { Project } from 'mrmd-project';
|
|
14
13
|
|
|
15
14
|
const DOC_EXTENSIONS = ['.md', '.qmd'];
|
|
16
15
|
|
|
@@ -30,25 +29,61 @@ function stripDocExtension(fileName) {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
34
|
-
* Returns { root, config } or null if not in a project
|
|
32
|
+
* Resolve project root from a file path without requiring mrmd.md.
|
|
35
33
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
function findGitRoot(startDir) {
|
|
35
|
+
let current = path.resolve(startDir || '/');
|
|
36
|
+
while (true) {
|
|
37
|
+
if (existsSync(path.join(current, '.git'))) return current;
|
|
38
|
+
const parent = path.dirname(current);
|
|
39
|
+
if (!parent || parent === current) break;
|
|
40
|
+
current = parent;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
async function hasDocsInDir(dir) {
|
|
46
|
+
try {
|
|
47
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
48
|
+
return entries.some((entry) => entry.isFile() && isDocFile(entry.name));
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
async function detectProject(filePath) {
|
|
55
|
+
const abs = path.resolve(filePath);
|
|
56
|
+
let startDir = abs;
|
|
43
57
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
const stat = await fs.stat(abs);
|
|
59
|
+
if (stat.isFile()) startDir = path.dirname(abs);
|
|
60
|
+
} catch {
|
|
61
|
+
if (path.extname(abs)) startDir = path.dirname(abs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const gitRoot = findGitRoot(startDir);
|
|
65
|
+
if (gitRoot) {
|
|
66
|
+
return { root: gitRoot, config: { name: path.basename(gitRoot) || 'Project' } };
|
|
51
67
|
}
|
|
68
|
+
|
|
69
|
+
let candidate = startDir;
|
|
70
|
+
let current = startDir;
|
|
71
|
+
const homeDir = path.resolve(process.env.HOME || '/');
|
|
72
|
+
for (let i = 0; i < 4; i++) {
|
|
73
|
+
const parent = path.dirname(current);
|
|
74
|
+
if (!parent || parent === current || parent === '/' || parent === homeDir) break;
|
|
75
|
+
if (await hasDocsInDir(parent)) {
|
|
76
|
+
candidate = parent;
|
|
77
|
+
current = parent;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
root: candidate,
|
|
85
|
+
config: { name: path.basename(candidate) || 'Project' },
|
|
86
|
+
};
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
/**
|
|
@@ -75,7 +110,7 @@ export function createProjectRoutes(ctx) {
|
|
|
75
110
|
|
|
76
111
|
// Detect project from file path
|
|
77
112
|
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.projectDir || process.cwd(), filePath);
|
|
78
|
-
const detected = detectProject(resolvedPath);
|
|
113
|
+
const detected = await detectProject(resolvedPath);
|
|
79
114
|
|
|
80
115
|
if (detected) {
|
|
81
116
|
// Get or start sync server for this project
|
|
@@ -164,7 +199,7 @@ export function createProjectRoutes(ctx) {
|
|
|
164
199
|
* Mirrors: electronAPI.project.create(targetPath)
|
|
165
200
|
*
|
|
166
201
|
* Uses ProjectService.createProject() which:
|
|
167
|
-
* 1. Creates scaffold files (
|
|
202
|
+
* 1. Creates scaffold files (index + assets)
|
|
168
203
|
* 2. Creates venv
|
|
169
204
|
* 3. Installs mrmd-python
|
|
170
205
|
*/
|
|
@@ -202,6 +237,34 @@ export function createProjectRoutes(ctx) {
|
|
|
202
237
|
}
|
|
203
238
|
});
|
|
204
239
|
|
|
240
|
+
/**
|
|
241
|
+
* GET /api/project/raw-tree?root=...&showSystem=true&maxDepth=...
|
|
242
|
+
* Get raw file tree (all files, actual filenames) for file browser view.
|
|
243
|
+
* Mirrors: electronAPI.project.rawTree(root, showSystem, maxDepth)
|
|
244
|
+
*/
|
|
245
|
+
router.get('/raw-tree', async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const root = req.query.root || ctx.projectDir;
|
|
248
|
+
const showSystem = req.query.showSystem === 'true';
|
|
249
|
+
const maxDepth = Number.isFinite(Number(req.query.maxDepth)) ? Number(req.query.maxDepth) : undefined;
|
|
250
|
+
|
|
251
|
+
if (!root) {
|
|
252
|
+
return res.status(400).json({ error: 'root query parameter required' });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Use shared ProjectService implementation so Electron + server stay aligned.
|
|
256
|
+
const tree = await projectService.getRawTree(root, {
|
|
257
|
+
showSystem,
|
|
258
|
+
...(Number.isInteger(maxDepth) && maxDepth >= 0 ? { maxDepth } : {}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
res.json(tree);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error('[project:raw-tree]', err);
|
|
264
|
+
res.status(500).json({ error: err.message });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
205
268
|
/**
|
|
206
269
|
* POST /api/project/invalidate
|
|
207
270
|
* Invalidate cached project info
|
|
@@ -210,9 +273,11 @@ export function createProjectRoutes(ctx) {
|
|
|
210
273
|
router.post('/invalidate', async (req, res) => {
|
|
211
274
|
try {
|
|
212
275
|
const { projectRoot } = req.body;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
276
|
+
const root = projectRoot || ctx.projectDir;
|
|
277
|
+
if (root) {
|
|
278
|
+
projectService.invalidate(root);
|
|
279
|
+
}
|
|
280
|
+
ctx.eventBus.projectChanged(root);
|
|
216
281
|
res.json({ success: true });
|
|
217
282
|
} catch (err) {
|
|
218
283
|
console.error('[project:invalidate]', err);
|
|
@@ -243,7 +308,9 @@ export function createProjectRoutes(ctx) {
|
|
|
243
308
|
});
|
|
244
309
|
|
|
245
310
|
watcher.on('all', (event, filePath) => {
|
|
246
|
-
|
|
311
|
+
const isDirectoryEvent = !path.extname(filePath || '');
|
|
312
|
+
if (isDocFile(filePath) || isDirectoryEvent) {
|
|
313
|
+
projectService.invalidate(watchPath);
|
|
247
314
|
ctx.eventBus.projectChanged(watchPath);
|
|
248
315
|
}
|
|
249
316
|
});
|
|
@@ -283,29 +350,9 @@ export function createProjectRoutes(ctx) {
|
|
|
283
350
|
*/
|
|
284
351
|
async function getProjectInfo(filePath, defaultRoot) {
|
|
285
352
|
const resolvedPath = path.resolve(defaultRoot, filePath);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
let mrmdConfig = null;
|
|
290
|
-
|
|
291
|
-
for (let i = 0; i < 10; i++) {
|
|
292
|
-
const mrmdPath = path.join(projectRoot, 'mrmd.md');
|
|
293
|
-
try {
|
|
294
|
-
const content = await fs.readFile(mrmdPath, 'utf-8');
|
|
295
|
-
mrmdConfig = parseMrmdConfig(content);
|
|
296
|
-
break;
|
|
297
|
-
} catch {
|
|
298
|
-
const parent = path.dirname(projectRoot);
|
|
299
|
-
if (parent === projectRoot) break;
|
|
300
|
-
projectRoot = parent;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// If no mrmd.md found, use the provided directory
|
|
305
|
-
if (!mrmdConfig) {
|
|
306
|
-
projectRoot = defaultRoot;
|
|
307
|
-
mrmdConfig = { venv: '.venv' };
|
|
308
|
-
}
|
|
353
|
+
const detected = await detectProject(resolvedPath);
|
|
354
|
+
const projectRoot = detected?.root || path.dirname(resolvedPath);
|
|
355
|
+
const config = detected?.config || { name: path.basename(projectRoot) || 'Project' };
|
|
309
356
|
|
|
310
357
|
// Build nav tree
|
|
311
358
|
const navTree = await buildNavTree(projectRoot);
|
|
@@ -315,7 +362,7 @@ async function getProjectInfo(filePath, defaultRoot) {
|
|
|
315
362
|
|
|
316
363
|
return {
|
|
317
364
|
root: projectRoot,
|
|
318
|
-
config
|
|
365
|
+
config,
|
|
319
366
|
navTree,
|
|
320
367
|
files,
|
|
321
368
|
currentFile: resolvedPath,
|
|
@@ -337,67 +384,6 @@ function flattenNavTree(nodes, projectRoot) {
|
|
|
337
384
|
return files;
|
|
338
385
|
}
|
|
339
386
|
|
|
340
|
-
/**
|
|
341
|
-
* Parse mrmd.md config (from yaml config code blocks)
|
|
342
|
-
*/
|
|
343
|
-
function parseMrmdConfig(content) {
|
|
344
|
-
const config = { venv: '.venv' };
|
|
345
|
-
|
|
346
|
-
// Find all ```yaml config blocks and merge them
|
|
347
|
-
const configBlockRegex = /```yaml\s+config\n([\s\S]*?)```/g;
|
|
348
|
-
let match;
|
|
349
|
-
|
|
350
|
-
while ((match = configBlockRegex.exec(content)) !== null) {
|
|
351
|
-
const yaml = match[1];
|
|
352
|
-
|
|
353
|
-
// Parse name
|
|
354
|
-
const nameMatch = yaml.match(/^name:\s*["']?([^"'\n]+)["']?/m);
|
|
355
|
-
if (nameMatch) {
|
|
356
|
-
config.name = nameMatch[1].trim();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Parse venv (direct or under session.python)
|
|
360
|
-
const venvMatch = yaml.match(/^\s*venv:\s*["']?([^"'\n]+)["']?/m);
|
|
361
|
-
if (venvMatch) {
|
|
362
|
-
config.venv = venvMatch[1].trim();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Parse session config
|
|
366
|
-
const sessionMatch = yaml.match(/^session:\n([\s\S]*?)(?=^[^\s]|\Z)/m);
|
|
367
|
-
if (sessionMatch) {
|
|
368
|
-
config.session = {};
|
|
369
|
-
const sessionYaml = sessionMatch[1];
|
|
370
|
-
|
|
371
|
-
// Parse python session
|
|
372
|
-
const pythonMatch = sessionYaml.match(/^\s+python:\n([\s\S]*?)(?=^\s+\w+:|\Z)/m);
|
|
373
|
-
if (pythonMatch) {
|
|
374
|
-
config.session.python = {};
|
|
375
|
-
const pyYaml = pythonMatch[1];
|
|
376
|
-
const pyVenv = pyYaml.match(/venv:\s*["']?([^"'\n]+)["']?/);
|
|
377
|
-
if (pyVenv) config.session.python.venv = pyVenv[1].trim();
|
|
378
|
-
const pyCwd = pyYaml.match(/cwd:\s*["']?([^"'\n]+)["']?/);
|
|
379
|
-
if (pyCwd) config.session.python.cwd = pyCwd[1].trim();
|
|
380
|
-
const pyName = pyYaml.match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
381
|
-
if (pyName) config.session.python.name = pyName[1].trim();
|
|
382
|
-
const pyAutoStart = pyYaml.match(/auto_start:\s*(true|false)/);
|
|
383
|
-
if (pyAutoStart) config.session.python.auto_start = pyAutoStart[1] === 'true';
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Also check YAML frontmatter for backwards compatibility
|
|
389
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
390
|
-
if (fmMatch) {
|
|
391
|
-
const yaml = fmMatch[1];
|
|
392
|
-
const venvMatch = yaml.match(/venv:\s*(.+)/);
|
|
393
|
-
if (venvMatch && !config.venv) {
|
|
394
|
-
config.venv = venvMatch[1].trim();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return config;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
387
|
/**
|
|
402
388
|
* Build navigation tree for a project
|
|
403
389
|
*/
|
|
@@ -437,7 +423,7 @@ async function buildNavTree(projectRoot, relativePath = '') {
|
|
|
437
423
|
children,
|
|
438
424
|
});
|
|
439
425
|
}
|
|
440
|
-
} else if (isDocFile(entry.name)
|
|
426
|
+
} else if (isDocFile(entry.name)) {
|
|
441
427
|
nodes.push({
|
|
442
428
|
isFolder: false,
|
|
443
429
|
title: cleanName(stripDocExtension(entry.name)),
|
|
@@ -467,3 +453,4 @@ async function hasIndexFile(dirPath) {
|
|
|
467
453
|
function cleanName(name) {
|
|
468
454
|
return name.replace(/^\d+[-_.\s]*/, '');
|
|
469
455
|
}
|
|
456
|
+
|