mrmd-server 0.1.12 → 0.1.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrmd-server",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "HTTP server for mrmd - run mrmd in any browser, access from anywhere",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/api/asset.js CHANGED
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * Asset API routes
3
3
  *
4
- * Mirrors electronAPI.asset.*
4
+ * Mirrors electronAPI.asset.* using AssetService from mrmd-electron
5
5
  */
6
6
 
7
7
  import { Router } from 'express';
8
8
  import path from 'path';
9
- import fs from 'fs/promises';
10
- import crypto from 'crypto';
11
9
  import multer from 'multer';
12
10
 
13
11
  // Configure multer for file uploads
@@ -22,6 +20,7 @@ const upload = multer({
22
20
  */
23
21
  export function createAssetRoutes(ctx) {
24
22
  const router = Router();
23
+ const { assetService } = ctx;
25
24
 
26
25
  /**
27
26
  * GET /api/asset?projectRoot=...
@@ -31,33 +30,8 @@ export function createAssetRoutes(ctx) {
31
30
  router.get('/', async (req, res) => {
32
31
  try {
33
32
  const projectRoot = req.query.projectRoot || ctx.projectDir;
34
- const assetsDir = path.join(projectRoot, '_assets');
35
-
36
- try {
37
- const files = await fs.readdir(assetsDir);
38
- const assets = [];
39
-
40
- for (const file of files) {
41
- if (file.startsWith('.')) continue;
42
-
43
- const filePath = path.join(assetsDir, file);
44
- const stat = await fs.stat(filePath);
45
-
46
- assets.push({
47
- name: file,
48
- path: `_assets/${file}`,
49
- size: stat.size,
50
- modified: stat.mtime.toISOString(),
51
- });
52
- }
53
-
54
- res.json(assets);
55
- } catch (err) {
56
- if (err.code === 'ENOENT') {
57
- return res.json([]);
58
- }
59
- throw err;
60
- }
33
+ const assets = await assetService.list(projectRoot);
34
+ res.json(assets);
61
35
  } catch (err) {
62
36
  console.error('[asset:list]', err);
63
37
  res.status(500).json({ error: err.message });
@@ -94,36 +68,9 @@ export function createAssetRoutes(ctx) {
94
68
  return res.status(400).json({ error: 'No file provided' });
95
69
  }
96
70
 
97
- const assetsDir = path.join(projectRoot, '_assets');
98
- await fs.mkdir(assetsDir, { recursive: true });
99
-
100
- // Compute hash for deduplication
101
- const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex').slice(0, 8);
102
- const ext = path.extname(filename);
103
- const base = path.basename(filename, ext);
104
-
105
- // Check if identical file already exists
106
- const existingFiles = await fs.readdir(assetsDir).catch(() => []);
107
- for (const existing of existingFiles) {
108
- if (existing.startsWith(hash + '-')) {
109
- // Found duplicate
110
- return res.json({
111
- path: `_assets/${existing}`,
112
- deduplicated: true,
113
- });
114
- }
115
- }
116
-
117
- // Save with hash prefix
118
- const finalName = `${hash}-${base}${ext}`;
119
- const finalPath = path.join(assetsDir, finalName);
120
-
121
- await fs.writeFile(finalPath, fileBuffer);
122
-
123
- res.json({
124
- path: `_assets/${finalName}`,
125
- deduplicated: false,
126
- });
71
+ // AssetService.save expects a file-like object
72
+ const result = await assetService.save(projectRoot, fileBuffer, filename);
73
+ res.json(result);
127
74
  } catch (err) {
128
75
  console.error('[asset:save]', err);
129
76
  res.status(500).json({ error: err.message });
@@ -162,40 +109,8 @@ export function createAssetRoutes(ctx) {
162
109
  router.get('/orphans', async (req, res) => {
163
110
  try {
164
111
  const projectRoot = req.query.projectRoot || ctx.projectDir;
165
- const assetsDir = path.join(projectRoot, '_assets');
166
-
167
- // Get all assets
168
- let assetFiles;
169
- try {
170
- assetFiles = await fs.readdir(assetsDir);
171
- } catch {
172
- return res.json([]);
173
- }
174
-
175
- // Get all markdown files
176
- const mdFiles = await scanMarkdownFiles(projectRoot);
177
-
178
- // Read all markdown content and find referenced assets
179
- const referencedAssets = new Set();
180
- for (const mdFile of mdFiles) {
181
- try {
182
- const content = await fs.readFile(mdFile, 'utf-8');
183
- // Find asset references (images, links)
184
- const matches = content.matchAll(/!\[.*?\]\(([^)]+)\)|href="([^"]+)"/g);
185
- for (const match of matches) {
186
- const ref = match[1] || match[2];
187
- if (ref && ref.includes('_assets/')) {
188
- const assetName = path.basename(ref);
189
- referencedAssets.add(assetName);
190
- }
191
- }
192
- } catch {}
193
- }
194
-
195
- // Find orphans
196
- const orphans = assetFiles.filter(f => !f.startsWith('.') && !referencedAssets.has(f));
197
-
198
- res.json(orphans.map(f => `_assets/${f}`));
112
+ const orphans = await assetService.findOrphans(projectRoot);
113
+ res.json(orphans);
199
114
  } catch (err) {
200
115
  console.error('[asset:orphans]', err);
201
116
  res.status(500).json({ error: err.message });
@@ -216,14 +131,7 @@ export function createAssetRoutes(ctx) {
216
131
  return res.status(400).json({ error: 'assetPath required' });
217
132
  }
218
133
 
219
- const fullPath = path.join(projectRoot, assetPath);
220
-
221
- // Security check
222
- if (!fullPath.startsWith(path.resolve(projectRoot))) {
223
- return res.status(400).json({ error: 'Invalid path' });
224
- }
225
-
226
- await fs.unlink(fullPath);
134
+ await assetService.delete(projectRoot, assetPath);
227
135
  res.json({ success: true });
228
136
  } catch (err) {
229
137
  console.error('[asset:delete]', err);
@@ -254,30 +162,3 @@ export function createAssetRoutes(ctx) {
254
162
 
255
163
  return router;
256
164
  }
257
-
258
- /**
259
- * Recursively scan for markdown files
260
- */
261
- async function scanMarkdownFiles(dir, maxDepth = 6, currentDepth = 0) {
262
- if (currentDepth > maxDepth) return [];
263
-
264
- const files = [];
265
- const entries = await fs.readdir(dir, { withFileTypes: true });
266
-
267
- for (const entry of entries) {
268
- if (entry.name.startsWith('.')) continue;
269
- if (entry.name === 'node_modules') continue;
270
- if (entry.name === '_assets') continue;
271
-
272
- const fullPath = path.join(dir, entry.name);
273
-
274
- if (entry.isDirectory()) {
275
- const subFiles = await scanMarkdownFiles(fullPath, maxDepth, currentDepth + 1);
276
- files.push(...subFiles);
277
- } else if (entry.name.endsWith('.md')) {
278
- files.push(fullPath);
279
- }
280
- }
281
-
282
- return files;
283
- }
package/src/api/file.js CHANGED
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * File API routes
3
3
  *
4
- * Mirrors electronAPI.file.*
4
+ * Mirrors electronAPI.file.* using FileService from mrmd-electron
5
5
  */
6
6
 
7
7
  import { Router } from 'express';
8
8
  import path from 'path';
9
- import fs from 'fs/promises';
10
- import { constants as fsConstants } from 'fs';
11
9
 
12
10
  /**
13
11
  * Create file routes
@@ -15,6 +13,7 @@ import { constants as fsConstants } from 'fs';
15
13
  */
16
14
  export function createFileRoutes(ctx) {
17
15
  const router = Router();
16
+ const { fileService } = ctx;
18
17
 
19
18
  /**
20
19
  * GET /api/file/scan?root=...&extensions=...&maxDepth=...
@@ -24,11 +23,13 @@ export function createFileRoutes(ctx) {
24
23
  router.get('/scan', async (req, res) => {
25
24
  try {
26
25
  const root = req.query.root || ctx.projectDir;
27
- const extensions = req.query.extensions?.split(',') || ['.md'];
28
- const maxDepth = parseInt(req.query.maxDepth) || 6;
29
- const includeHidden = req.query.includeHidden === 'true';
26
+ const options = {
27
+ extensions: req.query.extensions?.split(',') || ['.md'],
28
+ maxDepth: parseInt(req.query.maxDepth) || 6,
29
+ includeHidden: req.query.includeHidden === 'true',
30
+ };
30
31
 
31
- const files = await scanDirectory(root, extensions, maxDepth, includeHidden);
32
+ const files = await fileService.scan(root, options);
32
33
  res.json(files);
33
34
  } catch (err) {
34
35
  console.error('[file:scan]', err);
@@ -49,23 +50,14 @@ export function createFileRoutes(ctx) {
49
50
  }
50
51
 
51
52
  const fullPath = resolvePath(ctx.projectDir, filePath);
52
-
53
- // Create directory if needed
54
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
55
-
56
- // Check if file exists
57
- try {
58
- await fs.access(fullPath, fsConstants.F_OK);
59
- return res.status(409).json({ error: 'File already exists' });
60
- } catch {
61
- // File doesn't exist, good to create
62
- }
63
-
64
- await fs.writeFile(fullPath, content, 'utf-8');
53
+ const result = await fileService.createFile(fullPath, content);
65
54
 
66
55
  ctx.eventBus.projectChanged(ctx.projectDir);
67
- res.json({ success: true, path: fullPath });
56
+ res.json({ success: true, path: result });
68
57
  } catch (err) {
58
+ if (err.message?.includes('already exists')) {
59
+ return res.status(409).json({ error: 'File already exists' });
60
+ }
69
61
  console.error('[file:create]', err);
70
62
  res.status(500).json({ error: err.message });
71
63
  }
@@ -84,40 +76,12 @@ export function createFileRoutes(ctx) {
84
76
  }
85
77
 
86
78
  const root = projectRoot || ctx.projectDir;
87
- const fullPath = resolvePath(root, relativePath);
88
-
89
- // Create directory if needed
90
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
91
-
92
- // If file exists, find next available FSML name
93
- let finalPath = fullPath;
94
- try {
95
- await fs.access(fullPath, fsConstants.F_OK);
96
- // File exists, generate unique name
97
- const dir = path.dirname(fullPath);
98
- const ext = path.extname(fullPath);
99
- const base = path.basename(fullPath, ext);
100
-
101
- let counter = 1;
102
- while (true) {
103
- finalPath = path.join(dir, `${base}-${counter}${ext}`);
104
- try {
105
- await fs.access(finalPath, fsConstants.F_OK);
106
- counter++;
107
- } catch {
108
- break;
109
- }
110
- }
111
- } catch {
112
- // File doesn't exist, use original path
113
- }
114
-
115
- await fs.writeFile(finalPath, content, 'utf-8');
79
+ const result = await fileService.createInProject(root, relativePath, content);
116
80
 
117
81
  ctx.eventBus.projectChanged(root);
118
82
  res.json({
119
83
  success: true,
120
- path: path.relative(root, finalPath),
84
+ path: result.relativePath,
121
85
  });
122
86
  } catch (err) {
123
87
  console.error('[file:createInProject]', err);
@@ -138,27 +102,13 @@ export function createFileRoutes(ctx) {
138
102
  }
139
103
 
140
104
  const root = projectRoot || ctx.projectDir;
141
- const fullFromPath = resolvePath(root, fromPath);
142
- const fullToPath = resolvePath(root, toPath);
143
-
144
- // Create destination directory if needed
145
- await fs.mkdir(path.dirname(fullToPath), { recursive: true });
146
-
147
- // Move the file
148
- await fs.rename(fullFromPath, fullToPath);
149
-
150
- // TODO: Update internal links in other files (refactoring)
151
- // This would require parsing all .md files and updating links
152
- // For now, just return the moved file
105
+ const result = await fileService.move(root, fromPath, toPath);
153
106
 
154
107
  ctx.eventBus.projectChanged(root);
155
108
  res.json({
156
109
  success: true,
157
- movedFile: {
158
- from: fromPath,
159
- to: toPath,
160
- },
161
- updatedFiles: [], // TODO: implement link refactoring
110
+ movedFile: result.movedFile,
111
+ updatedFiles: result.updatedFiles || [],
162
112
  });
163
113
  } catch (err) {
164
114
  console.error('[file:move]', err);
@@ -179,36 +129,13 @@ export function createFileRoutes(ctx) {
179
129
  }
180
130
 
181
131
  const root = projectRoot || ctx.projectDir;
182
-
183
- // TODO: Implement FSML reordering
184
- // This involves:
185
- // 1. Reading the source and target directories
186
- // 2. Calculating new FSML prefixes
187
- // 3. Renaming files with new prefixes
188
-
189
- // For now, just do a simple move
190
- const fullSourcePath = resolvePath(root, sourcePath);
191
- let fullTargetPath;
192
-
193
- if (position === 'inside') {
194
- // Move into target directory
195
- fullTargetPath = resolvePath(root, path.join(targetPath, path.basename(sourcePath)));
196
- } else {
197
- // Move to same directory as target
198
- fullTargetPath = resolvePath(root, path.join(path.dirname(targetPath), path.basename(sourcePath)));
199
- }
200
-
201
- await fs.mkdir(path.dirname(fullTargetPath), { recursive: true });
202
- await fs.rename(fullSourcePath, fullTargetPath);
132
+ const result = await fileService.reorder(root, sourcePath, targetPath, position);
203
133
 
204
134
  ctx.eventBus.projectChanged(root);
205
135
  res.json({
206
136
  success: true,
207
- movedFile: {
208
- from: sourcePath,
209
- to: path.relative(root, fullTargetPath),
210
- },
211
- updatedFiles: [],
137
+ movedFile: result.movedFile,
138
+ updatedFiles: result.updatedFiles || [],
212
139
  });
213
140
  } catch (err) {
214
141
  console.error('[file:reorder]', err);
@@ -229,13 +156,7 @@ export function createFileRoutes(ctx) {
229
156
  }
230
157
 
231
158
  const fullPath = resolvePath(ctx.projectDir, filePath);
232
-
233
- const stat = await fs.stat(fullPath);
234
- if (stat.isDirectory()) {
235
- await fs.rm(fullPath, { recursive: true });
236
- } else {
237
- await fs.unlink(fullPath);
238
- }
159
+ await fileService.delete(fullPath);
239
160
 
240
161
  ctx.eventBus.projectChanged(ctx.projectDir);
241
162
  res.json({ success: true });
@@ -258,7 +179,7 @@ export function createFileRoutes(ctx) {
258
179
  }
259
180
 
260
181
  const fullPath = resolvePath(ctx.projectDir, filePath);
261
- const content = await fs.readFile(fullPath, 'utf-8');
182
+ const content = await fileService.read(fullPath);
262
183
 
263
184
  res.json({ success: true, content });
264
185
  } catch (err) {
@@ -283,11 +204,7 @@ export function createFileRoutes(ctx) {
283
204
  }
284
205
 
285
206
  const fullPath = resolvePath(ctx.projectDir, filePath);
286
-
287
- // Create directory if needed
288
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
289
-
290
- await fs.writeFile(fullPath, content ?? '', 'utf-8');
207
+ await fileService.write(fullPath, content ?? '');
291
208
 
292
209
  res.json({ success: true });
293
210
  } catch (err) {
@@ -311,7 +228,7 @@ export function createFileRoutes(ctx) {
311
228
  }
312
229
 
313
230
  const fullPath = resolvePath(ctx.projectDir, filePath);
314
- const content = await fs.readFile(fullPath, 'utf-8');
231
+ const content = await fileService.read(fullPath);
315
232
  const previewLines = content.split('\n').slice(0, lines).join('\n');
316
233
 
317
234
  res.json({ success: true, content: previewLines });
@@ -337,6 +254,8 @@ export function createFileRoutes(ctx) {
337
254
  }
338
255
 
339
256
  const fullPath = resolvePath(ctx.projectDir, filePath);
257
+ // FileService doesn't have getInfo, use fs directly for this simple operation
258
+ const fs = await import('fs/promises');
340
259
  const stat = await fs.stat(fullPath);
341
260
 
342
261
  res.json({
@@ -361,53 +280,13 @@ export function createFileRoutes(ctx) {
361
280
  /**
362
281
  * Resolve path - allows full filesystem access
363
282
  *
364
- * Security model: The server runs on the user's machine with their permissions.
365
- * Access control is handled via auth token, not path restrictions.
366
- *
367
283
  * @param {string} basePath - Base path for relative paths (ignored for absolute)
368
284
  * @param {string} inputPath - Path to resolve (absolute or relative)
369
285
  * @returns {string} Resolved absolute path
370
286
  */
371
287
  function resolvePath(basePath, inputPath) {
372
- // If it's already absolute, use it directly
373
288
  if (path.isAbsolute(inputPath)) {
374
289
  return inputPath;
375
290
  }
376
-
377
- // Otherwise resolve relative to basePath
378
291
  return path.resolve(basePath, inputPath);
379
292
  }
380
-
381
- /**
382
- * Scan directory for files
383
- */
384
- async function scanDirectory(root, extensions, maxDepth, includeHidden, currentDepth = 0) {
385
- if (currentDepth > maxDepth) return [];
386
-
387
- const files = [];
388
- const entries = await fs.readdir(root, { withFileTypes: true });
389
-
390
- for (const entry of entries) {
391
- // Skip hidden files unless requested
392
- if (!includeHidden && entry.name.startsWith('.')) continue;
393
-
394
- // Skip common non-content directories
395
- if (entry.name === 'node_modules') continue;
396
- if (entry.name === '__pycache__') continue;
397
- if (entry.name === '.git') continue;
398
-
399
- const fullPath = path.join(root, entry.name);
400
-
401
- if (entry.isDirectory()) {
402
- const subFiles = await scanDirectory(fullPath, extensions, maxDepth, includeHidden, currentDepth + 1);
403
- files.push(...subFiles);
404
- } else {
405
- const ext = path.extname(entry.name);
406
- if (extensions.includes(ext)) {
407
- files.push(fullPath);
408
- }
409
- }
410
- }
411
-
412
- return files;
413
- }