mrmd-server 0.2.0 → 0.2.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/package.json +1 -1
- package/src/cloud-session-service.js +156 -84
- package/src/sync-manager.js +59 -1
- package/static/http-shim.js +150 -2
package/package.json
CHANGED
|
@@ -1,35 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CloudSessionService —
|
|
2
|
+
* CloudSessionService — hybrid runtime manager for cloud mode
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* mrmd-server runs inside an editor container (CLOUD_MODE=1).
|
|
4
|
+
* Python: Routes to a pre-existing runtime container (RUNTIME_PORT)
|
|
5
|
+
* Bash/R/Julia/PTY: Spawns local child processes via RuntimeService
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Used when mrmd-server runs inside an editor container (CLOUD_MODE=1).
|
|
8
|
+
* The Python runtime container is started by the orchestrator/compute-manager.
|
|
10
9
|
*/
|
|
11
10
|
|
|
11
|
+
import { RuntimeService } from './services.js';
|
|
12
|
+
|
|
13
|
+
/** Languages handled by the external runtime container */
|
|
14
|
+
const CLOUD_LANGUAGES = new Set(['python', 'py', 'python3']);
|
|
15
|
+
|
|
12
16
|
class CloudSessionService {
|
|
13
17
|
constructor(runtimePort, runtimeHost) {
|
|
14
18
|
this.runtimePort = runtimePort;
|
|
15
19
|
this.runtimeHost = runtimeHost || '127.0.0.1';
|
|
16
20
|
this.homeDir = process.env.CLOUD_HOME || process.env.HOME || '/home/ubuntu';
|
|
17
|
-
this.
|
|
21
|
+
this.pythonSessions = new Map();
|
|
18
22
|
this.defaultRuntime = null;
|
|
23
|
+
|
|
24
|
+
// Local RuntimeService for bash, R, Julia, PTY
|
|
25
|
+
this.localService = new RuntimeService();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Is this language handled by the cloud runtime container?
|
|
30
|
+
*/
|
|
31
|
+
_isCloudLanguage(language) {
|
|
32
|
+
return CLOUD_LANGUAGES.has((language || '').toLowerCase());
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
/**
|
|
22
|
-
* List all sessions.
|
|
23
|
-
* In cloud mode there's typically one session per runtime container.
|
|
36
|
+
* List all sessions (cloud Python + local runtimes).
|
|
24
37
|
*/
|
|
25
|
-
list() {
|
|
26
|
-
|
|
38
|
+
list(language) {
|
|
39
|
+
const cloud = Array.from(this.pythonSessions.values());
|
|
40
|
+
const local = this.localService.list(language);
|
|
41
|
+
|
|
42
|
+
if (language) {
|
|
43
|
+
if (this._isCloudLanguage(language)) return cloud;
|
|
44
|
+
return local;
|
|
45
|
+
}
|
|
46
|
+
return [...cloud, ...local];
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
/**
|
|
30
|
-
* Start a session
|
|
50
|
+
* Start a session.
|
|
51
|
+
* Python → register cloud runtime. Others → delegate to local RuntimeService.
|
|
31
52
|
*/
|
|
32
53
|
async start(config) {
|
|
54
|
+
const language = config.language || 'python';
|
|
55
|
+
|
|
56
|
+
if (!this._isCloudLanguage(language)) {
|
|
57
|
+
return this.localService.start(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Cloud Python: register pre-existing runtime container
|
|
33
61
|
const name = config.name || 'default';
|
|
34
62
|
const port = this.runtimePort;
|
|
35
63
|
|
|
@@ -45,7 +73,9 @@ class CloudSessionService {
|
|
|
45
73
|
|
|
46
74
|
const info = {
|
|
47
75
|
name,
|
|
76
|
+
language: 'python',
|
|
48
77
|
port,
|
|
78
|
+
url: `http://${this.runtimeHost}:${port}/mrp/v1`,
|
|
49
79
|
pid: null,
|
|
50
80
|
venv: null,
|
|
51
81
|
cwd: config.cwd || this.homeDir,
|
|
@@ -54,127 +84,169 @@ class CloudSessionService {
|
|
|
54
84
|
cloud: true,
|
|
55
85
|
};
|
|
56
86
|
|
|
57
|
-
this.
|
|
87
|
+
this.pythonSessions.set(name, info);
|
|
58
88
|
if (!this.defaultRuntime) this.defaultRuntime = name;
|
|
59
89
|
|
|
60
90
|
return info;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
/**
|
|
64
|
-
* Stop a session
|
|
65
|
-
* (that's the orchestrator's job). Just deregister locally.
|
|
94
|
+
* Stop a session.
|
|
66
95
|
*/
|
|
67
96
|
async stop(sessionName) {
|
|
68
|
-
this.
|
|
69
|
-
|
|
97
|
+
if (this.pythonSessions.has(sessionName)) {
|
|
98
|
+
this.pythonSessions.delete(sessionName);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return this.localService.stop(sessionName);
|
|
70
102
|
}
|
|
71
103
|
|
|
72
104
|
/**
|
|
73
|
-
* Restart
|
|
105
|
+
* Restart a session.
|
|
74
106
|
*/
|
|
75
107
|
async restart(sessionName) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
if (this.pythonSessions.has(sessionName)) {
|
|
109
|
+
const session = this.pythonSessions.get(sessionName);
|
|
110
|
+
const config = { name: sessionName, language: 'python', cwd: session?.cwd };
|
|
111
|
+
this.pythonSessions.delete(sessionName);
|
|
112
|
+
return this.start(config);
|
|
113
|
+
}
|
|
114
|
+
return this.localService.restart(sessionName);
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
/**
|
|
83
|
-
* Attach to
|
|
118
|
+
* Attach to an existing session.
|
|
84
119
|
*/
|
|
85
120
|
attach(sessionName) {
|
|
86
|
-
const
|
|
87
|
-
|
|
121
|
+
const cloudSession = this.pythonSessions.get(sessionName);
|
|
122
|
+
if (cloudSession) return cloudSession;
|
|
123
|
+
return this.localService.attach(sessionName);
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
/**
|
|
91
|
-
* Get or create
|
|
92
|
-
*
|
|
93
|
-
* Auto-starts (registers) the session if needed.
|
|
127
|
+
* Get or create ALL runtimes needed for a document.
|
|
128
|
+
* Python → cloud container. Bash/R/Julia/PTY → local RuntimeService.
|
|
94
129
|
*/
|
|
95
130
|
async getForDocument(documentPath, projectConfig, frontmatter, projectRoot) {
|
|
96
|
-
|
|
97
|
-
const projectName = projectRoot ? projectRoot.split('/').pop() : 'default';
|
|
98
|
-
const name = `${projectName}:default`;
|
|
131
|
+
const results = {};
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
// Python: cloud runtime
|
|
134
|
+
const projectName = projectRoot ? projectRoot.split('/').pop() : 'default';
|
|
135
|
+
const pythonName = `${projectName}:python:default`;
|
|
136
|
+
|
|
137
|
+
const existingPython = this.pythonSessions.get(pythonName);
|
|
138
|
+
if (existingPython?.alive) {
|
|
139
|
+
results.python = { ...existingPython, autoStart: true, available: true };
|
|
140
|
+
} else {
|
|
141
|
+
try {
|
|
142
|
+
const info = await this.start({ name: pythonName, language: 'python', cwd: projectRoot || this.homeDir });
|
|
143
|
+
results.python = {
|
|
144
|
+
...info,
|
|
145
|
+
autoStart: true,
|
|
146
|
+
available: true,
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
results.python = {
|
|
150
|
+
name: pythonName,
|
|
151
|
+
language: 'python',
|
|
152
|
+
port: null,
|
|
153
|
+
alive: false,
|
|
154
|
+
autoStart: true,
|
|
155
|
+
available: false,
|
|
156
|
+
error: err.message,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
103
159
|
}
|
|
104
160
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
name,
|
|
110
|
-
port: info.port,
|
|
111
|
-
alive: true,
|
|
112
|
-
autoStart: true,
|
|
113
|
-
venv: null,
|
|
114
|
-
cwd: info.cwd,
|
|
115
|
-
pid: null,
|
|
116
|
-
startedAt: info.startedAt,
|
|
117
|
-
};
|
|
118
|
-
} catch (err) {
|
|
119
|
-
return {
|
|
120
|
-
name,
|
|
121
|
-
port: null,
|
|
122
|
-
alive: false,
|
|
123
|
-
autoStart: true,
|
|
124
|
-
venv: null,
|
|
125
|
-
cwd: projectRoot || this.homeDir,
|
|
126
|
-
pid: null,
|
|
127
|
-
error: err.message,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
161
|
+
// Other languages: delegate to local RuntimeService
|
|
162
|
+
const localResults = await this.localService.getForDocument(
|
|
163
|
+
documentPath, projectConfig, frontmatter, projectRoot,
|
|
164
|
+
);
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const oldPort = this.runtimePort;
|
|
138
|
-
const oldHost = this.runtimeHost;
|
|
139
|
-
this.runtimePort = newPort;
|
|
140
|
-
if (newHost) this.runtimeHost = newHost;
|
|
141
|
-
for (const [name, session] of this.sessions) {
|
|
142
|
-
session.port = newPort;
|
|
166
|
+
// Merge: local results for non-Python, cloud result for Python
|
|
167
|
+
for (const [lang, info] of Object.entries(localResults)) {
|
|
168
|
+
if (lang !== 'python') {
|
|
169
|
+
results[lang] = info;
|
|
170
|
+
}
|
|
143
171
|
}
|
|
144
|
-
|
|
145
|
-
return
|
|
172
|
+
|
|
173
|
+
return results;
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
/**
|
|
149
|
-
* Get or create runtime for a specific language.
|
|
150
|
-
* In cloud mode, all languages route to the same runtime container.
|
|
177
|
+
* Get or create a runtime for a specific language.
|
|
151
178
|
*/
|
|
152
179
|
async getForDocumentLanguage(language, documentPath, projectConfig, frontmatter, projectRoot) {
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
if (this._isCloudLanguage(language)) {
|
|
181
|
+
const projectName = projectRoot ? projectRoot.split('/').pop() : 'default';
|
|
182
|
+
const name = `${projectName}:python:default`;
|
|
183
|
+
|
|
184
|
+
const existing = this.pythonSessions.get(name);
|
|
185
|
+
if (existing?.alive) {
|
|
186
|
+
return { ...existing, autoStart: true, available: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const info = await this.start({ name, language: 'python', cwd: projectRoot || this.homeDir });
|
|
191
|
+
return { ...info, autoStart: true, available: true };
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return {
|
|
194
|
+
name,
|
|
195
|
+
language: 'python',
|
|
196
|
+
port: null,
|
|
197
|
+
alive: false,
|
|
198
|
+
autoStart: true,
|
|
199
|
+
available: false,
|
|
200
|
+
error: err.message,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Non-Python: delegate to local RuntimeService
|
|
206
|
+
return this.localService.getForDocumentLanguage(
|
|
207
|
+
language, documentPath, projectConfig, frontmatter, projectRoot,
|
|
208
|
+
);
|
|
155
209
|
}
|
|
156
210
|
|
|
157
211
|
/**
|
|
158
212
|
* Check if a language is available.
|
|
159
|
-
* In cloud mode, the runtime container handles whatever it supports.
|
|
160
213
|
*/
|
|
161
214
|
isAvailable(language) {
|
|
162
|
-
return { available: true };
|
|
215
|
+
if (this._isCloudLanguage(language)) return { available: true };
|
|
216
|
+
return this.localService.isAvailable(language);
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
/**
|
|
166
|
-
* List supported languages.
|
|
167
|
-
* In cloud mode, Python is the primary; others depend on container image.
|
|
220
|
+
* List ALL supported languages (cloud + local).
|
|
168
221
|
*/
|
|
169
222
|
supportedLanguages() {
|
|
170
|
-
return ['python'];
|
|
223
|
+
return ['python', ...this.localService.supportedLanguages().filter(l => l !== 'python')];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update the cloud runtime port/host (called after CRIU migration).
|
|
228
|
+
*/
|
|
229
|
+
updateRuntimePort(newPort, newHost) {
|
|
230
|
+
const oldPort = this.runtimePort;
|
|
231
|
+
const oldHost = this.runtimeHost;
|
|
232
|
+
this.runtimePort = newPort;
|
|
233
|
+
if (newHost) this.runtimeHost = newHost;
|
|
234
|
+
for (const [, session] of this.pythonSessions) {
|
|
235
|
+
session.port = newPort;
|
|
236
|
+
session.url = `http://${this.runtimeHost}:${newPort}/mrp/v1`;
|
|
237
|
+
}
|
|
238
|
+
console.log(`[cloud-session] Runtime updated: ${oldHost}:${oldPort} → ${this.runtimeHost}:${newPort}`);
|
|
239
|
+
return { oldPort, newPort, oldHost, newHost: this.runtimeHost, sessionsUpdated: this.pythonSessions.size };
|
|
171
240
|
}
|
|
172
241
|
|
|
173
242
|
/**
|
|
174
|
-
* Shutdown
|
|
243
|
+
* Shutdown all sessions.
|
|
175
244
|
*/
|
|
176
245
|
shutdown() {
|
|
177
|
-
this.
|
|
246
|
+
this.pythonSessions.clear();
|
|
247
|
+
if (typeof this.localService.shutdown === 'function') {
|
|
248
|
+
this.localService.shutdown();
|
|
249
|
+
}
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
252
|
|
package/src/sync-manager.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ported from mrmd-electron/main.js to provide dynamic per-project sync servers.
|
|
5
5
|
* Allows mrmd-server to handle files from any project, not just a fixed projectDir.
|
|
6
|
+
*
|
|
7
|
+
* Supports three sync modes (via SYNC_MODE env var):
|
|
8
|
+
* - legacy: spawn local mrmd-sync per project (default)
|
|
9
|
+
* - mirror: same as legacy (mirroring handled by orchestrator WS proxy)
|
|
10
|
+
* - relay_primary: use relay-client to sync project dir with cloud relay
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { spawn } from 'child_process';
|
|
@@ -19,6 +24,10 @@ import { SYNC_SERVER_MEMORY_MB, DIR_HASH_LENGTH } from 'mrmd-electron/src/config
|
|
|
19
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
25
|
const __dirname = path.dirname(__filename);
|
|
21
26
|
|
|
27
|
+
const SYNC_MODE = process.env.SYNC_MODE || 'legacy';
|
|
28
|
+
const SYNC_RELAY_URL = process.env.SYNC_RELAY_URL || 'ws://localhost:3006';
|
|
29
|
+
const CLOUD_USER_ID = process.env.CLOUD_USER_ID || '';
|
|
30
|
+
|
|
22
31
|
// Track active sync servers by directory hash
|
|
23
32
|
const syncServers = new Map();
|
|
24
33
|
|
|
@@ -94,6 +103,9 @@ export function onSyncDeath(listener) {
|
|
|
94
103
|
* Get or start a sync server for a project directory
|
|
95
104
|
* Uses reference counting so multiple documents can share a sync server
|
|
96
105
|
*
|
|
106
|
+
* In relay_primary mode, creates a relay client instead of a local sync server.
|
|
107
|
+
* The relay client syncs the project dir with the cloud relay bidirectionally.
|
|
108
|
+
*
|
|
97
109
|
* @param {string} projectDir - The project directory to sync
|
|
98
110
|
* @returns {Promise<{port: number, dir: string, refCount: number}>}
|
|
99
111
|
*/
|
|
@@ -104,10 +116,56 @@ export async function acquireSyncServer(projectDir) {
|
|
|
104
116
|
if (syncServers.has(dirHash)) {
|
|
105
117
|
const server = syncServers.get(dirHash);
|
|
106
118
|
server.refCount++;
|
|
107
|
-
console.log(`[sync] Reusing server for ${projectDir}
|
|
119
|
+
console.log(`[sync] Reusing server for ${projectDir} (refCount: ${server.refCount})`);
|
|
108
120
|
return server;
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
// ── Relay-primary mode: use relay client instead of local sync server ──
|
|
124
|
+
if (SYNC_MODE === 'relay_primary' && CLOUD_USER_ID) {
|
|
125
|
+
// Derive project name from the directory name
|
|
126
|
+
const projectName = path.basename(projectDir);
|
|
127
|
+
|
|
128
|
+
console.log(`[sync] Starting relay client for ${projectDir} (project: ${projectName})`);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Dynamic import so this doesn't fail in environments without relay-client
|
|
132
|
+
const { createRelayClient } = await import('mrmd-sync/src/relay-client.js');
|
|
133
|
+
|
|
134
|
+
const relayClient = createRelayClient({
|
|
135
|
+
relayUrl: SYNC_RELAY_URL,
|
|
136
|
+
projectDir,
|
|
137
|
+
userId: CLOUD_USER_ID,
|
|
138
|
+
project: projectName,
|
|
139
|
+
writeDebounceMs: 1000,
|
|
140
|
+
log: (msg) => console.log(msg),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await relayClient.start();
|
|
144
|
+
|
|
145
|
+
// Return a server-like object that the rest of the code can use.
|
|
146
|
+
// port=0 signals that sync is handled by the relay (not a local WS server).
|
|
147
|
+
// The orchestrator's WS proxy routes sync traffic directly to the relay.
|
|
148
|
+
const server = {
|
|
149
|
+
proc: null,
|
|
150
|
+
port: 0,
|
|
151
|
+
dir: projectDir,
|
|
152
|
+
refCount: 1,
|
|
153
|
+
owned: true,
|
|
154
|
+
isRelay: true,
|
|
155
|
+
relayClient,
|
|
156
|
+
};
|
|
157
|
+
syncServers.set(dirHash, server);
|
|
158
|
+
console.log(`[sync] Relay client started for ${projectDir}`);
|
|
159
|
+
return server;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(`[sync] Failed to start relay client for ${projectDir}: ${err.message}`);
|
|
162
|
+
console.warn(`[sync] Falling back to local sync server`);
|
|
163
|
+
// Fall through to local server below
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Legacy / mirror mode: spawn local mrmd-sync process ──
|
|
168
|
+
|
|
111
169
|
// Check for existing server from a PID file (in case of restart)
|
|
112
170
|
const syncStatePath = path.join(os.tmpdir(), `mrmd-sync-${dirHash}`, 'server.pid');
|
|
113
171
|
try {
|
package/static/http-shim.js
CHANGED
|
@@ -110,6 +110,39 @@
|
|
|
110
110
|
const POST = (path, body) => apiCall('POST', path, body);
|
|
111
111
|
const DELETE = (path) => apiCall('DELETE', path);
|
|
112
112
|
|
|
113
|
+
const DOC_EXTENSIONS = ['.md', '.qmd'];
|
|
114
|
+
|
|
115
|
+
function basenameFromPath(filePath = '') {
|
|
116
|
+
const normalized = String(filePath).replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
117
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
118
|
+
return parts.length ? parts[parts.length - 1] : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function dirnameFromPath(filePath = '') {
|
|
122
|
+
const normalized = String(filePath).replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
123
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
124
|
+
if (lastSlash <= 0) return normalized.startsWith('/') ? '/' : '.';
|
|
125
|
+
return normalized.slice(0, lastSlash) || '/';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function stripDocExtension(fileName = '') {
|
|
129
|
+
const lower = fileName.toLowerCase();
|
|
130
|
+
for (const ext of DOC_EXTENSIONS) {
|
|
131
|
+
if (lower.endsWith(ext)) {
|
|
132
|
+
return fileName.slice(0, -ext.length);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return fileName;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function makeRuntimeIdFromVenv(venvPath = '', forceNew = false) {
|
|
139
|
+
const venvName = basenameFromPath(venvPath).replace(/^\.+/, '') || 'venv';
|
|
140
|
+
const projectName = basenameFromPath(dirnameFromPath(venvPath)).replace(/^\.+/, '') || 'project';
|
|
141
|
+
let name = `${projectName}:${venvName}`.replace(/[^a-zA-Z0-9-:]/g, '-');
|
|
142
|
+
if (forceNew) name += '-' + Date.now().toString(36).slice(-4);
|
|
143
|
+
return name;
|
|
144
|
+
}
|
|
145
|
+
|
|
113
146
|
// ==========================================================================
|
|
114
147
|
// WebSocket for Events
|
|
115
148
|
// ==========================================================================
|
|
@@ -322,8 +355,20 @@
|
|
|
322
355
|
GET(`/api/file/preview?path=${encodeURIComponent(filePath)}&lines=${lines || 40}`)
|
|
323
356
|
.then(r => r.content),
|
|
324
357
|
|
|
325
|
-
getFileInfo: (filePath) =>
|
|
326
|
-
|
|
358
|
+
getFileInfo: async (filePath) => {
|
|
359
|
+
try {
|
|
360
|
+
const info = await GET(`/api/file/info?path=${encodeURIComponent(filePath)}`);
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
...info,
|
|
364
|
+
};
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
error: err.message,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
},
|
|
327
372
|
|
|
328
373
|
// ========================================================================
|
|
329
374
|
// Venv creation (still useful for setup flows)
|
|
@@ -332,6 +377,109 @@
|
|
|
332
377
|
createVenv: (venvPath) =>
|
|
333
378
|
POST('/api/system/create-venv', { venvPath }),
|
|
334
379
|
|
|
380
|
+
installMrmdPython: (venvPath) =>
|
|
381
|
+
POST('/api/system/install-mrmd-python', { venvPath }),
|
|
382
|
+
|
|
383
|
+
startPython: async (venvPath, forceNew = false) => {
|
|
384
|
+
try {
|
|
385
|
+
const runtimeId = makeRuntimeIdFromVenv(venvPath, forceNew);
|
|
386
|
+
const cwd = dirnameFromPath(venvPath);
|
|
387
|
+
const result = await POST('/api/runtime', {
|
|
388
|
+
config: {
|
|
389
|
+
name: runtimeId,
|
|
390
|
+
language: 'python',
|
|
391
|
+
cwd,
|
|
392
|
+
venv: venvPath,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
success: true,
|
|
398
|
+
port: result.port,
|
|
399
|
+
runtimeId,
|
|
400
|
+
venv: result.venv || venvPath,
|
|
401
|
+
url: result.url,
|
|
402
|
+
};
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: err.message,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
attachRuntime: async (runtimeId) => {
|
|
412
|
+
try {
|
|
413
|
+
const result = await POST(`/api/runtime/${encodeURIComponent(runtimeId)}/attach`, {});
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
port: result.port,
|
|
417
|
+
url: result.url,
|
|
418
|
+
venv: result.venv,
|
|
419
|
+
};
|
|
420
|
+
} catch (err) {
|
|
421
|
+
return {
|
|
422
|
+
success: false,
|
|
423
|
+
error: err.message,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
openFile: async (filePath) => {
|
|
429
|
+
try {
|
|
430
|
+
// Ensure project is detected and sync server is available.
|
|
431
|
+
const project = await GET(`/api/project?path=${encodeURIComponent(filePath)}`);
|
|
432
|
+
const projectDir = project?.root || dirnameFromPath(filePath);
|
|
433
|
+
|
|
434
|
+
let syncPort = project?.syncPort;
|
|
435
|
+
if (!syncPort) {
|
|
436
|
+
const sync = await POST('/api/project/sync/acquire', { projectDir });
|
|
437
|
+
syncPort = sync.port;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Mirror Electron behavior: track recent file usage.
|
|
441
|
+
try {
|
|
442
|
+
await POST('/api/system/recent', { file: filePath });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.warn('[http-shim] Failed to update recent file:', err.message);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const fileName = basenameFromPath(filePath);
|
|
448
|
+
const docName = stripDocExtension(fileName);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
syncPort,
|
|
453
|
+
docName,
|
|
454
|
+
pythonPort: null,
|
|
455
|
+
projectDir,
|
|
456
|
+
};
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: err.message,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
listRuntimes: async () => {
|
|
466
|
+
try {
|
|
467
|
+
const runtimes = await GET('/api/runtime');
|
|
468
|
+
return { runtimes };
|
|
469
|
+
} catch (err) {
|
|
470
|
+
return { runtimes: [], error: err.message };
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
killRuntime: async (runtimeId) => {
|
|
475
|
+
try {
|
|
476
|
+
await DELETE(`/api/runtime/${encodeURIComponent(runtimeId)}`);
|
|
477
|
+
return { success: true };
|
|
478
|
+
} catch (err) {
|
|
479
|
+
return { success: false, error: err.message };
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
|
|
335
483
|
// ========================================================================
|
|
336
484
|
// PROJECT SERVICE
|
|
337
485
|
// ========================================================================
|