mrmd-server 0.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/README.md +230 -0
- package/bin/cli.js +161 -0
- package/package.json +35 -0
- package/src/api/asset.js +283 -0
- package/src/api/bash.js +293 -0
- package/src/api/file.js +407 -0
- package/src/api/index.js +11 -0
- package/src/api/julia.js +345 -0
- package/src/api/project.js +296 -0
- package/src/api/pty.js +401 -0
- package/src/api/runtime.js +140 -0
- package/src/api/session.js +358 -0
- package/src/api/system.js +256 -0
- package/src/auth.js +60 -0
- package/src/events.js +50 -0
- package/src/index.js +9 -0
- package/src/server-v2.js +118 -0
- package/src/server.js +297 -0
- package/src/websocket.js +85 -0
- package/static/http-shim.js +371 -0
- package/static/index.html +171 -0
package/src/api/pty.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Session API routes (for ```term blocks)
|
|
3
|
+
*
|
|
4
|
+
* Mirrors electronAPI.pty.*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import net from 'net';
|
|
12
|
+
|
|
13
|
+
// Session registry: sessionName -> { port, process, cwd, venv, wsUrl }
|
|
14
|
+
const sessions = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create PTY routes
|
|
18
|
+
* @param {import('../server.js').ServerContext} ctx
|
|
19
|
+
*/
|
|
20
|
+
export function createPtyRoutes(ctx) {
|
|
21
|
+
const router = Router();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/pty
|
|
25
|
+
* List all running PTY sessions
|
|
26
|
+
* Mirrors: electronAPI.pty.list()
|
|
27
|
+
*/
|
|
28
|
+
router.get('/', async (req, res) => {
|
|
29
|
+
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
|
+
}
|
|
41
|
+
res.json(list);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[pty:list]', err);
|
|
44
|
+
res.status(500).json({ error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* POST /api/pty
|
|
50
|
+
* Start a new PTY session (mrmd-pty server)
|
|
51
|
+
* Mirrors: electronAPI.pty.start(config)
|
|
52
|
+
*/
|
|
53
|
+
router.post('/', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const { config } = req.body;
|
|
56
|
+
const { name, cwd, venv } = config || {};
|
|
57
|
+
|
|
58
|
+
if (!name) {
|
|
59
|
+
return res.status(400).json({ error: 'config.name required' });
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
});
|
|
155
|
+
|
|
156
|
+
res.json({
|
|
157
|
+
name,
|
|
158
|
+
port,
|
|
159
|
+
cwd: workDir,
|
|
160
|
+
venv,
|
|
161
|
+
wsUrl,
|
|
162
|
+
alive: true,
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error('[pty:start]', err);
|
|
166
|
+
res.status(500).json({ error: err.message });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* DELETE /api/pty/:name
|
|
172
|
+
* Stop a PTY session
|
|
173
|
+
* Mirrors: electronAPI.pty.stop(sessionName)
|
|
174
|
+
*/
|
|
175
|
+
router.delete('/:name', async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
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);
|
|
189
|
+
res.json({ success: true });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error('[pty:stop]', err);
|
|
192
|
+
res.status(500).json({ error: err.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* POST /api/pty/:name/restart
|
|
198
|
+
* Restart a PTY session
|
|
199
|
+
* Mirrors: electronAPI.pty.restart(sessionName)
|
|
200
|
+
*/
|
|
201
|
+
router.post('/:name/restart', async (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const { name } = req.params;
|
|
204
|
+
const session = sessions.get(name);
|
|
205
|
+
|
|
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');
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('[pty:restart]', err);
|
|
225
|
+
res.status(500).json({ error: err.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* POST /api/pty/for-document
|
|
231
|
+
* Get or create PTY session for a document
|
|
232
|
+
* Returns session info including wsUrl for WebSocket connection
|
|
233
|
+
* Mirrors: electronAPI.pty.forDocument(documentPath)
|
|
234
|
+
*/
|
|
235
|
+
router.post('/for-document', async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const { documentPath } = req.body;
|
|
238
|
+
if (!documentPath) {
|
|
239
|
+
return res.status(400).json({ error: 'documentPath required' });
|
|
240
|
+
}
|
|
241
|
+
|
|
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();
|
|
267
|
+
}
|
|
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
|
+
}
|
|
280
|
+
|
|
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) {
|
|
289
|
+
try {
|
|
290
|
+
await fs.access(path.join(p, 'package.json'));
|
|
291
|
+
mrmdPtyPath = p;
|
|
292
|
+
break;
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
|
|
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
|
+
}
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await waitForPort(port, 10000);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
proc.kill();
|
|
316
|
+
return res.json(null);
|
|
317
|
+
}
|
|
318
|
+
|
|
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
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('[pty:forDocument]', err);
|
|
343
|
+
res.status(500).json({ error: err.message });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return router;
|
|
348
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime API routes
|
|
3
|
+
*
|
|
4
|
+
* Mirrors electronAPI runtime management functions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router } from 'express';
|
|
8
|
+
|
|
9
|
+
// Global runtime registry (shared with session.js in a real impl)
|
|
10
|
+
const runtimes = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create runtime routes
|
|
14
|
+
* @param {import('../server.js').ServerContext} ctx
|
|
15
|
+
*/
|
|
16
|
+
export function createRuntimeRoutes(ctx) {
|
|
17
|
+
const router = Router();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/runtime
|
|
21
|
+
* List all runtimes
|
|
22
|
+
* Mirrors: electronAPI.listRuntimes()
|
|
23
|
+
*/
|
|
24
|
+
router.get('/', async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const list = [];
|
|
27
|
+
for (const [id, runtime] of runtimes) {
|
|
28
|
+
list.push({
|
|
29
|
+
id,
|
|
30
|
+
type: runtime.type,
|
|
31
|
+
port: runtime.port,
|
|
32
|
+
venv: runtime.venv,
|
|
33
|
+
cwd: runtime.cwd,
|
|
34
|
+
running: runtime.process && !runtime.process.killed,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
res.json(list);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('[runtime:list]', err);
|
|
40
|
+
res.status(500).json({ error: err.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* DELETE /api/runtime/:id
|
|
46
|
+
* Kill a runtime
|
|
47
|
+
* Mirrors: electronAPI.killRuntime(runtimeId)
|
|
48
|
+
*/
|
|
49
|
+
router.delete('/:id', async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { id } = req.params;
|
|
52
|
+
const runtime = runtimes.get(id);
|
|
53
|
+
|
|
54
|
+
if (!runtime) {
|
|
55
|
+
return res.json({ success: true, message: 'Runtime not found' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (runtime.process && !runtime.process.killed) {
|
|
59
|
+
runtime.process.kill();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
runtimes.delete(id);
|
|
63
|
+
res.json({ success: true });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('[runtime:kill]', err);
|
|
66
|
+
res.status(500).json({ error: err.message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* POST /api/runtime/:id/attach
|
|
72
|
+
* Attach to an existing runtime
|
|
73
|
+
* Mirrors: electronAPI.attachRuntime(runtimeId)
|
|
74
|
+
*/
|
|
75
|
+
router.post('/:id/attach', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const { id } = req.params;
|
|
78
|
+
const runtime = runtimes.get(id);
|
|
79
|
+
|
|
80
|
+
if (!runtime) {
|
|
81
|
+
return res.status(404).json({ error: 'Runtime not found' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
res.json({
|
|
85
|
+
id,
|
|
86
|
+
port: runtime.port,
|
|
87
|
+
url: `http://localhost:${runtime.port}/mrp/v1`,
|
|
88
|
+
attached: true,
|
|
89
|
+
});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[runtime:attach]', err);
|
|
92
|
+
res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* POST /api/runtime/start-python
|
|
98
|
+
* Start a Python runtime
|
|
99
|
+
* Mirrors: electronAPI.startPython(venvPath, forceNew)
|
|
100
|
+
*/
|
|
101
|
+
router.post('/start-python', async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const { venvPath, forceNew = false } = req.body;
|
|
104
|
+
|
|
105
|
+
// Generate a runtime ID
|
|
106
|
+
const id = `python-${Date.now()}`;
|
|
107
|
+
|
|
108
|
+
// Check if we can reuse an existing runtime
|
|
109
|
+
if (!forceNew && venvPath) {
|
|
110
|
+
for (const [existingId, runtime] of runtimes) {
|
|
111
|
+
if (runtime.type === 'python' && runtime.venv === venvPath) {
|
|
112
|
+
if (runtime.process && !runtime.process.killed) {
|
|
113
|
+
return res.json({
|
|
114
|
+
id: existingId,
|
|
115
|
+
port: runtime.port,
|
|
116
|
+
url: `http://localhost:${runtime.port}/mrp/v1`,
|
|
117
|
+
reused: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Start new runtime via session API (reuse that logic)
|
|
125
|
+
// For now, return a placeholder
|
|
126
|
+
res.json({
|
|
127
|
+
id,
|
|
128
|
+
message: 'Use /api/session to start runtimes',
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error('[runtime:start-python]', err);
|
|
132
|
+
res.status(500).json({ error: err.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return router;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Export for use by session.js
|
|
140
|
+
export { runtimes };
|