mrmd-server 0.1.0 → 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/LICENSE +21 -0
- package/bin/cli.js +4 -20
- package/package.json +20 -3
- package/src/api/bash.js +72 -189
- package/src/api/file.js +26 -20
- package/src/api/index.js +5 -0
- package/src/api/notebook.js +290 -0
- package/src/api/project.js +178 -12
- package/src/api/pty.js +73 -293
- package/src/api/r.js +337 -0
- package/src/api/session.js +96 -251
- package/src/api/settings.js +782 -0
- package/src/api/system.js +199 -1
- package/src/server.js +117 -6
- package/src/services.js +42 -0
- package/src/sync-manager.js +223 -0
- package/static/favicon.png +0 -0
- package/static/http-shim.js +172 -3
- package/static/index.html +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maxime Rivest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/cli.js
CHANGED
|
@@ -128,26 +128,10 @@ async function main() {
|
|
|
128
128
|
|
|
129
129
|
await server.start();
|
|
130
130
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log(' Starting mrmd-sync...');
|
|
137
|
-
const syncProc = spawn('node', [syncPath, '--port', '4444', options.projectDir], {
|
|
138
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
syncProc.stdout.on('data', (data) => {
|
|
142
|
-
if (data.toString().includes('Server started')) {
|
|
143
|
-
console.log(` Sync: ws://localhost:4444`);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
server.context.syncProcess = syncProc;
|
|
148
|
-
} catch {
|
|
149
|
-
console.log(' Sync: (mrmd-sync not found, start manually)');
|
|
150
|
-
}
|
|
131
|
+
// Sync servers are now started dynamically per-project when files are opened
|
|
132
|
+
// via the sync-manager.js acquireSyncServer() function
|
|
133
|
+
console.log(' Sync: (dynamic per-project)');
|
|
134
|
+
console.log('');
|
|
151
135
|
|
|
152
136
|
// Keep running
|
|
153
137
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mrmd-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"mrmd-server": "./bin/cli.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"bin",
|
|
13
|
+
"static",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
10
16
|
"scripts": {
|
|
11
17
|
"start": "node bin/cli.js",
|
|
12
18
|
"dev": "node bin/cli.js --dev"
|
|
@@ -17,9 +23,18 @@
|
|
|
17
23
|
"notebook",
|
|
18
24
|
"server",
|
|
19
25
|
"remote",
|
|
20
|
-
"collaboration"
|
|
26
|
+
"collaboration",
|
|
27
|
+
"literate-programming",
|
|
28
|
+
"code-execution"
|
|
21
29
|
],
|
|
30
|
+
"author": "Anthropic",
|
|
22
31
|
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/anthropics/mrmd.git",
|
|
35
|
+
"directory": "packages/mrmd-server"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/anthropics/mrmd",
|
|
23
38
|
"engines": {
|
|
24
39
|
"node": ">=18.0.0"
|
|
25
40
|
},
|
|
@@ -30,6 +45,8 @@
|
|
|
30
45
|
"multer": "^1.4.5-lts.1",
|
|
31
46
|
"chokidar": "^3.6.0",
|
|
32
47
|
"fzf": "^0.5.2",
|
|
33
|
-
"mrmd-project": "
|
|
48
|
+
"mrmd-project": "^0.1.1",
|
|
49
|
+
"mrmd-electron": "^0.3.1",
|
|
50
|
+
"mrmd-sync": "^0.3.2"
|
|
34
51
|
}
|
|
35
52
|
}
|
package/src/api/bash.js
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bash Session API routes
|
|
3
3
|
*
|
|
4
|
-
* Mirrors electronAPI.bash.*
|
|
4
|
+
* Mirrors electronAPI.bash.* using BashSessionService from mrmd-electron
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Router } from 'express';
|
|
8
|
-
import {
|
|
8
|
+
import { Project } from 'mrmd-project';
|
|
9
|
+
import fs from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
|
-
import net from 'net';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
28
|
|
|
15
29
|
/**
|
|
16
30
|
* Create bash routes
|
|
@@ -18,6 +32,7 @@ const bashSessions = new Map();
|
|
|
18
32
|
*/
|
|
19
33
|
export function createBashRoutes(ctx) {
|
|
20
34
|
const router = Router();
|
|
35
|
+
const { bashSessionService } = ctx;
|
|
21
36
|
|
|
22
37
|
/**
|
|
23
38
|
* GET /api/bash
|
|
@@ -26,15 +41,7 @@ export function createBashRoutes(ctx) {
|
|
|
26
41
|
*/
|
|
27
42
|
router.get('/', async (req, res) => {
|
|
28
43
|
try {
|
|
29
|
-
const list =
|
|
30
|
-
for (const [name, session] of bashSessions) {
|
|
31
|
-
list.push({
|
|
32
|
-
name,
|
|
33
|
-
port: session.port,
|
|
34
|
-
cwd: session.cwd,
|
|
35
|
-
running: session.process && !session.process.killed,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
44
|
+
const list = bashSessionService.list();
|
|
38
45
|
res.json(list);
|
|
39
46
|
} catch (err) {
|
|
40
47
|
console.error('[bash:list]', err);
|
|
@@ -50,86 +57,19 @@ export function createBashRoutes(ctx) {
|
|
|
50
57
|
router.post('/', async (req, res) => {
|
|
51
58
|
try {
|
|
52
59
|
const { config } = req.body;
|
|
53
|
-
const { name, cwd } = config || {};
|
|
54
60
|
|
|
55
|
-
if (!name) {
|
|
61
|
+
if (!config?.name) {
|
|
56
62
|
return res.status(400).json({ error: 'config.name required' });
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
if (bashSessions.has(name)) {
|
|
61
|
-
const existing = bashSessions.get(name);
|
|
62
|
-
if (existing.process && !existing.process.killed) {
|
|
63
|
-
return res.json({
|
|
64
|
-
name,
|
|
65
|
-
port: existing.port,
|
|
66
|
-
cwd: existing.cwd,
|
|
67
|
-
reused: true,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Find free port
|
|
73
|
-
const port = await findFreePort(8101, 8200);
|
|
74
|
-
|
|
75
|
-
const workDir = cwd ? path.resolve(ctx.projectDir, cwd) : ctx.projectDir;
|
|
76
|
-
|
|
77
|
-
// Try to find mrmd-bash package
|
|
78
|
-
const mrmdBashPaths = [
|
|
79
|
-
path.join(ctx.projectDir, '../mrmd-bash'),
|
|
80
|
-
path.join(process.cwd(), '../mrmd-bash'),
|
|
81
|
-
path.join(process.cwd(), 'mrmd-bash'),
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
let mrmdBashPath = null;
|
|
85
|
-
for (const p of mrmdBashPaths) {
|
|
86
|
-
try {
|
|
87
|
-
const fs = await import('fs/promises');
|
|
88
|
-
await fs.access(path.join(p, 'pyproject.toml'));
|
|
89
|
-
mrmdBashPath = p;
|
|
90
|
-
break;
|
|
91
|
-
} catch {}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let proc;
|
|
95
|
-
if (mrmdBashPath) {
|
|
96
|
-
proc = spawn('uv', [
|
|
97
|
-
'run', '--project', mrmdBashPath,
|
|
98
|
-
'mrmd-bash',
|
|
99
|
-
'--port', port.toString(),
|
|
100
|
-
], {
|
|
101
|
-
cwd: workDir,
|
|
102
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
// Fallback: assume mrmd-bash is installed
|
|
106
|
-
proc = spawn('mrmd-bash', [
|
|
107
|
-
'--port', port.toString(),
|
|
108
|
-
], {
|
|
109
|
-
cwd: workDir,
|
|
110
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Wait for server to start
|
|
115
|
-
await waitForPort(port, 15000);
|
|
116
|
-
|
|
117
|
-
bashSessions.set(name, {
|
|
118
|
-
port,
|
|
119
|
-
process: proc,
|
|
120
|
-
cwd: workDir,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
proc.on('exit', (code) => {
|
|
124
|
-
console.log(`[bash] ${name} exited with code ${code}`);
|
|
125
|
-
bashSessions.delete(name);
|
|
126
|
-
});
|
|
65
|
+
const result = await bashSessionService.start(config);
|
|
127
66
|
|
|
128
67
|
res.json({
|
|
129
|
-
name,
|
|
130
|
-
port,
|
|
131
|
-
cwd:
|
|
132
|
-
|
|
68
|
+
name: result.name,
|
|
69
|
+
port: result.port,
|
|
70
|
+
cwd: result.cwd,
|
|
71
|
+
pid: result.pid,
|
|
72
|
+
url: `http://localhost:${result.port}/mrp/v1`,
|
|
133
73
|
});
|
|
134
74
|
} catch (err) {
|
|
135
75
|
console.error('[bash:start]', err);
|
|
@@ -145,21 +85,11 @@ export function createBashRoutes(ctx) {
|
|
|
145
85
|
router.delete('/:name', async (req, res) => {
|
|
146
86
|
try {
|
|
147
87
|
const { name } = req.params;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!session) {
|
|
151
|
-
return res.json({ success: true, message: 'Session not found' });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (session.process && !session.process.killed) {
|
|
155
|
-
session.process.kill();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
bashSessions.delete(name);
|
|
88
|
+
await bashSessionService.stop(name);
|
|
159
89
|
res.json({ success: true });
|
|
160
90
|
} catch (err) {
|
|
161
91
|
console.error('[bash:stop]', err);
|
|
162
|
-
res.
|
|
92
|
+
res.json({ success: true, message: err.message });
|
|
163
93
|
}
|
|
164
94
|
});
|
|
165
95
|
|
|
@@ -171,29 +101,15 @@ export function createBashRoutes(ctx) {
|
|
|
171
101
|
router.post('/:name/restart', async (req, res) => {
|
|
172
102
|
try {
|
|
173
103
|
const { name } = req.params;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
if (!session) {
|
|
177
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Kill existing
|
|
181
|
-
if (session.process && !session.process.killed) {
|
|
182
|
-
session.process.kill();
|
|
183
|
-
}
|
|
104
|
+
const result = await bashSessionService.restart(name);
|
|
184
105
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (handler) {
|
|
193
|
-
return handler.route.stack[0].handle(req, res);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
res.status(500).json({ error: 'Could not restart' });
|
|
106
|
+
res.json({
|
|
107
|
+
name: result.name,
|
|
108
|
+
port: result.port,
|
|
109
|
+
cwd: result.cwd,
|
|
110
|
+
pid: result.pid,
|
|
111
|
+
url: `http://localhost:${result.port}/mrp/v1`,
|
|
112
|
+
});
|
|
197
113
|
} catch (err) {
|
|
198
114
|
console.error('[bash:restart]', err);
|
|
199
115
|
res.status(500).json({ error: err.message });
|
|
@@ -204,90 +120,57 @@ export function createBashRoutes(ctx) {
|
|
|
204
120
|
* POST /api/bash/for-document
|
|
205
121
|
* Get or create bash session for a document
|
|
206
122
|
* Mirrors: electronAPI.bash.forDocument(documentPath)
|
|
123
|
+
*
|
|
124
|
+
* Automatically detects project if projectConfig/projectRoot not provided
|
|
207
125
|
*/
|
|
208
126
|
router.post('/for-document', async (req, res) => {
|
|
209
127
|
try {
|
|
210
|
-
|
|
128
|
+
let { documentPath, projectConfig, frontmatter, projectRoot } = req.body;
|
|
129
|
+
|
|
211
130
|
if (!documentPath) {
|
|
212
131
|
return res.status(400).json({ error: 'documentPath required' });
|
|
213
132
|
}
|
|
214
133
|
|
|
215
|
-
|
|
134
|
+
// Auto-detect project if not provided
|
|
135
|
+
if (!projectConfig || !projectRoot) {
|
|
136
|
+
const detected = detectProject(documentPath);
|
|
137
|
+
if (detected) {
|
|
138
|
+
projectRoot = projectRoot || detected.root;
|
|
139
|
+
projectConfig = projectConfig || detected.config;
|
|
140
|
+
} else {
|
|
141
|
+
projectRoot = projectRoot || (ctx.projectDir || process.cwd());
|
|
142
|
+
projectConfig = projectConfig || {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
216
145
|
|
|
217
|
-
//
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
146
|
+
// Auto-parse frontmatter if not provided
|
|
147
|
+
if (!frontmatter) {
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(documentPath, 'utf8');
|
|
150
|
+
frontmatter = Project.parseFrontmatter(content);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
frontmatter = null;
|
|
153
|
+
}
|
|
226
154
|
}
|
|
227
155
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
156
|
+
const result = await bashSessionService.getForDocument(
|
|
157
|
+
documentPath,
|
|
158
|
+
projectConfig,
|
|
159
|
+
frontmatter,
|
|
160
|
+
projectRoot
|
|
161
|
+
);
|
|
234
162
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return handler.route.stack[0].handle(req, res);
|
|
163
|
+
// Add url if we have a port
|
|
164
|
+
if (result?.port) {
|
|
165
|
+
result.url = `http://localhost:${result.port}/mrp/v1`;
|
|
239
166
|
}
|
|
240
167
|
|
|
241
|
-
res.
|
|
168
|
+
res.json(result);
|
|
242
169
|
} catch (err) {
|
|
243
170
|
console.error('[bash:forDocument]', err);
|
|
244
|
-
res.
|
|
171
|
+
res.json(null);
|
|
245
172
|
}
|
|
246
173
|
});
|
|
247
174
|
|
|
248
175
|
return router;
|
|
249
176
|
}
|
|
250
|
-
|
|
251
|
-
async function findFreePort(start, end) {
|
|
252
|
-
for (let port = start; port <= end; port++) {
|
|
253
|
-
if (await isPortFree(port)) {
|
|
254
|
-
return port;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
throw new Error(`No free port found in range ${start}-${end}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function isPortFree(port) {
|
|
261
|
-
return new Promise((resolve) => {
|
|
262
|
-
const server = net.createServer();
|
|
263
|
-
server.once('error', () => resolve(false));
|
|
264
|
-
server.once('listening', () => {
|
|
265
|
-
server.close();
|
|
266
|
-
resolve(true);
|
|
267
|
-
});
|
|
268
|
-
server.listen(port, '127.0.0.1');
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function waitForPort(port, timeout = 10000) {
|
|
273
|
-
return new Promise((resolve, reject) => {
|
|
274
|
-
const start = Date.now();
|
|
275
|
-
|
|
276
|
-
function check() {
|
|
277
|
-
const socket = net.connect(port, '127.0.0.1');
|
|
278
|
-
socket.once('connect', () => {
|
|
279
|
-
socket.end();
|
|
280
|
-
resolve();
|
|
281
|
-
});
|
|
282
|
-
socket.once('error', () => {
|
|
283
|
-
if (Date.now() - start > timeout) {
|
|
284
|
-
reject(new Error(`Timeout waiting for port ${port}`));
|
|
285
|
-
} else {
|
|
286
|
-
setTimeout(check, 200);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
check();
|
|
292
|
-
});
|
|
293
|
-
}
|
package/src/api/file.js
CHANGED
|
@@ -48,7 +48,7 @@ export function createFileRoutes(ctx) {
|
|
|
48
48
|
return res.status(400).json({ error: 'filePath required' });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const fullPath =
|
|
51
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
52
52
|
|
|
53
53
|
// Create directory if needed
|
|
54
54
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
@@ -84,7 +84,7 @@ export function createFileRoutes(ctx) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const root = projectRoot || ctx.projectDir;
|
|
87
|
-
const fullPath =
|
|
87
|
+
const fullPath = resolvePath(root, relativePath);
|
|
88
88
|
|
|
89
89
|
// Create directory if needed
|
|
90
90
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
@@ -138,8 +138,8 @@ export function createFileRoutes(ctx) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
const root = projectRoot || ctx.projectDir;
|
|
141
|
-
const fullFromPath =
|
|
142
|
-
const fullToPath =
|
|
141
|
+
const fullFromPath = resolvePath(root, fromPath);
|
|
142
|
+
const fullToPath = resolvePath(root, toPath);
|
|
143
143
|
|
|
144
144
|
// Create destination directory if needed
|
|
145
145
|
await fs.mkdir(path.dirname(fullToPath), { recursive: true });
|
|
@@ -187,15 +187,15 @@ export function createFileRoutes(ctx) {
|
|
|
187
187
|
// 3. Renaming files with new prefixes
|
|
188
188
|
|
|
189
189
|
// For now, just do a simple move
|
|
190
|
-
const fullSourcePath =
|
|
190
|
+
const fullSourcePath = resolvePath(root, sourcePath);
|
|
191
191
|
let fullTargetPath;
|
|
192
192
|
|
|
193
193
|
if (position === 'inside') {
|
|
194
194
|
// Move into target directory
|
|
195
|
-
fullTargetPath =
|
|
195
|
+
fullTargetPath = resolvePath(root, path.join(targetPath, path.basename(sourcePath)));
|
|
196
196
|
} else {
|
|
197
197
|
// Move to same directory as target
|
|
198
|
-
fullTargetPath =
|
|
198
|
+
fullTargetPath = resolvePath(root, path.join(path.dirname(targetPath), path.basename(sourcePath)));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
await fs.mkdir(path.dirname(fullTargetPath), { recursive: true });
|
|
@@ -228,7 +228,7 @@ export function createFileRoutes(ctx) {
|
|
|
228
228
|
return res.status(400).json({ error: 'path query parameter required' });
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
const fullPath =
|
|
231
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
232
232
|
|
|
233
233
|
const stat = await fs.stat(fullPath);
|
|
234
234
|
if (stat.isDirectory()) {
|
|
@@ -257,7 +257,7 @@ export function createFileRoutes(ctx) {
|
|
|
257
257
|
return res.status(400).json({ error: 'path query parameter required' });
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
const fullPath =
|
|
260
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
261
261
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
262
262
|
|
|
263
263
|
res.json({ success: true, content });
|
|
@@ -282,7 +282,7 @@ export function createFileRoutes(ctx) {
|
|
|
282
282
|
return res.status(400).json({ error: 'filePath required' });
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
const fullPath =
|
|
285
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
286
286
|
|
|
287
287
|
// Create directory if needed
|
|
288
288
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
@@ -310,7 +310,7 @@ export function createFileRoutes(ctx) {
|
|
|
310
310
|
return res.status(400).json({ error: 'path query parameter required' });
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const fullPath =
|
|
313
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
314
314
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
315
315
|
const previewLines = content.split('\n').slice(0, lines).join('\n');
|
|
316
316
|
|
|
@@ -336,7 +336,7 @@ export function createFileRoutes(ctx) {
|
|
|
336
336
|
return res.status(400).json({ error: 'path query parameter required' });
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
const fullPath =
|
|
339
|
+
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
340
340
|
const stat = await fs.stat(fullPath);
|
|
341
341
|
|
|
342
342
|
res.json({
|
|
@@ -359,17 +359,23 @@ export function createFileRoutes(ctx) {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
/**
|
|
362
|
-
* Resolve path
|
|
362
|
+
* Resolve path - allows full filesystem access
|
|
363
|
+
*
|
|
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
|
+
* @param {string} basePath - Base path for relative paths (ignored for absolute)
|
|
368
|
+
* @param {string} inputPath - Path to resolve (absolute or relative)
|
|
369
|
+
* @returns {string} Resolved absolute path
|
|
363
370
|
*/
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!resolved.startsWith(path.resolve(projectDir))) {
|
|
369
|
-
throw new Error('Path traversal not allowed');
|
|
371
|
+
function resolvePath(basePath, inputPath) {
|
|
372
|
+
// If it's already absolute, use it directly
|
|
373
|
+
if (path.isAbsolute(inputPath)) {
|
|
374
|
+
return inputPath;
|
|
370
375
|
}
|
|
371
376
|
|
|
372
|
-
|
|
377
|
+
// Otherwise resolve relative to basePath
|
|
378
|
+
return path.resolve(basePath, inputPath);
|
|
373
379
|
}
|
|
374
380
|
|
|
375
381
|
/**
|
package/src/api/index.js
CHANGED
|
@@ -9,3 +9,8 @@ export { createFileRoutes } from './file.js';
|
|
|
9
9
|
export { createAssetRoutes } from './asset.js';
|
|
10
10
|
export { createSystemRoutes } from './system.js';
|
|
11
11
|
export { createRuntimeRoutes } from './runtime.js';
|
|
12
|
+
export { createJuliaRoutes } from './julia.js';
|
|
13
|
+
export { createPtyRoutes } from './pty.js';
|
|
14
|
+
export { createNotebookRoutes } from './notebook.js';
|
|
15
|
+
export { createSettingsRoutes } from './settings.js';
|
|
16
|
+
export { createRRoutes } from './r.js';
|