mrmd-server 0.1.0 → 0.1.2

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/src/api/pty.js CHANGED
@@ -1,17 +1,30 @@
1
1
  /**
2
2
  * PTY Session API routes (for ```term blocks)
3
3
  *
4
- * Mirrors electronAPI.pty.*
4
+ * Mirrors electronAPI.pty.* using PtySessionService from mrmd-electron
5
5
  */
6
6
 
7
7
  import { Router } from 'express';
8
- import { spawn } from 'child_process';
8
+ import { Project } from 'mrmd-project';
9
+ import fs from 'fs';
9
10
  import path from 'path';
10
- import fs from 'fs/promises';
11
- import net from 'net';
12
11
 
13
- // Session registry: sessionName -> { port, process, cwd, venv, wsUrl }
14
- const sessions = new Map();
12
+ /**
13
+ * Detect project from a file path
14
+ */
15
+ function detectProject(filePath) {
16
+ const root = Project.findRoot(filePath, (dir) => fs.existsSync(path.join(dir, 'mrmd.md')));
17
+ if (!root) return null;
18
+
19
+ try {
20
+ const mrmdPath = path.join(root, 'mrmd.md');
21
+ const content = fs.readFileSync(mrmdPath, 'utf8');
22
+ const config = Project.parseConfig(content);
23
+ return { root, config };
24
+ } catch (e) {
25
+ return { root, config: {} };
26
+ }
27
+ }
15
28
 
16
29
  /**
17
30
  * Create PTY routes
@@ -19,6 +32,7 @@ const sessions = new Map();
19
32
  */
