@goodandready/dsh-remote-workspace 0.3.1 → 0.3.3
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/docs/design/DESIGN.md +15 -1
- package/lib/client.js +2322 -2179
- package/lib/docker-service.js +1 -1
- package/lib/index.js +42 -12
- package/lib/mirror-sync-service.js +2 -2
- package/lib/routes/docker.js +85 -0
- package/lib/routes/env.js +60 -0
- package/lib/routes/files.js +88 -0
- package/lib/routes/helpers.js +40 -0
- package/lib/routes/profiles.js +141 -0
- package/lib/routes/sync.js +52 -0
- package/lib/routes/terminal.js +103 -0
- package/lib/routes/tunnels.js +59 -0
- package/lib/routes.js +53 -627
- package/lib/ssh-service.js +24 -9
- package/lib/tunnel-service.js +2 -2
- package/lib/updater-service.js +339 -0
- package/lib/vault-service.js +29 -5
- package/lib/watcher-service.js +3 -3
- package/package.json +9 -4
- package/docs/plans/findings.md +0 -19
- package/docs/plans/progress.md +0 -15
- package/docs/plans/task_plan.md +0 -21
package/lib/routes.js
CHANGED
|
@@ -1,627 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
sctx
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
55
|
-
try {
|
|
56
|
-
const profiles = store.getProfiles();
|
|
57
|
-
const activeId = store.getActiveId() || (profiles[0]?.id || '');
|
|
58
|
-
const tunnels = tunnelService.listActiveTunnels();
|
|
59
|
-
const autoSync = store.isAutoSyncEnabled ? store.isAutoSyncEnabled() : false;
|
|
60
|
-
const alerts = alertService ? alertService.getAlerts() : [];
|
|
61
|
-
writeJson(res, 200, {
|
|
62
|
-
ok: true,
|
|
63
|
-
profiles,
|
|
64
|
-
activeId,
|
|
65
|
-
tunnels,
|
|
66
|
-
autoSync,
|
|
67
|
-
alerts,
|
|
68
|
-
vaultProtected: true
|
|
69
|
-
});
|
|
70
|
-
} catch (err) {
|
|
71
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}), 'dsh-remote-workspace: /state');
|
|
75
|
-
|
|
76
|
-
// 2. POST /dsh-remote-workspace/profiles/save
|
|
77
|
-
sctx.effect(() => webServer.register({
|
|
78
|
-
kind: 'exact',
|
|
79
|
-
path: '/dsh-remote-workspace/profiles/save',
|
|
80
|
-
handler: async (req, res) => {
|
|
81
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
82
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
83
|
-
try {
|
|
84
|
-
const profile = await readBody(req);
|
|
85
|
-
if (!profile.id || !profile.host) {
|
|
86
|
-
return writeJson(res, 400, { ok: false, error: 'ID and Host are required' });
|
|
87
|
-
}
|
|
88
|
-
sshService.invalidate(profile.id);
|
|
89
|
-
await store.saveProfile(profile);
|
|
90
|
-
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
|
|
91
|
-
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
92
|
-
} catch (err) {
|
|
93
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}), 'dsh-remote-workspace: /profiles/save');
|
|
97
|
-
|
|
98
|
-
// 3. POST /dsh-remote-workspace/profiles/delete
|
|
99
|
-
sctx.effect(() => webServer.register({
|
|
100
|
-
kind: 'exact',
|
|
101
|
-
path: '/dsh-remote-workspace/profiles/delete',
|
|
102
|
-
handler: async (req, res) => {
|
|
103
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
104
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
105
|
-
try {
|
|
106
|
-
const { id } = await readBody(req);
|
|
107
|
-
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
108
|
-
sshService.disconnect(id);
|
|
109
|
-
await store.deleteProfile(id);
|
|
110
|
-
writeJson(res, 200, { ok: true });
|
|
111
|
-
} catch (err) {
|
|
112
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}), 'dsh-remote-workspace: /profiles/delete');
|
|
116
|
-
|
|
117
|
-
// 4. POST /dsh-remote-workspace/profiles/active
|
|
118
|
-
sctx.effect(() => webServer.register({
|
|
119
|
-
kind: 'exact',
|
|
120
|
-
path: '/dsh-remote-workspace/profiles/active',
|
|
121
|
-
handler: async (req, res) => {
|
|
122
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
123
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
124
|
-
try {
|
|
125
|
-
const { id } = await readBody(req);
|
|
126
|
-
await store.setActiveId(id || null);
|
|
127
|
-
writeJson(res, 200, { ok: true, activeId: store.getActiveId() });
|
|
128
|
-
} catch (err) {
|
|
129
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}), 'dsh-remote-workspace: /profiles/active');
|
|
133
|
-
|
|
134
|
-
// 5. POST /dsh-remote-workspace/profiles/import-ssh-config (New in v0.3.1)
|
|
135
|
-
sctx.effect(() => webServer.register({
|
|
136
|
-
kind: 'exact',
|
|
137
|
-
path: '/dsh-remote-workspace/profiles/import-ssh-config',
|
|
138
|
-
handler: async (req, res) => {
|
|
139
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
140
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
141
|
-
try {
|
|
142
|
-
const { text } = await readBody(req);
|
|
143
|
-
const rawText = text || SshConfigParser.readDefaultConfig();
|
|
144
|
-
if (!rawText) {
|
|
145
|
-
return writeJson(res, 400, { ok: false, error: 'No SSH config text provided and ~/.ssh/config not found' });
|
|
146
|
-
}
|
|
147
|
-
const imported = SshConfigParser.parse(rawText);
|
|
148
|
-
const saved = [];
|
|
149
|
-
for (const p of imported) {
|
|
150
|
-
await store.saveProfile(p);
|
|
151
|
-
saved.push(store.vault ? store.vault.sanitizeProfile(p) : p);
|
|
152
|
-
}
|
|
153
|
-
writeJson(res, 200, { ok: true, count: saved.length, profiles: saved });
|
|
154
|
-
} catch (err) {
|
|
155
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}), 'dsh-remote-workspace: /profiles/import-ssh-config');
|
|
159
|
-
|
|
160
|
-
// 6. POST /dsh-remote-workspace/test
|
|
161
|
-
sctx.effect(() => webServer.register({
|
|
162
|
-
kind: 'exact',
|
|
163
|
-
path: '/dsh-remote-workspace/test',
|
|
164
|
-
handler: async (req, res) => {
|
|
165
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
166
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
167
|
-
try {
|
|
168
|
-
const body = await readBody(req);
|
|
169
|
-
let profile = body.profile || (body.id ? store.getProfile(body.id) : body);
|
|
170
|
-
if (store.vault && profile) {
|
|
171
|
-
profile = store.vault.hydrateProfile(profile);
|
|
172
|
-
}
|
|
173
|
-
if (!profile || !profile.host) {
|
|
174
|
-
return writeJson(res, 400, { ok: false, error: 'Invalid profile configuration' });
|
|
175
|
-
}
|
|
176
|
-
const result = await sshService.testConnection(profile);
|
|
177
|
-
writeJson(res, 200, {
|
|
178
|
-
ok: result.success,
|
|
179
|
-
success: result.success,
|
|
180
|
-
latency: result.latency,
|
|
181
|
-
latencyMs: result.latency,
|
|
182
|
-
os: result.os,
|
|
183
|
-
remoteOs: result.os
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
writeJson(res, 200, { ok: false, error: err.message });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}), 'dsh-remote-workspace: /test');
|
|
190
|
-
|
|
191
|
-
// 7. POST /dsh-remote-workspace/browse
|
|
192
|
-
sctx.effect(() => webServer.register({
|
|
193
|
-
kind: 'exact',
|
|
194
|
-
path: '/dsh-remote-workspace/browse',
|
|
195
|
-
handler: async (req, res) => {
|
|
196
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
197
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
198
|
-
try {
|
|
199
|
-
const body = await readBody(req);
|
|
200
|
-
const profile = body.profile || (body.profileId ? store.getProfile(body.profileId) : store.getActiveProfile());
|
|
201
|
-
if (!profile || !profile.host) {
|
|
202
|
-
return writeJson(res, 400, { ok: false, error: 'No profile specified' });
|
|
203
|
-
}
|
|
204
|
-
const remotePath = body.remotePath || '/';
|
|
205
|
-
const entries = await remoteFs.listDir(profile, remotePath, Boolean(body.refresh));
|
|
206
|
-
writeJson(res, 200, { ok: true, currentPath: remotePath, entries });
|
|
207
|
-
} catch (err) {
|
|
208
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}), 'dsh-remote-workspace: /browse');
|
|
212
|
-
|
|
213
|
-
// 8. POST /dsh-remote-workspace/file/view
|
|
214
|
-
sctx.effect(() => webServer.register({
|
|
215
|
-
kind: 'exact',
|
|
216
|
-
path: '/dsh-remote-workspace/file/view',
|
|
217
|
-
handler: async (req, res) => {
|
|
218
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
219
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
220
|
-
try {
|
|
221
|
-
const { profileId, filePath } = await readBody(req);
|
|
222
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
223
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
224
|
-
if (!filePath) return writeJson(res, 400, { ok: false, error: 'filePath required' });
|
|
225
|
-
|
|
226
|
-
const stat = await remoteFs.stat(profile, filePath);
|
|
227
|
-
if (stat.size > 1024 * 512) {
|
|
228
|
-
return writeJson(res, 400, { ok: false, error: `File too large for preview (${Math.round(stat.size / 1024)} KB). Max 512 KB.` });
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const content = await remoteFs.readFile(profile, filePath, 'utf8');
|
|
232
|
-
writeJson(res, 200, { ok: true, filePath, content, size: stat.size, mtime: stat.mtime });
|
|
233
|
-
} catch (err) {
|
|
234
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}), 'dsh-remote-workspace: /file/view');
|
|
238
|
-
|
|
239
|
-
// 9. POST /dsh-remote-workspace/file/save
|
|
240
|
-
sctx.effect(() => webServer.register({
|
|
241
|
-
kind: 'exact',
|
|
242
|
-
path: '/dsh-remote-workspace/file/save',
|
|
243
|
-
handler: async (req, res) => {
|
|
244
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
245
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
246
|
-
try {
|
|
247
|
-
const { profileId, filePath, content } = await readBody(req);
|
|
248
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
249
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
250
|
-
if (!filePath || content === undefined) return writeJson(res, 400, { ok: false, error: 'filePath and content required' });
|
|
251
|
-
|
|
252
|
-
const result = await remoteFs.writeFile(profile, filePath, content, 'utf8');
|
|
253
|
-
writeJson(res, 200, { ok: true, ...result });
|
|
254
|
-
} catch (err) {
|
|
255
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}), 'dsh-remote-workspace: /file/save');
|
|
259
|
-
|
|
260
|
-
// 10. POST /dsh-remote-workspace/env/view (New in v0.3.1)
|
|
261
|
-
sctx.effect(() => webServer.register({
|
|
262
|
-
kind: 'exact',
|
|
263
|
-
path: '/dsh-remote-workspace/env/view',
|
|
264
|
-
handler: async (req, res) => {
|
|
265
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
266
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
267
|
-
try {
|
|
268
|
-
const { profileId, filePath } = await readBody(req);
|
|
269
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
270
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
271
|
-
if (!envService) return writeJson(res, 500, { ok: false, error: 'EnvService unavailable' });
|
|
272
|
-
|
|
273
|
-
const target = filePath || (profile.remoteWorkspace ? `${profile.remoteWorkspace}/.env` : '.env');
|
|
274
|
-
const data = await envService.readEnv(profile, target);
|
|
275
|
-
writeJson(res, 200, { ok: true, ...data });
|
|
276
|
-
} catch (err) {
|
|
277
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}), 'dsh-remote-workspace: /env/view');
|
|
281
|
-
|
|
282
|
-
// 11. POST /dsh-remote-workspace/env/save (New in v0.3.1)
|
|
283
|
-
sctx.effect(() => webServer.register({
|
|
284
|
-
kind: 'exact',
|
|
285
|
-
path: '/dsh-remote-workspace/env/save',
|
|
286
|
-
handler: async (req, res) => {
|
|
287
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
288
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
289
|
-
try {
|
|
290
|
-
const { profileId, filePath, key, value } = await readBody(req);
|
|
291
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
292
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
293
|
-
if (!key) return writeJson(res, 400, { ok: false, error: 'key is required' });
|
|
294
|
-
if (!envService) return writeJson(res, 500, { ok: false, error: 'EnvService unavailable' });
|
|
295
|
-
|
|
296
|
-
const target = filePath || (profile.remoteWorkspace ? `${profile.remoteWorkspace}/.env` : '.env');
|
|
297
|
-
const result = await envService.setEnvVar(profile, target, key, value || '');
|
|
298
|
-
writeJson(res, 200, { ok: true, ...result });
|
|
299
|
-
} catch (err) {
|
|
300
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}), 'dsh-remote-workspace: /env/save');
|
|
304
|
-
|
|
305
|
-
// 12. POST /dsh-remote-workspace/diagnose (New in v0.3.1)
|
|
306
|
-
sctx.effect(() => webServer.register({
|
|
307
|
-
kind: 'exact',
|
|
308
|
-
path: '/dsh-remote-workspace/diagnose',
|
|
309
|
-
handler: async (req, res) => {
|
|
310
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
311
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
312
|
-
try {
|
|
313
|
-
const { profileId, category, target } = await readBody(req);
|
|
314
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
315
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
316
|
-
if (!category) return writeJson(res, 400, { ok: false, error: 'category is required' });
|
|
317
|
-
if (!diagnoseService) return writeJson(res, 500, { ok: false, error: 'DiagnoseService unavailable' });
|
|
318
|
-
|
|
319
|
-
const result = await diagnoseService.diagnose(profile, category, target);
|
|
320
|
-
writeJson(res, 200, { ok: true, ...result });
|
|
321
|
-
} catch (err) {
|
|
322
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}), 'dsh-remote-workspace: /diagnose');
|
|
326
|
-
|
|
327
|
-
// 13. POST /dsh-remote-workspace/terminal/create
|
|
328
|
-
sctx.effect(() => webServer.register({
|
|
329
|
-
kind: 'exact',
|
|
330
|
-
path: '/dsh-remote-workspace/terminal/create',
|
|
331
|
-
handler: async (req, res) => {
|
|
332
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
333
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
334
|
-
try {
|
|
335
|
-
const { profileId, cols, rows } = await readBody(req);
|
|
336
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
337
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
338
|
-
|
|
339
|
-
const session = await sshService.createTerminalSession(profile, { cols, rows });
|
|
340
|
-
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
341
|
-
} catch (err) {
|
|
342
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}), 'dsh-remote-workspace: /terminal/create');
|
|
346
|
-
|
|
347
|
-
// 14. POST /dsh-remote-workspace/terminal/input
|
|
348
|
-
sctx.effect(() => webServer.register({
|
|
349
|
-
kind: 'exact',
|
|
350
|
-
path: '/dsh-remote-workspace/terminal/input',
|
|
351
|
-
handler: async (req, res) => {
|
|
352
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
353
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
354
|
-
try {
|
|
355
|
-
const { sessionId, data } = await readBody(req);
|
|
356
|
-
const session = sshService.getTerminalSession(sessionId);
|
|
357
|
-
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
358
|
-
session.write(data || '');
|
|
359
|
-
writeJson(res, 200, { ok: true });
|
|
360
|
-
} catch (err) {
|
|
361
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}), 'dsh-remote-workspace: /terminal/input');
|
|
365
|
-
|
|
366
|
-
// 15. GET /dsh-remote-workspace/terminal/stream (SSE)
|
|
367
|
-
sctx.effect(() => webServer.register({
|
|
368
|
-
kind: 'exact',
|
|
369
|
-
path: '/dsh-remote-workspace/terminal/stream',
|
|
370
|
-
handler: async (req, res) => {
|
|
371
|
-
const url = new URL(req.url, 'http://localhost');
|
|
372
|
-
const sessionId = url.searchParams.get('sessionId');
|
|
373
|
-
const session = sshService.getTerminalSession(sessionId);
|
|
374
|
-
if (!session) return writeJson(res, 404, { ok: false, error: 'Terminal session not found' });
|
|
375
|
-
|
|
376
|
-
if (typeof res.writeHead === 'function') {
|
|
377
|
-
res.writeHead(200, {
|
|
378
|
-
'Content-Type': 'text/event-stream',
|
|
379
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
380
|
-
'Connection': 'keep-alive'
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const chunk of session.buffer) {
|
|
385
|
-
res.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const listener = (chunk) => {
|
|
389
|
-
res.write(`data: ${JSON.stringify({ chunk })}\\n\\n`);
|
|
390
|
-
};
|
|
391
|
-
session.subscribers.add(listener);
|
|
392
|
-
|
|
393
|
-
req.on('close', () => {
|
|
394
|
-
session.subscribers.delete(listener);
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}), 'dsh-remote-workspace: /terminal/stream');
|
|
398
|
-
|
|
399
|
-
// 16. POST /dsh-remote-workspace/terminal/close
|
|
400
|
-
sctx.effect(() => webServer.register({
|
|
401
|
-
kind: 'exact',
|
|
402
|
-
path: '/dsh-remote-workspace/terminal/close',
|
|
403
|
-
handler: async (req, res) => {
|
|
404
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
405
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
406
|
-
try {
|
|
407
|
-
const { sessionId } = await readBody(req);
|
|
408
|
-
const ok = sshService.closeTerminalSession(sessionId);
|
|
409
|
-
writeJson(res, 200, { ok });
|
|
410
|
-
} catch (err) {
|
|
411
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}), 'dsh-remote-workspace: /terminal/close');
|
|
415
|
-
|
|
416
|
-
// 17. POST /dsh-remote-workspace/sync
|
|
417
|
-
sctx.effect(() => webServer.register({
|
|
418
|
-
kind: 'exact',
|
|
419
|
-
path: '/dsh-remote-workspace/sync',
|
|
420
|
-
handler: async (req, res) => {
|
|
421
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
422
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
423
|
-
try {
|
|
424
|
-
const { profileId, direction, force, mode } = await readBody(req);
|
|
425
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
426
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
427
|
-
if (!profile.remoteWorkspace || !profile.localMirrorPath) {
|
|
428
|
-
return writeJson(res, 400, { ok: false, error: 'Both remoteWorkspace and localMirrorPath must be configured' });
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (mode === 'tarball' && tarSyncService) {
|
|
432
|
-
const resTar = direction === 'push'
|
|
433
|
-
? await tarSyncService.pushTar(profile, profile.localMirrorPath, profile.remoteWorkspace)
|
|
434
|
-
: await tarSyncService.pullTar(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
435
|
-
return writeJson(res, 200, { ok: true, ...resTar });
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const resSync = direction === 'push'
|
|
439
|
-
? await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, Boolean(force))
|
|
440
|
-
: await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
441
|
-
writeJson(res, 200, { ok: resSync.success, ...resSync });
|
|
442
|
-
} catch (err) {
|
|
443
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}), 'dsh-remote-workspace: /sync');
|
|
447
|
-
|
|
448
|
-
// 18. POST /dsh-remote-workspace/tunnels/start
|
|
449
|
-
sctx.effect(() => webServer.register({
|
|
450
|
-
kind: 'exact',
|
|
451
|
-
path: '/dsh-remote-workspace/tunnels/start',
|
|
452
|
-
handler: async (req, res) => {
|
|
453
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
454
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
455
|
-
try {
|
|
456
|
-
const { profileId, localPort, targetHost, remotePort } = await readBody(req);
|
|
457
|
-
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
458
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
459
|
-
const lPort = parseInt(localPort, 10);
|
|
460
|
-
const rPort = parseInt(remotePort, 10);
|
|
461
|
-
if (!lPort || !rPort) {
|
|
462
|
-
return writeJson(res, 400, { ok: false, error: 'Valid localPort and remotePort required' });
|
|
463
|
-
}
|
|
464
|
-
const result = await tunnelService.startLocalTunnel(profile, lPort, targetHost || '127.0.0.1', rPort);
|
|
465
|
-
writeJson(res, 200, { ok: result.success, ...result });
|
|
466
|
-
} catch (err) {
|
|
467
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}), 'dsh-remote-workspace: /tunnels/start');
|
|
471
|
-
|
|
472
|
-
// 19. POST /dsh-remote-workspace/tunnels/stop
|
|
473
|
-
sctx.effect(() => webServer.register({
|
|
474
|
-
kind: 'exact',
|
|
475
|
-
path: '/dsh-remote-workspace/tunnels/stop',
|
|
476
|
-
handler: async (req, res) => {
|
|
477
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
478
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
479
|
-
try {
|
|
480
|
-
const { tunnelId } = await readBody(req);
|
|
481
|
-
if (!tunnelId) return writeJson(res, 400, { ok: false, error: 'tunnelId required' });
|
|
482
|
-
const stopped = tunnelService.stopTunnel(tunnelId);
|
|
483
|
-
writeJson(res, 200, { ok: stopped });
|
|
484
|
-
} catch (err) {
|
|
485
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}), 'dsh-remote-workspace: /tunnels/stop');
|
|
489
|
-
|
|
490
|
-
// 20. GET /dsh-remote-workspace/tunnels/telemetry
|
|
491
|
-
sctx.effect(() => webServer.register({
|
|
492
|
-
kind: 'exact',
|
|
493
|
-
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
494
|
-
handler: async (req, res) => {
|
|
495
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
496
|
-
try {
|
|
497
|
-
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
498
|
-
} catch (err) {
|
|
499
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}), 'dsh-remote-workspace: /tunnels/telemetry');
|
|
503
|
-
|
|
504
|
-
// 21. GET /dsh-remote-workspace/health
|
|
505
|
-
sctx.effect(() => webServer.register({
|
|
506
|
-
kind: 'exact',
|
|
507
|
-
path: '/dsh-remote-workspace/health',
|
|
508
|
-
handler: async (req, res) => {
|
|
509
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
510
|
-
try {
|
|
511
|
-
const url = new URL(req.url, 'http://localhost');
|
|
512
|
-
const profileId = url.searchParams.get('profileId');
|
|
513
|
-
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
514
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
515
|
-
|
|
516
|
-
const health = await healthService.getHealth(profile);
|
|
517
|
-
writeJson(res, 200, { ok: true, health });
|
|
518
|
-
} catch (err) {
|
|
519
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}), 'dsh-remote-workspace: /health');
|
|
523
|
-
|
|
524
|
-
// 22. GET /dsh-remote-workspace/docker/list
|
|
525
|
-
sctx.effect(() => webServer.register({
|
|
526
|
-
kind: 'exact',
|
|
527
|
-
path: '/dsh-remote-workspace/docker/list',
|
|
528
|
-
handler: async (req, res) => {
|
|
529
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
530
|
-
try {
|
|
531
|
-
const url = new URL(req.url, 'http://localhost');
|
|
532
|
-
const profileId = url.searchParams.get('profileId');
|
|
533
|
-
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
534
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
535
|
-
|
|
536
|
-
const result = await dockerService.listContainers(profile, true);
|
|
537
|
-
writeJson(res, 200, { ok: true, ...result });
|
|
538
|
-
} catch (err) {
|
|
539
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}), 'dsh-remote-workspace: /docker/list');
|
|
543
|
-
|
|
544
|
-
// 23. POST /dsh-remote-workspace/docker/action
|
|
545
|
-
sctx.effect(() => webServer.register({
|
|
546
|
-
kind: 'exact',
|
|
547
|
-
path: '/dsh-remote-workspace/docker/action',
|
|
548
|
-
handler: async (req, res) => {
|
|
549
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
550
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
551
|
-
try {
|
|
552
|
-
const { profileId, containerId, action } = await readBody(req);
|
|
553
|
-
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
554
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
555
|
-
if (!containerId || !action) return writeJson(res, 400, { ok: false, error: 'containerId and action required' });
|
|
556
|
-
|
|
557
|
-
const result = await dockerService.containerAction(profile, containerId, action);
|
|
558
|
-
writeJson(res, 200, { ok: result.success, ...result });
|
|
559
|
-
} catch (err) {
|
|
560
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}), 'dsh-remote-workspace: /docker/action');
|
|
564
|
-
|
|
565
|
-
// 24. POST /dsh-remote-workspace/docker/logs
|
|
566
|
-
sctx.effect(() => webServer.register({
|
|
567
|
-
kind: 'exact',
|
|
568
|
-
path: '/dsh-remote-workspace/docker/logs',
|
|
569
|
-
handler: async (req, res) => {
|
|
570
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
571
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
572
|
-
try {
|
|
573
|
-
const { profileId, containerId, tail } = await readBody(req);
|
|
574
|
-
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
575
|
-
if (!profile) return writeJson(res, 404, { ok: false, error: 'No active profile' });
|
|
576
|
-
if (!containerId) return writeJson(res, 400, { ok: false, error: 'containerId required' });
|
|
577
|
-
|
|
578
|
-
const result = await dockerService.containerLogs(profile, containerId, tail || 100);
|
|
579
|
-
writeJson(res, 200, { ok: result.success, ...result });
|
|
580
|
-
} catch (err) {
|
|
581
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}), 'dsh-remote-workspace: /docker/logs');
|
|
585
|
-
|
|
586
|
-
// 25. POST /dsh-remote-workspace/watcher/toggle
|
|
587
|
-
sctx.effect(() => webServer.register({
|
|
588
|
-
kind: 'exact',
|
|
589
|
-
path: '/dsh-remote-workspace/watcher/toggle',
|
|
590
|
-
handler: async (req, res) => {
|
|
591
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
592
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
593
|
-
try {
|
|
594
|
-
const { enabled } = await readBody(req);
|
|
595
|
-
await store.setAutoSyncEnabled(Boolean(enabled));
|
|
596
|
-
writeJson(res, 200, { ok: true, autoSync: store.isAutoSyncEnabled() });
|
|
597
|
-
} catch (err) {
|
|
598
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}), 'dsh-remote-workspace: /watcher/toggle');
|
|
602
|
-
|
|
603
|
-
// 26. POST /dsh-remote-workspace/transfer
|
|
604
|
-
sctx.effect(() => webServer.register({
|
|
605
|
-
kind: 'exact',
|
|
606
|
-
path: '/dsh-remote-workspace/transfer',
|
|
607
|
-
handler: async (req, res) => {
|
|
608
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
609
|
-
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
610
|
-
try {
|
|
611
|
-
const { sourceProfileId, sourcePath, destProfileId, destPath } = await readBody(req);
|
|
612
|
-
const srcProfile = sourceProfileId ? store.getProfile(sourceProfileId) : store.getActiveProfile();
|
|
613
|
-
const destProfile = store.getProfile(destProfileId);
|
|
614
|
-
|
|
615
|
-
if (!srcProfile) return writeJson(res, 404, { ok: false, error: 'Source profile not found' });
|
|
616
|
-
if (!destProfile) return writeJson(res, 404, { ok: false, error: 'Destination profile not found' });
|
|
617
|
-
if (!sourcePath || !destPath) return writeJson(res, 400, { ok: false, error: 'sourcePath and destPath required' });
|
|
618
|
-
|
|
619
|
-
const result = await sshService.transferFileBetweenHosts(srcProfile, sourcePath, destProfile, destPath);
|
|
620
|
-
writeJson(res, 200, { ok: result.success, ...result });
|
|
621
|
-
} catch (err) {
|
|
622
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}), 'dsh-remote-workspace: /transfer');
|
|
626
|
-
});
|
|
627
|
-
}
|
|
1
|
+
import { registerPluginUpdater } from './updater-service.js';
|
|
2
|
+
import { isTrustedSettingsRequest } from './routes/helpers.js';
|
|
3
|
+
import { registerProfileRoutes } from './routes/profiles.js';
|
|
4
|
+
import { registerFileRoutes } from './routes/files.js';
|
|
5
|
+
import { registerEnvRoutes } from './routes/env.js';
|
|
6
|
+
import { registerTerminalRoutes } from './routes/terminal.js';
|
|
7
|
+
import { registerTunnelRoutes } from './routes/tunnels.js';
|
|
8
|
+
import { registerDockerRoutes } from './routes/docker.js';
|
|
9
|
+
import { registerSyncRoutes } from './routes/sync.js';
|
|
10
|
+
|
|
11
|
+
export { isTrustedSettingsRequest } from './routes/helpers.js';
|
|
12
|
+
|
|
13
|
+
export function registerApiRoutes(
|
|
14
|
+
ctx,
|
|
15
|
+
sshService,
|
|
16
|
+
remoteFs,
|
|
17
|
+
mirrorSync,
|
|
18
|
+
tunnelService,
|
|
19
|
+
store,
|
|
20
|
+
dockerService,
|
|
21
|
+
healthService,
|
|
22
|
+
watcherService,
|
|
23
|
+
diagnoseService,
|
|
24
|
+
envService,
|
|
25
|
+
tarSyncService,
|
|
26
|
+
alertService
|
|
27
|
+
) {
|
|
28
|
+
ctx.inject(['webServer'], (sctx) => {
|
|
29
|
+
const webServer = sctx.get ? (sctx.get('webServer') ?? sctx.webServer) : sctx.webServer;
|
|
30
|
+
if (!webServer || typeof webServer.register !== 'function') return;
|
|
31
|
+
|
|
32
|
+
// Plugin updater registration
|
|
33
|
+
sctx.effect(() => registerPluginUpdater(sctx, {
|
|
34
|
+
packageName: '@goodandready/dsh-remote-workspace',
|
|
35
|
+
endpoint: '/dsh-remote-workspace/update',
|
|
36
|
+
manifestUrl: new URL('../package.json', import.meta.url)
|
|
37
|
+
}), 'dsh-remote-workspace: updater');
|
|
38
|
+
|
|
39
|
+
sctx.effect(() => registerPluginUpdater(sctx, {
|
|
40
|
+
packageName: '@goodandready/dsh-remote-workspace',
|
|
41
|
+
endpoint: '/api/dsh-remote-workspace/update',
|
|
42
|
+
manifestUrl: new URL('../package.json', import.meta.url)
|
|
43
|
+
}), 'dsh-remote-workspace: api-updater');
|
|
44
|
+
|
|
45
|
+
registerProfileRoutes(sctx, webServer, { store, sshService, alertService, tunnelService });
|
|
46
|
+
registerFileRoutes(sctx, webServer, { remoteFs, store, sshService });
|
|
47
|
+
registerEnvRoutes(sctx, webServer, { envService, diagnoseService, sshService, store });
|
|
48
|
+
registerTerminalRoutes(sctx, webServer, { sshService, store });
|
|
49
|
+
registerTunnelRoutes(sctx, webServer, { tunnelService, store });
|
|
50
|
+
registerDockerRoutes(sctx, webServer, { dockerService, healthService, store });
|
|
51
|
+
registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService, watcherService, store });
|
|
52
|
+
});
|
|
53
|
+
}
|