@goodandready/dsh-remote-workspace 0.1.1 → 0.3.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/lib/client.js +1991 -1278
- package/lib/docker-service.js +93 -0
- package/lib/health-service.js +117 -0
- package/lib/index.js +116 -10
- package/lib/mirror-sync-service.js +54 -13
- package/lib/remote-fs-service.js +58 -7
- package/lib/routes.js +339 -46
- package/lib/ssh-service.js +207 -20
- package/lib/tools.js +124 -12
- package/lib/tunnel-service.js +79 -36
- package/lib/vault-service.js +184 -0
- package/lib/watcher-service.js +111 -0
- package/package.json +1 -1
package/lib/routes.js
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
|
-
export function
|
|
2
|
-
|
|
3
|
-
res.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
1
|
+
export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store, dockerService, healthService, watcherService) {
|
|
2
|
+
function writeJson(res, statusCode, body) {
|
|
3
|
+
if (typeof res.setHeader === 'function') {
|
|
4
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
5
|
+
res.statusCode = statusCode;
|
|
6
|
+
} else if (typeof res.writeHead === 'function') {
|
|
7
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8
|
+
} else {
|
|
9
|
+
res.statusCode = statusCode;
|
|
10
|
+
}
|
|
11
|
+
res.end(JSON.stringify(body));
|
|
12
|
+
}
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
req.on('end', () => {
|
|
22
|
-
try {
|
|
23
|
-
const raw = Buffer.concat(chunks).toString('utf8');
|
|
24
|
-
resolve(raw ? JSON.parse(raw) : {});
|
|
25
|
-
} catch (err) {
|
|
26
|
-
reject(new Error('Invalid JSON'));
|
|
27
|
-
}
|
|
14
|
+
function readBody(req) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let body = '';
|
|
17
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
18
|
+
req.on('end', () => {
|
|
19
|
+
try {
|
|
20
|
+
resolve(body ? JSON.parse(body) : {});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
reject(err);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
req.on('error', reject);
|
|
28
26
|
});
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
}
|
|
27
|
+
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
function isTrustedSettingsRequest(req) {
|
|
30
|
+
const rawAuth = req.headers['authorization'];
|
|
31
|
+
if (typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) return true;
|
|
32
|
+
const cookieHeader = req.headers['cookie'];
|
|
33
|
+
if (typeof cookieHeader === 'string' && (cookieHeader.includes('token=') || cookieHeader.includes('dsh_token='))) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const secFetch = req.headers['sec-fetch-site'];
|
|
37
|
+
if (secFetch === 'same-origin') return true;
|
|
38
|
+
const ip = req.socket?.remoteAddress || '';
|
|
39
|
+
if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
37
42
|
|
|
38
|
-
export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store) {
|
|
39
43
|
ctx.inject(['webServer'], (sctx) => {
|
|
40
44
|
const webServer = sctx.get ? (sctx.get('webServer') ?? sctx.webServer) : sctx.webServer;
|
|
41
45
|
if (!webServer || typeof webServer.register !== 'function') return;
|
|
@@ -46,10 +50,22 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
46
50
|
path: '/dsh-remote-workspace/state',
|
|
47
51
|
handler: async (req, res) => {
|
|
48
52
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
try {
|
|
54
|
+
const profiles = store.getProfiles();
|
|
55
|
+
const activeId = store.getActiveId() || (profiles[0]?.id || '');
|
|
56
|
+
const tunnels = tunnelService.listActiveTunnels();
|
|
57
|
+
const autoSync = store.isAutoSyncEnabled ? store.isAutoSyncEnabled() : false;
|
|
58
|
+
writeJson(res, 200, {
|
|
59
|
+
ok: true,
|
|
60
|
+
profiles,
|
|
61
|
+
activeId,
|
|
62
|
+
tunnels,
|
|
63
|
+
autoSync,
|
|
64
|
+
vaultProtected: true
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
68
|
+
}
|
|
53
69
|
}
|
|
54
70
|
}), 'dsh-remote-workspace: /state');
|
|
55
71
|
|
|
@@ -67,7 +83,8 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
67
83
|
}
|
|
68
84
|
sshService.invalidate(profile.id);
|
|
69
85
|
await store.saveProfile(profile);
|
|
70
|
-
|
|
86
|
+
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
|
|
87
|
+
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
71
88
|
} catch (err) {
|
|
72
89
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
73
90
|
}
|
|
@@ -119,7 +136,10 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
119
136
|
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
120
137
|
try {
|
|
121
138
|
const body = await readBody(req);
|
|
122
|
-
|
|
139
|
+
let profile = body.profile || (body.id ? store.getProfile(body.id) : body);
|
|
140
|
+
if (store.vault && profile) {
|
|
141
|
+
profile = store.vault.hydrateProfile(profile);
|
|
142
|
+
}
|
|
123
143
|
if (!profile || !profile.host) {
|
|
124
144
|
return writeJson(res, 400, { ok: false, error: 'Invalid profile configuration' });
|
|
125
145
|
}
|
|
@@ -152,7 +172,7 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
152
172
|
return writeJson(res, 400, { ok: false, error: 'No profile specified' });
|
|
153
173
|
}
|
|
154
174
|
const remotePath = body.remotePath || '/';
|
|
155
|
-
const entries = await remoteFs.listDir(profile, remotePath);
|
|
175
|
+
const entries = await remoteFs.listDir(profile, remotePath, Boolean(body.refresh));
|
|
156
176
|
writeJson(res, 200, { ok: true, currentPath: remotePath, entries });
|
|
157
177
|
} catch (err) {
|
|
158
178
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
@@ -160,7 +180,143 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
160
180
|
}
|
|
161
181
|
}), 'dsh-remote-workspace: /browse');
|
|
162
182
|
|
|
163
|
-
// 7. POST /dsh-remote-workspace/
|
|
183
|
+
// 7. POST /dsh-remote-workspace/file/view
|
|
184
|
+
sctx.effect(() => webServer.register({
|
|
185
|
+
kind: 'exact',
|
|
186
|
+
path: '/dsh-remote-workspace/file/view',
|
|
187
|
+
handler: async (req, res) => {
|
|
188
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
189
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
190
|
+
try {
|
|
191
|
+
const { profileId, filePath } = await readBody(req);
|
|
192
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
193
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
194
|
+
if (!filePath) return writeJson(res, 400, { ok: false, error: 'filePath required' });
|
|
195
|
+
|
|
196
|
+
const stat = await remoteFs.stat(profile, filePath);
|
|
197
|
+
if (stat.size > 1024 * 512) {
|
|
198
|
+
return writeJson(res, 400, { ok: false, error: `File too large for preview (${Math.round(stat.size / 1024)} KB). Max 512 KB.` });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const content = await remoteFs.readFile(profile, filePath, 'utf8');
|
|
202
|
+
writeJson(res, 200, { ok: true, filePath, content, size: stat.size, mtime: stat.mtime });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}), 'dsh-remote-workspace: /file/view');
|
|
208
|
+
|
|
209
|
+
// 8. POST /dsh-remote-workspace/file/save
|
|
210
|
+
sctx.effect(() => webServer.register({
|
|
211
|
+
kind: 'exact',
|
|
212
|
+
path: '/dsh-remote-workspace/file/save',
|
|
213
|
+
handler: async (req, res) => {
|
|
214
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
215
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
216
|
+
try {
|
|
217
|
+
const { profileId, filePath, content } = await readBody(req);
|
|
218
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
219
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
220
|
+
if (!filePath || content === undefined) return writeJson(res, 400, { ok: false, error: 'filePath and content required' });
|
|
221
|
+
|
|
222
|
+
const result = await remoteFs.writeFile(profile, filePath, content, 'utf8');
|
|
223
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
224
|
+
} catch (err) {
|
|
225
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}), 'dsh-remote-workspace: /file/save');
|
|
229
|
+
|
|
230
|
+
// 9. POST /dsh-remote-workspace/terminal/create
|
|
231
|
+
sctx.effect(() => webServer.register({
|
|
232
|
+
kind: 'exact',
|
|
233
|
+
path: '/dsh-remote-workspace/terminal/create',
|
|
234
|
+
handler: async (req, res) => {
|
|
235
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
236
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
237
|
+
try {
|
|
238
|
+
const { profileId, cols, rows } = await readBody(req);
|
|
239
|
+
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
240
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
241
|
+
|
|
242
|
+
const session = await sshService.createTerminalSession(profile, { cols, rows });
|
|
243
|
+
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}), 'dsh-remote-workspace: /terminal/create');
|
|
249
|
+
|
|
250
|
+
// 10. POST /dsh-remote-workspace/terminal/input
|
|
251
|
+
sctx.effect(() => webServer.register({
|
|
252
|
+
kind: 'exact',
|
|
253
|
+
path: '/dsh-remote-workspace/terminal/input',
|
|
254
|
+
handler: async (req, res) => {
|
|
255
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
256
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
257
|
+
try {
|
|
258
|
+
const { sessionId, data } = await readBody(req);
|
|
259
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
260
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
261
|
+
session.write(data || '');
|
|
262
|
+
writeJson(res, 200, { ok: true });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}), 'dsh-remote-workspace: /terminal/input');
|
|
268
|
+
|
|
269
|
+
// 11. GET /dsh-remote-workspace/terminal/stream (SSE)
|
|
270
|
+
sctx.effect(() => webServer.register({
|
|
271
|
+
kind: 'exact',
|
|
272
|
+
path: '/dsh-remote-workspace/terminal/stream',
|
|
273
|
+
handler: async (req, res) => {
|
|
274
|
+
const url = new URL(req.url, 'http://localhost');
|
|
275
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
276
|
+
const session = sshService.getTerminalSession(sessionId);
|
|
277
|
+
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
278
|
+
|
|
279
|
+
if (typeof res.writeHead === 'function') {
|
|
280
|
+
res.writeHead(200, {
|
|
281
|
+
'Content-Type': 'text/event-stream',
|
|
282
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
283
|
+
'Connection': 'keep-alive'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const chunk of session.buffer) {
|
|
288
|
+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const listener = (chunk) => {
|
|
292
|
+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
293
|
+
};
|
|
294
|
+
session.subscribers.add(listener);
|
|
295
|
+
|
|
296
|
+
req.on('close', () => {
|
|
297
|
+
session.subscribers.delete(listener);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}), 'dsh-remote-workspace: /terminal/stream');
|
|
301
|
+
|
|
302
|
+
// 12. POST /dsh-remote-workspace/terminal/close
|
|
303
|
+
sctx.effect(() => webServer.register({
|
|
304
|
+
kind: 'exact',
|
|
305
|
+
path: '/dsh-remote-workspace/terminal/close',
|
|
306
|
+
handler: async (req, res) => {
|
|
307
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
308
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
309
|
+
try {
|
|
310
|
+
const { sessionId } = await readBody(req);
|
|
311
|
+
const ok = sshService.closeTerminalSession(sessionId);
|
|
312
|
+
writeJson(res, 200, { ok });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}), 'dsh-remote-workspace: /terminal/close');
|
|
318
|
+
|
|
319
|
+
// 13. POST /dsh-remote-workspace/sync
|
|
164
320
|
sctx.effect(() => webServer.register({
|
|
165
321
|
kind: 'exact',
|
|
166
322
|
path: '/dsh-remote-workspace/sync',
|
|
@@ -168,14 +324,14 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
168
324
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
169
325
|
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
170
326
|
try {
|
|
171
|
-
const { profileId, direction } = await readBody(req);
|
|
327
|
+
const { profileId, direction, force } = await readBody(req);
|
|
172
328
|
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
173
329
|
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
174
330
|
if (!profile.remoteWorkspace || !profile.localMirrorPath) {
|
|
175
331
|
return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be configured' });
|
|
176
332
|
}
|
|
177
333
|
const resSync = direction === 'push'
|
|
178
|
-
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace)
|
|
334
|
+
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, Boolean(force))
|
|
179
335
|
: await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
180
336
|
writeJson(res, 200, { ok: resSync.success, ...resSync });
|
|
181
337
|
} catch (err) {
|
|
@@ -184,7 +340,7 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
184
340
|
}
|
|
185
341
|
}), 'dsh-remote-workspace: /sync');
|
|
186
342
|
|
|
187
|
-
//
|
|
343
|
+
// 14. POST /dsh-remote-workspace/tunnels/start
|
|
188
344
|
sctx.effect(() => webServer.register({
|
|
189
345
|
kind: 'exact',
|
|
190
346
|
path: '/dsh-remote-workspace/tunnels/start',
|
|
@@ -208,7 +364,7 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
208
364
|
}
|
|
209
365
|
}), 'dsh-remote-workspace: /tunnels/start');
|
|
210
366
|
|
|
211
|
-
//
|
|
367
|
+
// 15. POST /dsh-remote-workspace/tunnels/stop
|
|
212
368
|
sctx.effect(() => webServer.register({
|
|
213
369
|
kind: 'exact',
|
|
214
370
|
path: '/dsh-remote-workspace/tunnels/stop',
|
|
@@ -225,5 +381,142 @@ export function registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelS
|
|
|
225
381
|
}
|
|
226
382
|
}
|
|
227
383
|
}), 'dsh-remote-workspace: /tunnels/stop');
|
|
384
|
+
|
|
385
|
+
// 16. GET /dsh-remote-workspace/tunnels/telemetry
|
|
386
|
+
sctx.effect(() => webServer.register({
|
|
387
|
+
kind: 'exact',
|
|
388
|
+
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
389
|
+
handler: async (req, res) => {
|
|
390
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
391
|
+
try {
|
|
392
|
+
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
393
|
+
} catch (err) {
|
|
394
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}), 'dsh-remote-workspace: /tunnels/telemetry');
|
|
398
|
+
|
|
399
|
+
// 17. GET /dsh-remote-workspace/health
|
|
400
|
+
sctx.effect(() => webServer.register({
|
|
401
|
+
kind: 'exact',
|
|
402
|
+
path: '/dsh-remote-workspace/health',
|
|
403
|
+
handler: async (req, res) => {
|
|
404
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
405
|
+
try {
|
|
406
|
+
const url = new URL(req.url, 'http://localhost');
|
|
407
|
+
const profileId = url.searchParams.get('profileId');
|
|
408
|
+
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
409
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
410
|
+
|
|
411
|
+
const health = await healthService.getHealth(profile);
|
|
412
|
+
writeJson(res, 200, { ok: true, health });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}), 'dsh-remote-workspace: /health');
|
|
418
|
+
|
|
419
|
+
// 18. GET /dsh-remote-workspace/docker/list
|
|
420
|
+
sctx.effect(() => webServer.register({
|
|
421
|
+
kind: 'exact',
|
|
422
|
+
path: '/dsh-remote-workspace/docker/list',
|
|
423
|
+
handler: async (req, res) => {
|
|
424
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
425
|
+
try {
|
|
426
|
+
const url = new URL(req.url, 'http://localhost');
|
|
427
|
+
const profileId = url.searchParams.get('profileId');
|
|
428
|
+
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
429
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
430
|
+
|
|
431
|
+
const result = await dockerService.listContainers(profile, true);
|
|
432
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}), 'dsh-remote-workspace: /docker/list');
|
|
438
|
+
|
|
439
|
+
// 19. POST /dsh-remote-workspace/docker/action
|
|
440
|
+
sctx.effect(() => webServer.register({
|
|
441
|
+
kind: 'exact',
|
|
442
|
+
path: '/dsh-remote-workspace/docker/action',
|
|
443
|
+
handler: async (req, res) => {
|
|
444
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
445
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
446
|
+
try {
|
|
447
|
+
const { profileId, containerId, action } = await readBody(req);
|
|
448
|
+
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
449
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
450
|
+
if (!containerId || !action) return writeJson(res, 400, { ok: false, error: 'containerId and action required' });
|
|
451
|
+
|
|
452
|
+
const result = await dockerService.containerAction(profile, containerId, action);
|
|
453
|
+
writeJson(res, 200, { ok: result.success, ...result });
|
|
454
|
+
} catch (err) {
|
|
455
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}), 'dsh-remote-workspace: /docker/action');
|
|
459
|
+
|
|
460
|
+
// 20. POST /dsh-remote-workspace/docker/logs
|
|
461
|
+
sctx.effect(() => webServer.register({
|
|
462
|
+
kind: 'exact',
|
|
463
|
+
path: '/dsh-remote-workspace/docker/logs',
|
|
464
|
+
handler: async (req, res) => {
|
|
465
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
466
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
467
|
+
try {
|
|
468
|
+
const { profileId, containerId, tail } = await readBody(req);
|
|
469
|
+
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
470
|
+
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
471
|
+
if (!containerId) return writeJson(res, 400, { ok: false, error: 'containerId required' });
|
|
472
|
+
|
|
473
|
+
const result = await dockerService.containerLogs(profile, containerId, tail || 100);
|
|
474
|
+
writeJson(res, 200, { ok: result.success, ...result });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}), 'dsh-remote-workspace: /docker/logs');
|
|
480
|
+
|
|
481
|
+
// 21. POST /dsh-remote-workspace/watcher/toggle
|
|
482
|
+
sctx.effect(() => webServer.register({
|
|
483
|
+
kind: 'exact',
|
|
484
|
+
path: '/dsh-remote-workspace/watcher/toggle',
|
|
485
|
+
handler: async (req, res) => {
|
|
486
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
487
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
488
|
+
try {
|
|
489
|
+
const { enabled } = await readBody(req);
|
|
490
|
+
await store.setAutoSyncEnabled(Boolean(enabled));
|
|
491
|
+
writeJson(res, 200, { ok: true, autoSync: store.isAutoSyncEnabled() });
|
|
492
|
+
} catch (err) {
|
|
493
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}), 'dsh-remote-workspace: /watcher/toggle');
|
|
497
|
+
|
|
498
|
+
// 22. POST /dsh-remote-workspace/transfer
|
|
499
|
+
sctx.effect(() => webServer.register({
|
|
500
|
+
kind: 'exact',
|
|
501
|
+
path: '/dsh-remote-workspace/transfer',
|
|
502
|
+
handler: async (req, res) => {
|
|
503
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
504
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
505
|
+
try {
|
|
506
|
+
const { sourceProfileId, sourcePath, destProfileId, destPath } = await readBody(req);
|
|
507
|
+
const srcProfile = sourceProfileId ? store.getProfile(sourceProfileId) : store.getActiveProfile();
|
|
508
|
+
const destProfile = store.getProfile(destProfileId);
|
|
509
|
+
|
|
510
|
+
if (!srcProfile) return writeJson(res, 404, { ok: false, error: 'Source profile not found' });
|
|
511
|
+
if (!destProfile) return writeJson(res, 404, { ok: false, error: 'Destination profile not found' });
|
|
512
|
+
if (!sourcePath || !destPath) return writeJson(res, 400, { ok: false, error: 'sourcePath and destPath required' });
|
|
513
|
+
|
|
514
|
+
const result = await sshService.transferFileBetweenHosts(srcProfile, sourcePath, destProfile, destPath);
|
|
515
|
+
writeJson(res, 200, { ok: result.success, ...result });
|
|
516
|
+
} catch (err) {
|
|
517
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}), 'dsh-remote-workspace: /transfer');
|
|
228
521
|
});
|
|
229
522
|
}
|