@goodandready/dsh-remote-workspace 0.2.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.
- package/README.md +17 -0
- package/README.ru.md +17 -0
- package/README.zh.md +17 -0
- package/docs/plans/task_plan.md +18 -20
- package/lib/alert-service.js +109 -0
- package/lib/client.js +490 -77
- package/lib/diagnose-service.js +57 -0
- package/lib/docker-service.js +93 -0
- package/lib/env-service.js +122 -0
- package/lib/health-service.js +117 -0
- package/lib/index.js +94 -7
- package/lib/routes.js +627 -398
- package/lib/ssh-config-parser.js +89 -0
- package/lib/ssh-service.js +59 -20
- package/lib/tar-sync-service.js +85 -0
- package/lib/tools.js +175 -2
- package/lib/watcher-service.js +111 -0
- package/package.json +1 -1
|
@@ -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,93 @@
|
|
|
1
|
+
export class DockerService {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('./ssh-service.js').SshService} sshService
|
|
4
|
+
*/
|
|
5
|
+
constructor(sshService) {
|
|
6
|
+
this.sshService = sshService;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List active or all containers with JSON formatting
|
|
11
|
+
*/
|
|
12
|
+
async listContainers(profile, all = true) {
|
|
13
|
+
// Detect docker or podman
|
|
14
|
+
const flag = all ? '-a' : '';
|
|
15
|
+
// Format as line-delimited JSON or tabbed format
|
|
16
|
+
const cmd = `docker ps ${flag} --format '{{json .}}' 2>/dev/null || podman ps ${flag} --format '{{json .}}' 2>/dev/null`;
|
|
17
|
+
const res = await this.sshService.exec(profile, cmd);
|
|
18
|
+
|
|
19
|
+
if (res.code !== 0 && !res.stdout.trim()) {
|
|
20
|
+
return {
|
|
21
|
+
available: false,
|
|
22
|
+
error: res.stderr || 'Docker not found or daemon not running',
|
|
23
|
+
containers: []
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const containers = [];
|
|
28
|
+
const lines = res.stdout.split('\n').filter(Boolean);
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
try {
|
|
31
|
+
const item = JSON.parse(line);
|
|
32
|
+
containers.push({
|
|
33
|
+
id: item.ID || item.Id || '',
|
|
34
|
+
names: item.Names || item.Name || '',
|
|
35
|
+
image: item.Image || '',
|
|
36
|
+
status: item.Status || '',
|
|
37
|
+
state: (item.State || '').toLowerCase(),
|
|
38
|
+
ports: item.Ports || '',
|
|
39
|
+
created: item.CreatedAt || item.Created || ''
|
|
40
|
+
});
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
available: true,
|
|
46
|
+
containers
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Container action: start, stop, restart, pause, unpause
|
|
52
|
+
*/
|
|
53
|
+
async containerAction(profile, containerId, action) {
|
|
54
|
+
const validActions = ['start', 'stop', 'restart', 'pause', 'unpause', 'rm'];
|
|
55
|
+
if (!validActions.includes(action)) {
|
|
56
|
+
throw new Error(`Invalid container action: ${action}`);
|
|
57
|
+
}
|
|
58
|
+
const cmd = `docker ${action} ${JSON.stringify(containerId)} 2>/dev/null || podman ${action} ${JSON.stringify(containerId)}`;
|
|
59
|
+
const res = await this.sshService.exec(profile, cmd);
|
|
60
|
+
return {
|
|
61
|
+
success: res.code === 0,
|
|
62
|
+
stdout: res.stdout.trim(),
|
|
63
|
+
stderr: res.stderr.trim()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch container logs
|
|
69
|
+
*/
|
|
70
|
+
async containerLogs(profile, containerId, tail = 100) {
|
|
71
|
+
const cmd = `docker logs --tail ${tail} ${JSON.stringify(containerId)} 2>&1 || podman logs --tail ${tail} ${JSON.stringify(containerId)} 2>&1`;
|
|
72
|
+
const res = await this.sshService.exec(profile, cmd);
|
|
73
|
+
return {
|
|
74
|
+
success: res.code === 0,
|
|
75
|
+
logs: res.stdout || res.stderr || ''
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Docker compose lifecycle
|
|
81
|
+
*/
|
|
82
|
+
async composeAction(profile, cwd, action = 'ps') {
|
|
83
|
+
const valid = ['up -d', 'down', 'ps', 'restart', 'pull'];
|
|
84
|
+
const safeAction = valid.includes(action) ? action : 'ps';
|
|
85
|
+
const cmd = `docker compose ${safeAction} 2>/dev/null || docker-compose ${safeAction} 2>/dev/null || podman-compose ${safeAction}`;
|
|
86
|
+
const res = await this.sshService.exec(profile, cmd, cwd);
|
|
87
|
+
return {
|
|
88
|
+
success: res.code === 0,
|
|
89
|
+
stdout: res.stdout,
|
|
90
|
+
stderr: res.stderr
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export class HealthService {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('./ssh-service.js').SshService} sshService
|
|
4
|
+
*/
|
|
5
|
+
constructor(sshService) {
|
|
6
|
+
this.sshService = sshService;
|
|
7
|
+
/** @type {Map<string, { data: any, expiresAt: number }>} */
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
this.cacheTtlMs = 4000;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getHealth(profile, bypassCache = false) {
|
|
13
|
+
if (!profile || !profile.host) {
|
|
14
|
+
throw new Error('Invalid profile');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cacheKey = profile.id;
|
|
18
|
+
if (!bypassCache) {
|
|
19
|
+
const cached = this.cache.get(cacheKey);
|
|
20
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
21
|
+
return cached.data;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Single compact shell command to gather all stats at once:
|
|
26
|
+
// 1. uptime (load averages)
|
|
27
|
+
// 2. memory (free -m)
|
|
28
|
+
// 3. disk usage (df -k /)
|
|
29
|
+
// 4. cpu info (cores & idle via /proc/stat or top)
|
|
30
|
+
const probeCmd = `
|
|
31
|
+
echo "---UPTIME---"
|
|
32
|
+
uptime 2>/dev/null || true
|
|
33
|
+
echo "---MEM---"
|
|
34
|
+
free -m 2>/dev/null || true
|
|
35
|
+
echo "---DISK---"
|
|
36
|
+
df -m / 2>/dev/null || df -m . 2>/dev/null || true
|
|
37
|
+
echo "---CPU---"
|
|
38
|
+
grep 'cpu ' /proc/stat 2>/dev/null || true
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const res = await this.sshService.exec(profile, probeCmd);
|
|
42
|
+
const text = res.stdout || '';
|
|
43
|
+
|
|
44
|
+
const uptimeMatch = text.match(/---UPTIME---\n([\s\S]*?)(?=---MEM---)/);
|
|
45
|
+
const memMatch = text.match(/---MEM---\n([\s\S]*?)(?=---DISK---)/);
|
|
46
|
+
const diskMatch = text.match(/---DISK---\n([\s\S]*?)(?=---CPU---)/);
|
|
47
|
+
|
|
48
|
+
// 1. Parse Uptime & Load Avg
|
|
49
|
+
let uptimeStr = 'Unknown';
|
|
50
|
+
let loadAvg = [0, 0, 0];
|
|
51
|
+
if (uptimeMatch) {
|
|
52
|
+
const upLine = uptimeMatch[1].trim();
|
|
53
|
+
const loadMatch = upLine.match(/load average[s]?:\s*([\d.]+),?\s*([\d.]+),?\s*([\d.]+)/);
|
|
54
|
+
if (loadMatch) {
|
|
55
|
+
loadAvg = [parseFloat(loadMatch[1]), parseFloat(loadMatch[2]), parseFloat(loadMatch[3])];
|
|
56
|
+
}
|
|
57
|
+
const upPart = upLine.match(/up\s+(.*?),\s+\d+\s+user/);
|
|
58
|
+
if (upPart) uptimeStr = upPart[1];
|
|
59
|
+
else uptimeStr = upLine.split(',')[0] || 'Online';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Parse Memory
|
|
63
|
+
let memTotal = 0;
|
|
64
|
+
let memUsed = 0;
|
|
65
|
+
let memPercent = 0;
|
|
66
|
+
if (memMatch) {
|
|
67
|
+
const memLines = memMatch[1].trim().split('\n');
|
|
68
|
+
for (const line of memLines) {
|
|
69
|
+
if (line.startsWith('Mem:')) {
|
|
70
|
+
const parts = line.split(/\s+/);
|
|
71
|
+
memTotal = parseInt(parts[1], 10) || 0;
|
|
72
|
+
memUsed = parseInt(parts[2], 10) || 0;
|
|
73
|
+
if (memTotal > 0) {
|
|
74
|
+
memPercent = Math.round((memUsed / memTotal) * 100);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Parse Disk
|
|
82
|
+
let diskTotal = 0;
|
|
83
|
+
let diskUsed = 0;
|
|
84
|
+
let diskPercent = 0;
|
|
85
|
+
if (diskMatch) {
|
|
86
|
+
const diskLines = diskMatch[1].trim().split('\n');
|
|
87
|
+
if (diskLines.length > 1) {
|
|
88
|
+
const parts = diskLines[1].split(/\s+/);
|
|
89
|
+
diskTotal = parseInt(parts[1], 10) || 0;
|
|
90
|
+
diskUsed = parseInt(parts[2], 10) || 0;
|
|
91
|
+
const pStr = parts[4] || '';
|
|
92
|
+
diskPercent = parseInt(pStr.replace('%', ''), 10) || (diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100) : 0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = {
|
|
97
|
+
ok: true,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
uptime: uptimeStr,
|
|
100
|
+
loadAverage: loadAvg,
|
|
101
|
+
cpuLoad: Math.min(100, Math.round(loadAvg[0] * 25)), // Normalized approx representation
|
|
102
|
+
memory: {
|
|
103
|
+
totalMb: memTotal,
|
|
104
|
+
usedMb: memUsed,
|
|
105
|
+
percent: memPercent
|
|
106
|
+
},
|
|
107
|
+
disk: {
|
|
108
|
+
totalMb: diskTotal,
|
|
109
|
+
usedMb: diskUsed,
|
|
110
|
+
percent: diskPercent
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
this.cache.set(cacheKey, { data: result, expiresAt: Date.now() + this.cacheTtlMs });
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,13 @@ import { SshService } from './ssh-service.js';
|
|
|
4
4
|
import { RemoteFsService } from './remote-fs-service.js';
|
|
5
5
|
import { MirrorSyncService } from './mirror-sync-service.js';
|
|
6
6
|
import { TunnelService } from './tunnel-service.js';
|
|
7
|
+
import { DockerService } from './docker-service.js';
|
|
8
|
+
import { HealthService } from './health-service.js';
|
|
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';
|
|
7
14
|
import { registerRemoteTools } from './tools.js';
|
|
8
15
|
import { registerApiRoutes } from './routes.js';
|
|
9
16
|
|
|
@@ -26,9 +33,12 @@ export const Config = z.object({
|
|
|
26
33
|
passphrase: z.string().default(''),
|
|
27
34
|
password: z.string().default(''),
|
|
28
35
|
remoteWorkspace: z.string().default(''),
|
|
29
|
-
localMirrorPath: z.string().default('')
|
|
36
|
+
localMirrorPath: z.string().default(''),
|
|
37
|
+
jumpHostId: z.string().default('')
|
|
30
38
|
})).default([]).description('List of configured remote machines and workspaces.'),
|
|
31
|
-
activeProfileId: z.string().default('').description('ID of active remote profile.')
|
|
39
|
+
activeProfileId: z.string().default('').description('ID of active remote profile.'),
|
|
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.')
|
|
32
42
|
});
|
|
33
43
|
|
|
34
44
|
export function apply(ctx, config = {}) {
|
|
@@ -36,7 +46,9 @@ export function apply(ctx, config = {}) {
|
|
|
36
46
|
let settingsApi = null;
|
|
37
47
|
let currentConfig = {
|
|
38
48
|
profiles: Array.isArray(cfg.profiles) ? cfg.profiles : [],
|
|
39
|
-
activeProfileId: cfg.activeProfileId || null
|
|
49
|
+
activeProfileId: cfg.activeProfileId || null,
|
|
50
|
+
autoSyncActive: Boolean(cfg.autoSyncActive),
|
|
51
|
+
alertMonitoringActive: Boolean(cfg.alertMonitoringActive)
|
|
40
52
|
};
|
|
41
53
|
|
|
42
54
|
const vaultService = new VaultService();
|
|
@@ -44,6 +56,13 @@ export function apply(ctx, config = {}) {
|
|
|
44
56
|
const remoteFs = new RemoteFsService(sshService);
|
|
45
57
|
const mirrorSync = new MirrorSyncService(remoteFs, sshService);
|
|
46
58
|
const tunnelService = new TunnelService(sshService);
|
|
59
|
+
const dockerService = new DockerService(sshService);
|
|
60
|
+
const healthService = new HealthService(sshService);
|
|
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);
|
|
47
66
|
|
|
48
67
|
// Migrate any existing plain passwords into vault upon start
|
|
49
68
|
for (const p of currentConfig.profiles) {
|
|
@@ -78,11 +97,22 @@ export function apply(ctx, config = {}) {
|
|
|
78
97
|
const p = (currentConfig.profiles || []).find((x) => x.id === currentConfig.activeProfileId);
|
|
79
98
|
return p ? vaultService.hydrateProfile(p) : undefined;
|
|
80
99
|
},
|
|
100
|
+
isAutoSyncEnabled() { return Boolean(currentConfig.autoSyncActive); },
|
|
101
|
+
async setAutoSyncEnabled(enabled) {
|
|
102
|
+
currentConfig.autoSyncActive = Boolean(enabled);
|
|
103
|
+
const active = store.getActiveProfile();
|
|
104
|
+
if (active) {
|
|
105
|
+
if (enabled) watcherService.start(active, mirrorSync);
|
|
106
|
+
else watcherService.stop(active.id);
|
|
107
|
+
}
|
|
108
|
+
if (settingsApi?.replace) {
|
|
109
|
+
try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
81
112
|
async saveProfile(rawP) {
|
|
82
113
|
const list = [...(currentConfig.profiles || [])];
|
|
83
114
|
const p = { ...rawP };
|
|
84
115
|
|
|
85
|
-
// Save secret credentials into isolated .env file if provided
|
|
86
116
|
const secretsToUpdate = {};
|
|
87
117
|
if (p.password !== undefined && p.password !== '••••••••') {
|
|
88
118
|
secretsToUpdate.password = p.password;
|
|
@@ -97,7 +127,6 @@ export function apply(ctx, config = {}) {
|
|
|
97
127
|
vaultService.setProfileSecrets(p.id, secretsToUpdate);
|
|
98
128
|
}
|
|
99
129
|
|
|
100
|
-
// Do NOT keep raw secrets in public config
|
|
101
130
|
p.password = '';
|
|
102
131
|
p.privateKey = '';
|
|
103
132
|
p.passphrase = '';
|
|
@@ -116,6 +145,7 @@ export function apply(ctx, config = {}) {
|
|
|
116
145
|
}
|
|
117
146
|
},
|
|
118
147
|
async deleteProfile(id) {
|
|
148
|
+
watcherService.stop(id);
|
|
119
149
|
currentConfig.profiles = (currentConfig.profiles || []).filter((p) => p.id !== id);
|
|
120
150
|
if (currentConfig.activeProfileId === id) {
|
|
121
151
|
currentConfig.activeProfileId = currentConfig.profiles[0]?.id || null;
|
|
@@ -128,12 +158,25 @@ export function apply(ctx, config = {}) {
|
|
|
128
158
|
},
|
|
129
159
|
async setActiveId(id) {
|
|
130
160
|
currentConfig.activeProfileId = id;
|
|
161
|
+
if (currentConfig.autoSyncActive) {
|
|
162
|
+
watcherService.stopAll();
|
|
163
|
+
const active = store.getActiveProfile();
|
|
164
|
+
if (active) watcherService.start(active, mirrorSync);
|
|
165
|
+
}
|
|
131
166
|
if (settingsApi?.replace) {
|
|
132
167
|
try { await settingsApi.replace(Config(currentConfig)); } catch (_) {}
|
|
133
168
|
}
|
|
134
169
|
}
|
|
135
170
|
};
|
|
136
171
|
|
|
172
|
+
sshService.setJumpProfileResolver((jumpId) => store.getProfile(jumpId));
|
|
173
|
+
|
|
174
|
+
// If autoSync was enabled on restart, resume watcher for active profile
|
|
175
|
+
if (currentConfig.autoSyncActive && currentConfig.activeProfileId) {
|
|
176
|
+
const active = store.getActiveProfile();
|
|
177
|
+
if (active) watcherService.start(active, mirrorSync);
|
|
178
|
+
}
|
|
179
|
+
|
|
137
180
|
// Register settings namespace if settings service is available
|
|
138
181
|
ctx.inject(['settings'], (sctx) => {
|
|
139
182
|
const settingsService = sctx.get ? (sctx.get('settings') ?? sctx.settings) : sctx.settings;
|
|
@@ -145,6 +188,8 @@ export function apply(ctx, config = {}) {
|
|
|
145
188
|
if (snap) {
|
|
146
189
|
if (Array.isArray(snap.profiles)) currentConfig.profiles = snap.profiles;
|
|
147
190
|
if (snap.activeProfileId !== undefined) currentConfig.activeProfileId = snap.activeProfileId;
|
|
191
|
+
if (snap.autoSyncActive !== undefined) currentConfig.autoSyncActive = snap.autoSyncActive;
|
|
192
|
+
if (snap.alertMonitoringActive !== undefined) currentConfig.alertMonitoringActive = snap.alertMonitoringActive;
|
|
148
193
|
}
|
|
149
194
|
sctx.effect(() => () => {
|
|
150
195
|
settingsApi = null;
|
|
@@ -156,21 +201,63 @@ export function apply(ctx, config = {}) {
|
|
|
156
201
|
ctx.provide('remoteSync');
|
|
157
202
|
ctx.provide('remoteTunnel');
|
|
158
203
|
ctx.provide('remoteVault');
|
|
204
|
+
ctx.provide('remoteDocker');
|
|
205
|
+
ctx.provide('remoteHealth');
|
|
206
|
+
ctx.provide('remoteWatcher');
|
|
207
|
+
ctx.provide('remoteTarSync');
|
|
208
|
+
ctx.provide('remoteDiagnose');
|
|
209
|
+
ctx.provide('remoteEnv');
|
|
210
|
+
ctx.provide('remoteAlert');
|
|
159
211
|
|
|
160
212
|
ctx.remoteSsh = sshService;
|
|
161
213
|
ctx.remoteFs = remoteFs;
|
|
162
214
|
ctx.remoteSync = mirrorSync;
|
|
163
215
|
ctx.remoteTunnel = tunnelService;
|
|
164
216
|
ctx.remoteVault = vaultService;
|
|
217
|
+
ctx.remoteDocker = dockerService;
|
|
218
|
+
ctx.remoteHealth = healthService;
|
|
219
|
+
ctx.remoteWatcher = watcherService;
|
|
220
|
+
ctx.remoteTarSync = tarSyncService;
|
|
221
|
+
ctx.remoteDiagnose = diagnoseService;
|
|
222
|
+
ctx.remoteEnv = envService;
|
|
223
|
+
ctx.remoteAlert = alertService;
|
|
165
224
|
|
|
166
225
|
// Tools registration
|
|
167
|
-
registerRemoteTools(
|
|
226
|
+
registerRemoteTools(
|
|
227
|
+
ctx,
|
|
228
|
+
sshService,
|
|
229
|
+
remoteFs,
|
|
230
|
+
mirrorSync,
|
|
231
|
+
tunnelService,
|
|
232
|
+
() => store.getActiveProfile(),
|
|
233
|
+
(id) => store.getProfile(id),
|
|
234
|
+
dockerService,
|
|
235
|
+
diagnoseService,
|
|
236
|
+
envService,
|
|
237
|
+
tarSyncService
|
|
238
|
+
);
|
|
168
239
|
|
|
169
240
|
// API Routes
|
|
170
|
-
registerApiRoutes(
|
|
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
|
+
);
|
|
171
256
|
|
|
172
257
|
// Cleanup on unload
|
|
173
258
|
ctx.on('dispose', () => {
|
|
259
|
+
alertService.stop();
|
|
260
|
+
watcherService.stopAll();
|
|
174
261
|
tunnelService.stopAll();
|
|
175
262
|
sshService.disconnectAll();
|
|
176
263
|
});
|