freemd 0.0.0-stage → 0.1.1
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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.es.md +97 -0
- package/README.md +96 -2
- package/bin/openmd.js +93 -0
- package/dist/assets/index-C0w_6LCS.css +1 -0
- package/dist/assets/index-CiAQinMf.js +29 -0
- package/dist/index.html +17 -0
- package/package.json +48 -4
- package/server/export/highlight.js +143 -0
- package/server/export/index.js +85 -0
- package/server/export/to-docx.js +403 -0
- package/server/export/to-pdf.js +541 -0
- package/server/index.js +392 -0
package/server/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { exportMarkdown } from './export/index.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const DIST_DIR = path.resolve(__dirname, '..', 'dist');
|
|
12
|
+
|
|
13
|
+
const TEXT_EXTS = new Set([
|
|
14
|
+
'.md', '.markdown', '.mdown', '.txt', '.text', '.json', '.jsonc', '.js', '.mjs', '.cjs',
|
|
15
|
+
'.ts', '.tsx', '.jsx', '.css', '.scss', '.less', '.html', '.htm', '.xml', '.svg',
|
|
16
|
+
'.yml', '.yaml', '.toml', '.ini', '.csv', '.tsv', '.sh', '.py', '.rb', '.go', '.rs',
|
|
17
|
+
'.java', '.kt', '.c', '.h', '.cpp', '.hpp', '.cs', '.php', '.sql', '.gitignore', '.env'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
|
21
|
+
|
|
22
|
+
const IGNORED = new Set(['node_modules', '.git', '.DS_Store']);
|
|
23
|
+
const MAX_DEPTH = 6;
|
|
24
|
+
|
|
25
|
+
const isNoteName = (name) => name.endsWith('-md');
|
|
26
|
+
|
|
27
|
+
function safeJoin(root, rel = '') {
|
|
28
|
+
const abs = path.resolve(root, rel);
|
|
29
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
30
|
+
const error = new Error('Ruta no permitida');
|
|
31
|
+
error.status = 400;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return abs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function slug(input) {
|
|
38
|
+
return String(input)
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/\s+/g, '-')
|
|
42
|
+
.replace(/[^\p{L}\p{N}_-]+/gu, '')
|
|
43
|
+
.replace(/-{2,}/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toRelUrl(relPath) {
|
|
48
|
+
return relPath.split('/').map(encodeURIComponent).join('/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Nombre visible de un archivo: las notas (index.md dentro de carpeta *-md)
|
|
52
|
+
// se muestran como `nota.md`; `dir` es la ruta de carpetas para el breadcrumb.
|
|
53
|
+
function fileLabels(rel) {
|
|
54
|
+
const parts = rel.split('/');
|
|
55
|
+
const name = parts[parts.length - 1];
|
|
56
|
+
const parent = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
57
|
+
if (/^index\.md$/i.test(name) && parent.endsWith('-md')) {
|
|
58
|
+
return {
|
|
59
|
+
name: `${parent.slice(0, -3)}.md`,
|
|
60
|
+
dir: parts.length >= 3 ? `${parts.slice(0, -2).join('/')}/` : ''
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
dir: parts.length >= 2 ? `${parts.slice(0, -1).join('/')}/` : ''
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function buildTree(absDir, relDir = '', depth = 0) {
|
|
70
|
+
const entries = await fsp.readdir(absDir, { withFileTypes: true });
|
|
71
|
+
const nodes = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.name.startsWith('.') || IGNORED.has(entry.name)) continue;
|
|
74
|
+
const childRel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
if (isNoteName(entry.name)) {
|
|
77
|
+
nodes.push({
|
|
78
|
+
name: `${entry.name.slice(0, -3)}.md`,
|
|
79
|
+
path: `${childRel}/index.md`,
|
|
80
|
+
dir: childRel,
|
|
81
|
+
type: 'note',
|
|
82
|
+
ext: 'md'
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const childAbs = path.join(absDir, entry.name);
|
|
87
|
+
const children = depth >= MAX_DEPTH ? [] : await buildTree(childAbs, childRel, depth + 1);
|
|
88
|
+
nodes.push({
|
|
89
|
+
name: entry.name,
|
|
90
|
+
path: childRel,
|
|
91
|
+
type: 'folder',
|
|
92
|
+
children
|
|
93
|
+
});
|
|
94
|
+
} else if (entry.isFile()) {
|
|
95
|
+
nodes.push({
|
|
96
|
+
name: entry.name,
|
|
97
|
+
path: childRel,
|
|
98
|
+
type: 'file',
|
|
99
|
+
ext: path.extname(entry.name).replace(/^\./, '').toLowerCase()
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const rank = (node) => (node.type === 'note' ? 0 : node.type === 'folder' ? 1 : 2);
|
|
104
|
+
nodes.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name, 'es'));
|
|
105
|
+
return nodes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createApp(root) {
|
|
109
|
+
const app = express();
|
|
110
|
+
const wrap = (handler) => (req, res, next) =>
|
|
111
|
+
Promise.resolve(handler(req, res, next)).catch(next);
|
|
112
|
+
const json = express.json({ limit: '2mb' });
|
|
113
|
+
|
|
114
|
+
app.get('/api/health', (req, res) => res.json({ ok: true }));
|
|
115
|
+
|
|
116
|
+
app.get('/api/tree', wrap(async (req, res) => {
|
|
117
|
+
res.json({ root, tree: await buildTree(root) });
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
app.get('/api/file', wrap(async (req, res) => {
|
|
121
|
+
const rel = String(req.query.path ?? '');
|
|
122
|
+
const abs = safeJoin(root, rel);
|
|
123
|
+
let stat;
|
|
124
|
+
try {
|
|
125
|
+
stat = await fsp.stat(abs);
|
|
126
|
+
} catch {
|
|
127
|
+
// Nota sin index.md (p. ej. carpeta *-md creada por fuera): la inicializamos
|
|
128
|
+
const parts = rel.split('/');
|
|
129
|
+
const parent = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
130
|
+
const isNoteIndex = parent.endsWith('-md') && /^index\.md$/i.test(parts[parts.length - 1]);
|
|
131
|
+
if (isNoteIndex) {
|
|
132
|
+
try {
|
|
133
|
+
const parentAbs = safeJoin(root, parts.slice(0, -1).join('/'));
|
|
134
|
+
if ((await fsp.stat(parentAbs)).isDirectory()) {
|
|
135
|
+
const title = parent.slice(0, -3).replace(/^-+|-+$/g, '') || parent;
|
|
136
|
+
await fsp.writeFile(abs, `# ${title}\n\n`, 'utf8');
|
|
137
|
+
stat = await fsp.stat(abs);
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
if (!stat) return res.status(404).json({ error: 'El archivo no existe' });
|
|
142
|
+
}
|
|
143
|
+
if (!stat.isFile()) {
|
|
144
|
+
return res.status(400).json({ error: 'No es un archivo' });
|
|
145
|
+
}
|
|
146
|
+
const ext = path.extname(abs).toLowerCase();
|
|
147
|
+
const { name, dir } = fileLabels(rel);
|
|
148
|
+
if (TEXT_EXTS.has(ext)) {
|
|
149
|
+
const content = await fsp.readFile(abs, 'utf8');
|
|
150
|
+
return res.json({ name, dir, path: rel, type: 'file', ext: ext.slice(1), text: true, content });
|
|
151
|
+
}
|
|
152
|
+
return res.json({
|
|
153
|
+
name,
|
|
154
|
+
dir,
|
|
155
|
+
path: rel,
|
|
156
|
+
type: 'file',
|
|
157
|
+
ext: ext.slice(1),
|
|
158
|
+
text: false,
|
|
159
|
+
mediaUrl: `/media/${toRelUrl(rel)}`
|
|
160
|
+
});
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
app.put('/api/file', json, wrap(async (req, res) => {
|
|
164
|
+
const rel = String(req.body?.path ?? '');
|
|
165
|
+
const abs = safeJoin(root, rel);
|
|
166
|
+
// Sin mkdir: guardar solo funciona sobre archivos que siguen existiendo
|
|
167
|
+
// (evita recrear la carpeta vieja si la nota fue renombrada o borrada).
|
|
168
|
+
try {
|
|
169
|
+
await fsp.writeFile(abs, String(req.body?.content ?? ''), 'utf8');
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === 'ENOENT') {
|
|
172
|
+
return res.status(404).json({ error: 'El archivo ya no existe' });
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
res.json({ ok: true });
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
app.post('/api/file', json, wrap(async (req, res) => {
|
|
180
|
+
const name = slug(req.body?.name);
|
|
181
|
+
if (!name) return res.status(400).json({ error: 'Nombre inválido' });
|
|
182
|
+
const parentRel = String(req.body?.parent ?? '');
|
|
183
|
+
const parentAbs = safeJoin(root, parentRel);
|
|
184
|
+
const rel = parentRel ? `${parentRel}/${name}.md` : `${name}.md`;
|
|
185
|
+
const abs = path.join(parentAbs, `${name}.md`);
|
|
186
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'Ya existe un archivo con ese nombre' });
|
|
187
|
+
await fsp.writeFile(abs, String(req.body?.content ?? ''), 'utf8');
|
|
188
|
+
res.status(201).json({ ok: true, path: rel });
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
app.post('/api/note', json, wrap(async (req, res) => {
|
|
192
|
+
const raw = String(req.body?.name ?? '').trim();
|
|
193
|
+
let base = slug(raw);
|
|
194
|
+
if (!base) return res.status(400).json({ error: 'Nombre inválido' });
|
|
195
|
+
base = base.replace(/-md$/, '');
|
|
196
|
+
if (!base) return res.status(400).json({ error: 'Nombre inválido' });
|
|
197
|
+
const folder = `${base}-md`;
|
|
198
|
+
const abs = path.join(root, folder);
|
|
199
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'Ya existe una nota con ese nombre' });
|
|
200
|
+
await fsp.mkdir(abs, { recursive: true });
|
|
201
|
+
const title = raw.replace(/-md$/i, '').trim() || base;
|
|
202
|
+
await fsp.writeFile(path.join(abs, 'index.md'), `# ${title}\n\n`, 'utf8');
|
|
203
|
+
res.status(201).json({ ok: true, path: `${folder}/index.md` });
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
app.post('/api/folder', json, wrap(async (req, res) => {
|
|
207
|
+
const name = slug(req.body?.name);
|
|
208
|
+
if (!name) return res.status(400).json({ error: 'Nombre inválido' });
|
|
209
|
+
const parentRel = String(req.body?.parent ?? '');
|
|
210
|
+
const parentAbs = safeJoin(root, parentRel);
|
|
211
|
+
const abs = path.join(parentAbs, name);
|
|
212
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'Ya existe una carpeta con ese nombre' });
|
|
213
|
+
await fsp.mkdir(abs);
|
|
214
|
+
// Una carpeta terminada en -md es una nota: le creamos su index.md
|
|
215
|
+
if (name.endsWith('-md')) {
|
|
216
|
+
const title = name.slice(0, -3).replace(/^-+|-+$/g, '') || name;
|
|
217
|
+
await fsp.writeFile(path.join(abs, 'index.md'), `# ${title}\n\n`, 'utf8');
|
|
218
|
+
}
|
|
219
|
+
const rel = parentRel ? `${parentRel}/${name}` : name;
|
|
220
|
+
res.status(201).json({ ok: true, path: rel });
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
app.get('/api/resources', wrap(async (req, res) => {
|
|
224
|
+
const dirRel = String(req.query.dir ?? '');
|
|
225
|
+
const dirAbs = safeJoin(root, dirRel);
|
|
226
|
+
const stat = await fsp.stat(dirAbs);
|
|
227
|
+
if (!stat.isDirectory()) return res.status(400).json({ error: 'No es una carpeta' });
|
|
228
|
+
const entries = await fsp.readdir(dirAbs, { withFileTypes: true });
|
|
229
|
+
const files = [];
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (!entry.isFile() || entry.name.startsWith('.') || IGNORED.has(entry.name)) continue;
|
|
232
|
+
if (entry.name === 'index.md') continue;
|
|
233
|
+
const ext = path.extname(entry.name).slice(1).toLowerCase();
|
|
234
|
+
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
235
|
+
files.push({
|
|
236
|
+
name: entry.name,
|
|
237
|
+
path: rel,
|
|
238
|
+
ext,
|
|
239
|
+
type: IMAGE_EXTS.has(ext) ? 'image' : 'file',
|
|
240
|
+
mediaUrl: `/media/${toRelUrl(rel)}`
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
files.sort((a, b) => a.name.localeCompare(b.name, 'es'));
|
|
244
|
+
res.json({ files });
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
app.post('/api/upload', express.raw({ type: '*/*', limit: '64mb' }), wrap(async (req, res) => {
|
|
248
|
+
const dirRel = String(req.query.dir ?? '');
|
|
249
|
+
const dirAbs = safeJoin(root, dirRel);
|
|
250
|
+
const rawName = String(req.query.name ?? req.headers['x-filename'] ?? 'file');
|
|
251
|
+
const name = path.basename(rawName).replace(/[^\p{L}\p{N}._ -]/gu, '_');
|
|
252
|
+
if (!req.body?.length) return res.status(400).json({ error: 'Sin contenido' });
|
|
253
|
+
await fsp.mkdir(dirAbs, { recursive: true });
|
|
254
|
+
await fsp.writeFile(path.join(dirAbs, name), req.body);
|
|
255
|
+
const rel = dirRel ? `${dirRel}/${name}` : name;
|
|
256
|
+
res.status(201).json({ ok: true, path: rel, name });
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
app.delete('/api/entry', wrap(async (req, res) => {
|
|
260
|
+
const rel = String(req.query.path ?? '');
|
|
261
|
+
if (!rel) return res.status(400).json({ error: 'Falta la ruta' });
|
|
262
|
+
const abs = safeJoin(root, rel);
|
|
263
|
+
if (abs === root) return res.status(400).json({ error: 'No se puede borrar la carpeta raíz' });
|
|
264
|
+
await fsp.rm(abs, { recursive: true });
|
|
265
|
+
res.json({ ok: true });
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
app.patch('/api/entry', json, wrap(async (req, res) => {
|
|
269
|
+
const rel = String(req.body?.path ?? '').split('\\').join('/');
|
|
270
|
+
const rawName = String(req.body?.name ?? '');
|
|
271
|
+
if (!rel) return res.status(400).json({ error: 'Falta la ruta' });
|
|
272
|
+
const abs = safeJoin(root, rel);
|
|
273
|
+
const stat = await fsp.stat(abs).catch(() => null);
|
|
274
|
+
if (!stat) return res.status(404).json({ error: 'El elemento no existe' });
|
|
275
|
+
const parentRel = path.posix.dirname(rel);
|
|
276
|
+
const parentAbs = safeJoin(root, parentRel === '.' ? '' : parentRel);
|
|
277
|
+
const currentName = path.basename(abs);
|
|
278
|
+
|
|
279
|
+
let newName;
|
|
280
|
+
if (stat.isDirectory()) {
|
|
281
|
+
// notas (carpetas *-md): se admite que el usuario escriba el nombre con .md o -md
|
|
282
|
+
let base = slug(rawName.replace(/\.md$/i, '').replace(/-md$/i, ''));
|
|
283
|
+
if (!base) return res.status(400).json({ error: 'Nombre inválido' });
|
|
284
|
+
if (currentName.endsWith('-md')) base = `${base}-md`;
|
|
285
|
+
newName = base;
|
|
286
|
+
} else {
|
|
287
|
+
const ext = path.extname(rel);
|
|
288
|
+
const base = slug(rawName.replace(/\.[^.]+$/, ''));
|
|
289
|
+
if (!base) return res.status(400).json({ error: 'Nombre inválido' });
|
|
290
|
+
newName = `${base}${ext}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (newName === currentName) {
|
|
294
|
+
return res.json({ ok: true, path: rel, unchanged: true });
|
|
295
|
+
}
|
|
296
|
+
const target = path.join(parentAbs, newName);
|
|
297
|
+
if (fs.existsSync(target)) {
|
|
298
|
+
return res.status(409).json({ error: 'Ya existe un elemento con ese nombre' });
|
|
299
|
+
}
|
|
300
|
+
await fsp.rename(abs, target);
|
|
301
|
+
const newRel = parentRel === '.' ? newName : `${parentRel}/${newName}`;
|
|
302
|
+
res.json({ ok: true, path: newRel });
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
app.post('/api/export', json, wrap(async (req, res) => {
|
|
306
|
+
const rel = String(req.body?.path ?? '').split('\\').join('/');
|
|
307
|
+
const format = String(req.body?.format ?? 'pdf');
|
|
308
|
+
if (format !== 'pdf' && format !== 'docx') {
|
|
309
|
+
return res.status(400).json({ error: 'Formato no soportado' });
|
|
310
|
+
}
|
|
311
|
+
const abs = safeJoin(root, rel);
|
|
312
|
+
let stat;
|
|
313
|
+
try {
|
|
314
|
+
stat = await fsp.stat(abs);
|
|
315
|
+
} catch {
|
|
316
|
+
return res.status(404).json({ error: 'El archivo no existe' });
|
|
317
|
+
}
|
|
318
|
+
if (!stat.isFile()) return res.status(400).json({ error: 'No es un archivo' });
|
|
319
|
+
const source = await fsp.readFile(abs, 'utf8');
|
|
320
|
+
const dirName = path.posix.dirname(rel);
|
|
321
|
+
const baseRel = dirName === '.' ? '' : dirName;
|
|
322
|
+
// El nombre del documento es el de la nota; para notas (index.md) se usa la carpeta sin -md.
|
|
323
|
+
let title = path.basename(rel).replace(/\.(md|markdown)$/i, '');
|
|
324
|
+
if (/^index$/i.test(title)) {
|
|
325
|
+
const folder = path.basename(dirName).replace(/-md$/, '');
|
|
326
|
+
if (folder && folder !== '.') title = folder;
|
|
327
|
+
}
|
|
328
|
+
const buffer = await exportMarkdown(source, { format, root, baseRel, title });
|
|
329
|
+
res.setHeader(
|
|
330
|
+
'content-type',
|
|
331
|
+
format === 'pdf'
|
|
332
|
+
? 'application/pdf'
|
|
333
|
+
: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
334
|
+
);
|
|
335
|
+
res.setHeader('content-disposition', `attachment; filename*=UTF-8''${encodeURIComponent(title)}.${format}`);
|
|
336
|
+
res.send(buffer);
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
app.use('/media', express.static(root, { fallthrough: false }));
|
|
340
|
+
app.use(express.static(DIST_DIR));
|
|
341
|
+
|
|
342
|
+
app.use((req, res, next) => {
|
|
343
|
+
if (req.method !== 'GET' || req.path.startsWith('/api')) return next();
|
|
344
|
+
const index = path.join(DIST_DIR, 'index.html');
|
|
345
|
+
if (!fs.existsSync(index)) {
|
|
346
|
+
return res.status(500).send('Falta la build del frontend. Ejecuta: pnpm build');
|
|
347
|
+
}
|
|
348
|
+
res.sendFile(index);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
app.use((error, req, res, next) => {
|
|
352
|
+
const status = error.status ?? (error.code === 'ENOENT' ? 404 : 500);
|
|
353
|
+
res.status(status).json({ error: error.message ?? 'Error interno' });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return app;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function openBrowser(url) {
|
|
360
|
+
const platform = process.platform;
|
|
361
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
362
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
363
|
+
try {
|
|
364
|
+
spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function start(options = {}) {
|
|
369
|
+
const root = path.resolve(options.dir ?? path.join(os.homedir(), 'freemd'));
|
|
370
|
+
// Migración única: si existe la carpeta antigua ~/openmd y aún no hay ~/freemd,
|
|
371
|
+
// la renombramos para que las notas no se "pierdan" tras el renombrado.
|
|
372
|
+
let migrated = null;
|
|
373
|
+
const legacyRoot = path.join(os.homedir(), 'openmd');
|
|
374
|
+
if (!fs.existsSync(root) && fs.existsSync(legacyRoot)) {
|
|
375
|
+
try {
|
|
376
|
+
await fsp.rename(legacyRoot, root);
|
|
377
|
+
migrated = legacyRoot;
|
|
378
|
+
} catch {}
|
|
379
|
+
}
|
|
380
|
+
await fsp.mkdir(root, { recursive: true });
|
|
381
|
+
const app = createApp(root);
|
|
382
|
+
const port = options.port ?? (Number(process.env.FREEMD_PORT) || 5813);
|
|
383
|
+
const host = options.host ?? process.env.FREEMD_HOST ?? 'localhost';
|
|
384
|
+
const server = await new Promise((resolve, reject) => {
|
|
385
|
+
const instance = app.listen(port, host, () => resolve(instance));
|
|
386
|
+
instance.on('error', reject);
|
|
387
|
+
});
|
|
388
|
+
const display = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
|
|
389
|
+
const url = `http://${display}:${port}`;
|
|
390
|
+
if (options.open) openBrowser(url);
|
|
391
|
+
return { server, url, root, port, host, migrated };
|
|
392
|
+
}
|