@goodandready/dsh-remote-workspace 0.3.0 → 0.3.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.
@@ -0,0 +1,57 @@
1
+ export class DiagnoseService {
2
+ /**
3
+ * @param {import('./ssh-service.js').SshService} sshService
4
+ */
5
+ constructor(sshService) {
6
+ this.sshService = sshService;
7
+ }
8
+
9
+ /**
10
+ * Diagnose system aspects on remote host
11
+ * @param {any} profile
12
+ * @param {'ports'|'disk'|'memory'|'oom_killer'|'service_logs'} category
13
+ * @param {string} [target] optional port, service name, or path
14
+ */
15
+ async diagnose(profile, category, target = '') {
16
+ if (!profile || !profile.host) throw new Error('Invalid profile');
17
+
18
+ let cmd = '';
19
+ switch (category) {
20
+ case 'ports': {
21
+ const filter = target ? `grep ':${target} '` : 'cat';
22
+ cmd = `(ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null || lsof -i -P -n 2>/dev/null) | ${filter} | head -n 40`;
23
+ break;
24
+ }
25
+ case 'disk': {
26
+ const path = target || '/';
27
+ cmd = `df -h ${path} 2>/dev/null; echo "---TOP 10 DIRS---"; du -ahx ${path} 2>/dev/null | sort -rh | head -n 10`;
28
+ break;
29
+ }
30
+ case 'memory': {
31
+ cmd = `free -h 2>/dev/null || free -m; echo "---TOP 10 PROCESSES BY RAM---"; ps aux --sort=-%mem 2>/dev/null | head -n 11`;
32
+ break;
33
+ }
34
+ case 'oom_killer': {
35
+ cmd = `(dmesg -T 2>/dev/null || dmesg 2>/dev/null) | grep -i -E 'oom|out of memory|killed process' | tail -n 25 || echo "No OOM events detected"`;
36
+ break;
37
+ }
38
+ case 'service_logs': {
39
+ if (!target) throw new Error('Target service name required for service_logs');
40
+ const safeName = target.replace(/[^a-zA-Z0-9_.-]/g, '');
41
+ cmd = `(journalctl -u ${safeName} -n 40 --no-pager 2>/dev/null || pm2 logs ${safeName} --lines 40 --nostream 2>/dev/null || tail -n 40 /var/log/${safeName}.log 2>/dev/null || echo "No logs found for ${safeName}")`;
42
+ break;
43
+ }
44
+ default:
45
+ throw new Error(`Unsupported diagnosis category: ${category}`);
46
+ }
47
+
48
+ const res = await this.sshService.exec(profile, cmd);
49
+ const code = res.code !== undefined ? res.code : (res.exitCode !== undefined ? res.exitCode : 0);
50
+ return {
51
+ ok: code === 0,
52
+ category,
53
+ target: target || null,
54
+ output: (res.stdout || res.stderr || '').trim()
55
+ };
56
+ }
57
+ }
@@ -0,0 +1,122 @@
1
+ export class EnvService {
2
+ /**
3
+ * @param {import('./remote-fs-service.js').RemoteFsService} remoteFsService
4
+ */
5
+ constructor(remoteFsService) {
6
+ this.remoteFsService = remoteFsService;
7
+ }
8
+
9
+ /**
10
+ * Parse .env text into structured entries (preserving comments and order)
11
+ * @param {string} text
12
+ */
13
+ parseEnv(text) {
14
+ if (!text) return [];
15
+ const lines = text.split('\n');
16
+ const result = [];
17
+
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ const trimmed = line.trim();
21
+ if (!trimmed) {
22
+ result.push({ type: 'empty', raw: line });
23
+ continue;
24
+ }
25
+ if (trimmed.startsWith('#')) {
26
+ result.push({ type: 'comment', raw: line });
27
+ continue;
28
+ }
29
+
30
+ const eqIdx = line.indexOf('=');
31
+ if (eqIdx === -1) {
32
+ result.push({ type: 'raw', raw: line });
33
+ continue;
34
+ }
35
+
36
+ const key = line.slice(0, eqIdx).trim();
37
+ let val = line.slice(eqIdx + 1);
38
+ // Check if value is wrapped in quotes
39
+ let isQuoted = false;
40
+ const valTrimmed = val.trim();
41
+ if ((valTrimmed.startsWith('"') && valTrimmed.endsWith('"')) ||
42
+ (valTrimmed.startsWith("'") && valTrimmed.endsWith("'"))) {
43
+ isQuoted = true;
44
+ val = valTrimmed.slice(1, -1);
45
+ }
46
+
47
+ result.push({
48
+ type: 'var',
49
+ key,
50
+ value: val,
51
+ isQuoted,
52
+ raw: line
53
+ });
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Read and parse remote .env file
61
+ */
62
+ async readEnv(profile, filePath) {
63
+ const content = await this.remoteFsService.readFile(profile, filePath);
64
+ const parsed = this.parseEnv(content);
65
+ return {
66
+ filePath,
67
+ entries: parsed,
68
+ raw: content
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Set or update variable in remote .env file atomically
74
+ */
75
+ async setEnvVar(profile, filePath, key, value) {
76
+ if (!key || typeof key !== 'string') throw new Error('Key must be non-empty string');
77
+
78
+ let currentContent = '';
79
+ try {
80
+ currentContent = await this.remoteFsService.readFile(profile, filePath);
81
+ } catch {
82
+ currentContent = '';
83
+ }
84
+
85
+ const lines = currentContent ? currentContent.split('\n') : [];
86
+ let updated = false;
87
+ const newLines = [];
88
+
89
+ for (const line of lines) {
90
+ const trimmed = line.trim();
91
+ if (!trimmed.startsWith('#') && trimmed.includes('=')) {
92
+ const k = trimmed.slice(0, trimmed.indexOf('=')).trim();
93
+ if (k === key) {
94
+ // Quote if contains spaces or special symbols
95
+ const formattedVal = (value.includes(' ') || value.includes('\n')) ? `"${value}"` : value;
96
+ newLines.push(`${key}=${formattedVal}`);
97
+ updated = true;
98
+ continue;
99
+ }
100
+ }
101
+ newLines.push(line);
102
+ }
103
+
104
+ if (!updated) {
105
+ const formattedVal = (value.includes(' ') || value.includes('\n')) ? `"${value}"` : value;
106
+ if (newLines.length > 0 && newLines[newLines.length - 1].trim() !== '') {
107
+ newLines.push('');
108
+ }
109
+ newLines.push(`${key}=${formattedVal}`);
110
+ }
111
+
112
+ const finalContent = newLines.join('\n');
113
+ await this.remoteFsService.writeFile(profile, filePath, finalContent);
114
+
115
+ return {
116
+ ok: true,
117
+ filePath,
118
+ key,
119
+ updated
120
+ };
121
+ }
122
+ }
package/lib/index.js CHANGED
@@ -7,6 +7,10 @@ import { TunnelService } from './tunnel-service.js';
7
7
  import { DockerService } from './docker-service.js';
8
8
  import { HealthService } from './health-service.js';
9
9
  import { WatcherService } from './watcher-service.js';
10
+ import { TarSyncService } from './tar-sync-service.js';
11
+ import { DiagnoseService } from './diagnose-service.js';
12
+ import { EnvService } from './env-service.js';
13
+ import { AlertService } from './alert-service.js';
10
14
  import { registerRemoteTools } from './tools.js';
11
15
  import { registerApiRoutes } from './routes.js';
12
16
 
@@ -33,7 +37,8 @@ export const Config = z.object({
33
37
  jumpHostId: z.string().default('')
34
38
  })).default([]).description('List of configured remote machines and workspaces.'),
35
39
  activeProfileId: z.string().default('').description('ID of active remote profile.'),
36
- autoSyncActive: z.boolean().default(false).description('Whether Live Auto-Sync watcher is enabled.')
40
+ autoSyncActive: z.boolean().default(false).description('Whether Live Auto-Sync watcher is enabled.'),
41
+ alertMonitoringActive: z.boolean().default(false).description('Whether background anomaly alerts are monitored.')
37
42
  });