20
33
  export function createPtyRoutes(ctx) {
21
34
  const router = Router();
35
+ const { ptySessionService } = ctx;
22
36
 
23
37
  /**
24
38
  * GET /api/pty
@@ -27,17 +41,7 @@ export function createPtyRoutes(ctx) {
27
41
  */
28
42
  router.get('/', async (req, res) => {
29
43
  try {
30
- const list = [];
31
- for (const [name, session] of sessions) {
32
- list.push({
33
- name,
34
- port: session.port,
35
- cwd: session.cwd,
36
- venv: session.venv,
37
- wsUrl: session.wsUrl,
38
- running: session.process && !session.process.killed,
39
- });
40
- }
44
+ const list = ptySessionService.list();
41
45
  res.json(list);
42
46
  } catch (err) {
43
47
  console.error('[pty:list]', err);
@@ -53,113 +57,21 @@ export function createPtyRoutes(ctx) {
53
57
  router.post('/', async (req, res) => {
54
58
  try {
55
59
  const { config } = req.body;
56
- const { name, cwd, venv } = config || {};
57
60
 
58
- if (!name) {
61
+ if (!config?.name) {
59
62
  return res.status(400).json({ error: 'config.name required' });
60
63
  }
61
64
 
62
- // Check if session already exists
63
- if (sessions.has(name)) {
64
- const existing = sessions.get(name);
65
- if (existing.process && !existing.process.killed) {
66
- return res.json({
67
- name,
68
- port: existing.port,
69
- cwd: existing.cwd,
70
- venv: existing.venv,
71
- wsUrl: existing.wsUrl,
72
- reused: true,
73
- alive: true,
74
- });
75
- }
76
- }
77
-
78
- // Find free port
79
- const port = await findFreePort(7001, 7100);
80
- const workDir = cwd ? path.resolve(ctx.projectDir, cwd) : ctx.projectDir;
81
-
82
- // Find mrmd-pty package
83
- const mrmdPtyPaths = [
84
- path.join(ctx.projectDir, '../mrmd-pty'),
85
- path.join(process.cwd(), '../mrmd-pty'),
86
- path.join(process.cwd(), 'mrmd-pty'),
87
- path.join(__dirname, '../../../mrmd-pty'),
88
- ];
89
-
90
- let mrmdPtyPath = null;
91
- for (const p of mrmdPtyPaths) {
92
- try {
93
- await fs.access(path.join(p, 'package.json'));
94
- mrmdPtyPath = p;
95
- break;
96
- } catch {}
97
- }
98
-
99
- let proc;
100
- const env = { ...process.env };
101
-
102
- // If venv specified, activate it
103
- if (venv) {
104
- const venvPath = path.resolve(ctx.projectDir, venv);
105
- env.VIRTUAL_ENV = venvPath;
106
- env.PATH = `${path.join(venvPath, 'bin')}:${env.PATH}`;
107
- }
108
-
109
- if (mrmdPtyPath) {
110
- // Run mrmd-pty server
111
- proc = spawn('node', [
112
- path.join(mrmdPtyPath, 'src', 'server.js'),
113
- '--port', port.toString(),
114
- '--cwd', workDir,
115
- ], {
116
- cwd: workDir,
117
- stdio: ['pipe', 'pipe', 'pipe'],
118
- env,
119
- });
120
- } else {
121
- // Fallback: Try to use npx
122
- proc = spawn('npx', [
123
- 'mrmd-pty',
124
- '--port', port.toString(),
125
- '--cwd', workDir,
126
- ], {
127
- cwd: workDir,
128
- stdio: ['pipe', 'pipe', 'pipe'],
129
- env,
130
- });
131
- }
132
-
133
- // Wait for server to start
134
- try {
135
- await waitForPort(port, 10000);
136
- } catch (err) {
137
- proc.kill();
138
- return res.status(500).json({ error: `PTY server failed to start: ${err.message}` });
139
- }
140
-
141
- const wsUrl = `ws://localhost:${port}`;
142
-
143
- sessions.set(name, {
144
- port,
145
- process: proc,
146
- cwd: workDir,
147
- venv,
148
- wsUrl,
149
- });
150
-
151
- proc.on('exit', (code) => {
152
- console.log(`[pty] ${name} exited with code ${code}`);
153
- sessions.delete(name);
154
- });
65
+ const result = await ptySessionService.start(config);
155
66
 
156
67
  res.json({
157
- name,
158
- port,
159
- cwd: workDir,
160
- venv,
161
- wsUrl,
162
- alive: true,
68
+ name: result.name,
69
+ port: result.port,
70
+ cwd: result.cwd,
71
+ venv: result.venv,
72
+ pid: result.pid,
73
+ wsUrl: `ws://localhost:${result.port}`,
74
+ alive: result.alive,
163
75
  });
164
76
  } catch (err) {
165
77
  console.error('[pty:start]', err);
@@ -175,21 +87,11 @@ export function createPtyRoutes(ctx) {
175
87
  router.delete('/:name', async (req, res) => {
176
88
  try {
177
89
  const { name } = req.params;
178
- const session = sessions.get(name);
179
-
180
- if (!session) {
181
- return res.json({ success: true, message: 'Session not found' });
182
- }
183
-
184
- if (session.process && !session.process.killed) {
185
- session.process.kill();
186
- }
187
-
188
- sessions.delete(name);
90
+ await ptySessionService.stop(name);
189
91
  res.json({ success: true });
190
92
  } catch (err) {
191
93
  console.error('[pty:stop]', err);
192
- res.status(500).json({ error: err.message });
94
+ res.json({ success: true, message: err.message });
193
95
  }
194
96
  });
195
97
 
@@ -201,25 +103,17 @@ export function createPtyRoutes(ctx) {
201
103
  router.post('/:name/restart', async (req, res) => {
202
104
  try {
203
105
  const { name } = req.params;
204
- const session = sessions.get(name);
106
+ const result = await ptySessionService.restart(name);
205
107
 
206
- if (!session) {
207
- return res.status(404).json({ error: 'Session not found' });
208
- }
209
-
210
- const { cwd, venv } = session;
211
-
212
- // Kill existing
213
- if (session.process && !session.process.killed) {
214
- session.process.kill();
215
- }
216
-
217
- sessions.delete(name);
218
-
219
- // Create new session
220
- req.body.config = { name, cwd, venv };
221
- // Redirect to POST /
222
- return res.redirect(307, '/api/pty');
108
+ res.json({
109
+ name: result.name,
110
+ port: result.port,
111
+ cwd: result.cwd,
112
+ venv: result.venv,
113
+ pid: result.pid,
114
+ wsUrl: `ws://localhost:${result.port}`,
115
+ alive: result.alive,
116
+ });
223
117
  } catch (err) {
224
118
  console.error('[pty:restart]', err);
225
119
  res.status(500).json({ error: err.message });
@@ -231,171 +125,57 @@ export function createPtyRoutes(ctx) {
231
125
  * Get or create PTY session for a document
232
126
  * Returns session info including wsUrl for WebSocket connection
233
127
  * Mirrors: electronAPI.pty.forDocument(documentPath)
128
+ *
129
+ * Automatically detects project if projectConfig/projectRoot not provided
234
130
  */
235
131
  router.post('/for-document', async (req, res) => {
236
132
  try {
237
- const { documentPath } = req.body;
133
+ let { documentPath, projectConfig, frontmatter, projectRoot } = req.body;
134
+
238
135
  if (!documentPath) {
239
136
  return res.status(400).json({ error: 'documentPath required' });
240
137
  }
241
138
 
242
- const docName = `pty-${path.basename(documentPath, '.md')}`;
243
-
244
- // Check if session exists
245
- if (sessions.has(docName)) {
246
- const session = sessions.get(docName);
247
- const alive = session.process && !session.process.killed;
248
- return res.json({
249
- name: docName,
250
- port: session.port,
251
- cwd: session.cwd,
252
- venv: session.venv,
253
- wsUrl: session.wsUrl,
254
- alive,
255
- });
256
- }
257
-
258
- // Try to read venv from document frontmatter
259
- const fullPath = path.resolve(ctx.projectDir, documentPath);
260
- let venv = null;
261
-
262
- try {
263
- const content = await fs.readFile(fullPath, 'utf-8');
264
- const match = content.match(/^---\n[\s\S]*?venv:\s*(.+?)[\n\r]/m);
265
- if (match) {
266
- venv = match[1].trim();
139
+ // Auto-detect project if not provided
140
+ if (!projectConfig || !projectRoot) {
141
+ const detected = detectProject(documentPath);
142
+ if (detected) {
143
+ projectRoot = projectRoot || detected.root;
144
+ projectConfig = projectConfig || detected.config;
145
+ } else {
146
+ projectRoot = projectRoot || (ctx.projectDir || process.cwd());
147
+ projectConfig = projectConfig || {};
267
148
  }
268
- } catch {}
269
-
270
- // Create session
271
- const port = await findFreePort(7001, 7100);
272
- const workDir = path.dirname(fullPath);
273
- const env = { ...process.env };
274
-
275
- if (venv) {
276
- const venvPath = path.resolve(ctx.projectDir, venv);
277
- env.VIRTUAL_ENV = venvPath;
278
- env.PATH = `${path.join(venvPath, 'bin')}:${env.PATH}`;
279
149
  }
280
150
 
281
- // Try to start mrmd-pty
282
- const mrmdPtyPaths = [
283
- path.join(ctx.projectDir, '../mrmd-pty'),
284
- path.join(process.cwd(), '../mrmd-pty'),
285
- ];
286
-
287
- let mrmdPtyPath = null;
288
- for (const p of mrmdPtyPaths) {
151
+ // Auto-parse frontmatter if not provided
152
+ if (!frontmatter) {
289
153
  try {
290
- await fs.access(path.join(p, 'package.json'));
291
- mrmdPtyPath = p;
292
- break;
293
- } catch {}
154
+ const content = fs.readFileSync(documentPath, 'utf8');
155
+ frontmatter = Project.parseFrontmatter(content);
156
+ } catch (e) {
157
+ frontmatter = null;
158
+ }
294
159
  }
295
160
 
296
- let proc;
297
- if (mrmdPtyPath) {
298
- proc = spawn('node', [
299
- path.join(mrmdPtyPath, 'src', 'server.js'),
300
- '--port', port.toString(),
301
- '--cwd', workDir,
302
- ], {
303
- cwd: workDir,
304
- stdio: ['pipe', 'pipe', 'pipe'],
305
- env,
306
- });
307
- } else {
308
- // mrmd-pty not found, return null
309
- return res.json(null);
310
- }
161
+ const result = await ptySessionService.getForDocument(
162
+ documentPath,
163
+ projectConfig,
164
+ frontmatter,
165
+ projectRoot
166
+ );
311
167
 
312
- try {
313
- await waitForPort(port, 10000);
314
- } catch (err) {
315
- proc.kill();
316
- return res.json(null);
168
+ // Add wsUrl if we have a port
169
+ if (result?.port) {
170
+ result.wsUrl = `ws://localhost:${result.port}`;
317
171
  }
318
172
 
319
- const wsUrl = `ws://localhost:${port}`;
320
-
321
- sessions.set(docName, {
322
- port,
323
- process: proc,
324
- cwd: workDir,
325
- venv,
326
- wsUrl,
327
- });
328
-
329
- proc.on('exit', () => {
330
- sessions.delete(docName);
331
- });
332
-
333
- res.json({
334
- name: docName,
335
- port,
336
- cwd: workDir,
337
- venv,
338
- wsUrl,
339
- alive: true,
340
- });
173
+ res.json(result);
341
174
  } catch (err) {
342
175
  console.error('[pty:forDocument]', err);
343
- res.status(500).json({ error: err.message });
176
+ res.json(null);
344
177
  }
345
178
  });
346
179
 
347
180
  return router;
348
181
  }
349
-
350
- /**
351
- * Find a free port in range
352
- */
353
- async function findFreePort(start, end) {
354
- for (let port = start; port <= end; port++) {
355
- if (await isPortFree(port)) {
356
- return port;
357
- }
358
- }
359
- throw new Error(`No free port found in range ${start}-${end}`);
360
- }
361
-
362
- /**
363
- * Check if port is free
364
- */
365
- function isPortFree(port) {
366
- return new Promise((resolve) => {
367
- const server = net.createServer();
368
- server.once('error', () => resolve(false));
369
- server.once('listening', () => {
370
- server.close();
371
- resolve(true);
372
- });
373
- server.listen(port, '127.0.0.1');
374
- });
375
- }
376
-
377
- /**
378
- * Wait for port to be open
379
- */
380
- function waitForPort(port, timeout = 10000) {
381
- return new Promise((resolve, reject) => {
382
- const start = Date.now();
383
-
384
- function check() {
385
- const socket = net.connect(port, '127.0.0.1');
386
- socket.once('connect', () => {
387
- socket.end();
388
- resolve();
389
- });
390
- socket.once('error', () => {
391
- if (Date.now() - start > timeout) {
392
- reject(new Error(`Timeout waiting for port ${port}`));
393
- } else {
394
- setTimeout(check, 200);
395
- }
396
- });
397
- }
398
-
399
- check();
400
- });
401
- }