38
43
 
39
44
  export function apply(ctx, config = {}) {
@@ -42,7 +47,8 @@ export function apply(ctx, config = {}) {
42
47
  let currentConfig = {
43
48
  profiles: Array.isArray(cfg.profiles) ? cfg.profiles : [],
44
49
  activeProfileId: cfg.activeProfileId || null,
45
- autoSyncActive: Boolean(cfg.autoSyncActive)
50
+ autoSyncActive: Boolean(cfg.autoSyncActive),
51
+ alertMonitoringActive: Boolean(cfg.alertMonitoringActive)
46
52
  };
47
53
 
48
54
  const vaultService = new VaultService();
@@ -53,6 +59,10 @@ export function apply(ctx, config = {}) {
53
59
  const dockerService = new DockerService(sshService);
54
60
  const healthService = new HealthService(sshService);
55
61
  const watcherService = new WatcherService();
62
+ const tarSyncService = new TarSyncService(sshService);
63
+ const diagnoseService = new DiagnoseService(sshService);
64
+ const envService = new EnvService(remoteFs);
65
+ const alertService = new AlertService(ctx, healthService, dockerService);
56
66
 
57
67
  // Migrate any existing plain passwords into vault upon start
58
68
  for (const p of currentConfig.profiles) {
@@ -179,6 +189,7 @@ export function apply(ctx, config = {}) {
179
189
  if (Array.isArray(snap.profiles)) currentConfig.profiles = snap.profiles;
180
190
  if (snap.activeProfileId !== undefined) currentConfig.activeProfileId = snap.activeProfileId;
181
191
  if (snap.autoSyncActive !== undefined) currentConfig.autoSyncActive = snap.autoSyncActive;
192
+ if (snap.alertMonitoringActive !== undefined) currentConfig.alertMonitoringActive = snap.alertMonitoringActive;
182
193
  }
183
194
  sctx.effect(() => () => {
184
195
  settingsApi = null;
@@ -193,6 +204,10 @@ export function apply(ctx, config = {}) {
193
204
  ctx.provide('remoteDocker');
194
205
  ctx.provide('remoteHealth');
195
206
  ctx.provide('remoteWatcher');
207
+ ctx.provide('remoteTarSync');
208
+ ctx.provide('remoteDiagnose');
209
+ ctx.provide('remoteEnv');
210
+ ctx.provide('remoteAlert');
196
211
 
197
212
  ctx.remoteSsh = sshService;
198
213
  ctx.remoteFs = remoteFs;
@@ -202,6 +217,10 @@ export function apply(ctx, config = {}) {
202
217
  ctx.remoteDocker = dockerService;
203
218
  ctx.remoteHealth = healthService;
204
219
  ctx.remoteWatcher = watcherService;
220
+ ctx.remoteTarSync = tarSyncService;
221
+ ctx.remoteDiagnose = diagnoseService;
222
+ ctx.remoteEnv = envService;
223
+ ctx.remoteAlert = alertService;
205
224
 
206
225
  // Tools registration
207
226
  registerRemoteTools(
@@ -212,14 +231,32 @@ export function apply(ctx, config = {}) {
212
231
  tunnelService,
213
232
  () => store.getActiveProfile(),
214
233
  (id) => store.getProfile(id),
215
- dockerService
234
+ dockerService,
235
+ diagnoseService,
236
+ envService,
237
+ tarSyncService
216
238
  );
217
239
 
218
240
  // API Routes
219
- registerApiRoutes(ctx, sshService, remoteFs, mirrorSync, tunnelService, store, dockerService, healthService, watcherService);
241
+ registerApiRoutes(
242
+ ctx,
243
+ sshService,
244
+ remoteFs,
245
+ mirrorSync,
246
+ tunnelService,
247
+ store,
248
+ dockerService,
249
+ healthService,
250
+ watcherService,
251
+ diagnoseService,
252
+ envService,
253
+ tarSyncService,
254
+ alertService
255
+ );
220
256
 
221
257
  // Cleanup on unload
222
258
  ctx.on('dispose', () => {
259
+ alertService.stop();
223
260
  watcherService.stopAll();
224
261
  tunnelService.stopAll();
225
262
  sshService.disconnectAll();