@mgsoftwarebv/mg-dashboard-mcp 3.5.0 → 3.6.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/dist/index.js +264 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1372,6 +1372,8 @@ var TOOL_MODULE_MAP = {
|
|
|
1372
1372
|
"sftp-delete": "ssh_servers",
|
|
1373
1373
|
"docker-list": "ssh_servers",
|
|
1374
1374
|
"docker-logs": "ssh_servers",
|
|
1375
|
+
"docker-exec": "ssh_servers",
|
|
1376
|
+
"docker-compose": "ssh_servers",
|
|
1375
1377
|
"db-discover": "ssh_servers",
|
|
1376
1378
|
"db-tables": "ssh_servers",
|
|
1377
1379
|
"db-describe": "ssh_servers",
|
|
@@ -1819,6 +1821,29 @@ async function attemptVercelSync(appName, environment, knownStageId) {
|
|
|
1819
1821
|
return `Vercel sync error: ${msg}`;
|
|
1820
1822
|
}
|
|
1821
1823
|
}
|
|
1824
|
+
function posixQuote(arg) {
|
|
1825
|
+
if (arg === "") return "''";
|
|
1826
|
+
if (/^[A-Za-z0-9._\/=:@%+\-]+$/.test(arg)) return arg;
|
|
1827
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
1828
|
+
}
|
|
1829
|
+
function buildPosixCommand(command, args2) {
|
|
1830
|
+
const program = /^[A-Za-z0-9._\/\- ]+$/.test(command) ? command : posixQuote(command);
|
|
1831
|
+
if (args2.length === 0) return program;
|
|
1832
|
+
return `${program} ${args2.map(posixQuote).join(" ")}`;
|
|
1833
|
+
}
|
|
1834
|
+
function buildPowerShellEncodedCommand(command, args2) {
|
|
1835
|
+
const psSingleQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
1836
|
+
let body;
|
|
1837
|
+
if (args2.length === 0) {
|
|
1838
|
+
body = command;
|
|
1839
|
+
} else {
|
|
1840
|
+
body = `& ${psSingleQuote(command)} ${args2.map(psSingleQuote).join(" ")}`;
|
|
1841
|
+
}
|
|
1842
|
+
const psExpr = `$ProgressPreference='SilentlyContinue'; ${body}`;
|
|
1843
|
+
const utf16 = Buffer.from(psExpr, "utf16le");
|
|
1844
|
+
const b64 = utf16.toString("base64");
|
|
1845
|
+
return `powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${b64}`;
|
|
1846
|
+
}
|
|
1822
1847
|
var SSH_PROXY_SERVER_ID = "03659d55-e194-400d-b82a-bf6457371ded";
|
|
1823
1848
|
var _proxyConnCache = null;
|
|
1824
1849
|
async function getProxyConnection() {
|
|
@@ -1838,7 +1863,7 @@ async function getProxyConnection() {
|
|
|
1838
1863
|
}
|
|
1839
1864
|
async function getServerConnection(serverId) {
|
|
1840
1865
|
assertServerAccess(serverId);
|
|
1841
|
-
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted, allowed_ssh_ips").eq("id", serverId).single();
|
|
1866
|
+
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted, allowed_ssh_ips, os_type").eq("id", serverId).single();
|
|
1842
1867
|
if (error || !data) throw new Error(`Server not found: ${serverId}`);
|
|
1843
1868
|
if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
|
|
1844
1869
|
const conn = {
|
|
@@ -1851,10 +1876,11 @@ async function getServerConnection(serverId) {
|
|
|
1851
1876
|
};
|
|
1852
1877
|
const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
|
|
1853
1878
|
const proxy = needsProxy ? await getProxyConnection() : void 0;
|
|
1854
|
-
|
|
1879
|
+
const os = data.os_type === "windows" ? "windows" : "linux";
|
|
1880
|
+
return { conn, proxy, os };
|
|
1855
1881
|
}
|
|
1856
|
-
async function sshExec(opts, command, proxy) {
|
|
1857
|
-
if (proxy) return sshExecViaProxy(proxy, opts, command);
|
|
1882
|
+
async function sshExec(opts, command, proxy, options) {
|
|
1883
|
+
if (proxy) return sshExecViaProxy(proxy, opts, command, options);
|
|
1858
1884
|
return new Promise((resolve) => {
|
|
1859
1885
|
const ssh = new Client();
|
|
1860
1886
|
let stdout = "";
|
|
@@ -1893,6 +1919,9 @@ async function sshExec(opts, command, proxy) {
|
|
|
1893
1919
|
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
1894
1920
|
}
|
|
1895
1921
|
});
|
|
1922
|
+
if (options?.stdin !== void 0) {
|
|
1923
|
+
stream.end(options.stdin);
|
|
1924
|
+
}
|
|
1896
1925
|
});
|
|
1897
1926
|
});
|
|
1898
1927
|
ssh.on("error", (err) => {
|
|
@@ -1913,7 +1942,7 @@ async function sshExec(opts, command, proxy) {
|
|
|
1913
1942
|
});
|
|
1914
1943
|
});
|
|
1915
1944
|
}
|
|
1916
|
-
function sshExecViaProxy(proxyOpts, targetOpts, command) {
|
|
1945
|
+
function sshExecViaProxy(proxyOpts, targetOpts, command, options) {
|
|
1917
1946
|
return new Promise((resolve) => {
|
|
1918
1947
|
const proxyClient = new Client();
|
|
1919
1948
|
let done = false;
|
|
@@ -1967,6 +1996,9 @@ function sshExecViaProxy(proxyOpts, targetOpts, command) {
|
|
|
1967
1996
|
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
1968
1997
|
}
|
|
1969
1998
|
});
|
|
1999
|
+
if (options?.stdin !== void 0) {
|
|
2000
|
+
stream.end(options.stdin);
|
|
2001
|
+
}
|
|
1970
2002
|
});
|
|
1971
2003
|
});
|
|
1972
2004
|
targetClient.on("error", (targetErr) => {
|
|
@@ -2091,8 +2123,22 @@ function assertWritablePath(path) {
|
|
|
2091
2123
|
}
|
|
2092
2124
|
}
|
|
2093
2125
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2126
|
+
function globToRegExp(pattern) {
|
|
2127
|
+
let re = "";
|
|
2128
|
+
for (const c of pattern) {
|
|
2129
|
+
if (c === "*") re += ".*";
|
|
2130
|
+
else if (c === "?") re += ".";
|
|
2131
|
+
else if (/[.+^${}()|[\]\\]/.test(c)) re += "\\" + c;
|
|
2132
|
+
else re += c;
|
|
2133
|
+
}
|
|
2134
|
+
return new RegExp(`^${re}$`);
|
|
2135
|
+
}
|
|
2136
|
+
async function sftpReaddir(opts, dirPath, proxy, options) {
|
|
2137
|
+
const recursive = options?.recursive === true;
|
|
2138
|
+
const maxDepth = Math.max(1, Math.min(20, options?.maxDepth ?? 5));
|
|
2139
|
+
const maxResults = Math.max(1, Math.min(5e4, options?.maxResults ?? 5e3));
|
|
2140
|
+
const matcher = options?.pattern ? globToRegExp(options.pattern) : null;
|
|
2141
|
+
const rootSafe = sanitizePath(dirPath);
|
|
2096
2142
|
let cleanup;
|
|
2097
2143
|
try {
|
|
2098
2144
|
const { client, cleanup: c } = await connectSshClient(opts, proxy, 3e4);
|
|
@@ -2102,7 +2148,7 @@ async function sftpReaddir(opts, dirPath, proxy) {
|
|
|
2102
2148
|
cleanup?.();
|
|
2103
2149
|
resolve("Error: timeout");
|
|
2104
2150
|
cleanup = void 0;
|
|
2105
|
-
},
|
|
2151
|
+
}, 6e4);
|
|
2106
2152
|
client.sftp((err, sftp) => {
|
|
2107
2153
|
if (err) {
|
|
2108
2154
|
clearTimeout(timer);
|
|
@@ -2111,24 +2157,48 @@ async function sftpReaddir(opts, dirPath, proxy) {
|
|
|
2111
2157
|
resolve(`Error: ${err.message}`);
|
|
2112
2158
|
return;
|
|
2113
2159
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2160
|
+
const lines = [];
|
|
2161
|
+
let truncated = false;
|
|
2162
|
+
const readOne = (path, depth) => new Promise((resolveOne) => {
|
|
2163
|
+
sftp.readdir(path, (err2, list) => {
|
|
2164
|
+
if (err2) {
|
|
2165
|
+
lines.push(`! error reading ${path}: ${err2.message}`);
|
|
2166
|
+
return resolveOne();
|
|
2167
|
+
}
|
|
2168
|
+
const subdirs = [];
|
|
2169
|
+
for (const item of list) {
|
|
2170
|
+
if (lines.length >= maxResults) {
|
|
2171
|
+
truncated = true;
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
2174
|
+
const mode = item.attrs.mode || 0;
|
|
2175
|
+
const isDir = (mode & 61440) === 16384;
|
|
2176
|
+
const size = item.attrs.size || 0;
|
|
2177
|
+
const mtime = item.attrs.mtime ? new Date(item.attrs.mtime * 1e3).toISOString() : "";
|
|
2178
|
+
const fullPath = path === "/" ? `/${item.filename}` : `${path}/${item.filename}`;
|
|
2179
|
+
const include = !matcher || matcher.test(item.filename);
|
|
2180
|
+
if (include) {
|
|
2181
|
+
const display = recursive ? fullPath : item.filename;
|
|
2182
|
+
lines.push(`${isDir ? "d" : "-"} ${String(size).padStart(10)} ${mtime} ${display}`);
|
|
2183
|
+
}
|
|
2184
|
+
if (isDir && recursive && depth < maxDepth) subdirs.push(fullPath);
|
|
2185
|
+
}
|
|
2186
|
+
if (truncated || subdirs.length === 0) return resolveOne();
|
|
2187
|
+
(async () => {
|
|
2188
|
+
for (const sub of subdirs) {
|
|
2189
|
+
if (truncated) break;
|
|
2190
|
+
await readOne(sub, depth + 1);
|
|
2191
|
+
}
|
|
2192
|
+
resolveOne();
|
|
2193
|
+
})();
|
|
2128
2194
|
});
|
|
2195
|
+
});
|
|
2196
|
+
readOne(rootSafe, 1).then(() => {
|
|
2197
|
+
clearTimeout(timer);
|
|
2129
2198
|
cleanup?.();
|
|
2130
2199
|
cleanup = void 0;
|
|
2131
|
-
|
|
2200
|
+
if (truncated) lines.push(`... (truncated at ${maxResults} entries; raise maxResults or narrow path/pattern)`);
|
|
2201
|
+
resolve(lines.length ? lines.join("\n") : "No entries");
|
|
2132
2202
|
});
|
|
2133
2203
|
});
|
|
2134
2204
|
});
|
|
@@ -2552,12 +2622,15 @@ var TOOLS = [
|
|
|
2552
2622
|
},
|
|
2553
2623
|
{
|
|
2554
2624
|
name: "ssh-execute",
|
|
2555
|
-
description:
|
|
2625
|
+
description: 'Execute a command on a remote server via SSH. OS-aware: automatically wraps in bash on linux servers and `powershell -EncodedCommand` (UTF-16LE base64) on windows servers, so $, #, quotes, spaces inside `args` are never re-interpreted by a shell. Some dangerous commands are blocked. Use `list-servers` first to see each server\'s os_type.\nTwo ways to invoke (use `args` for anything with passwords or special chars):\n- Quick: `command` only, e.g. `command: "df -h"` (raw shell string, OS-dispatched but caller-quoted).\n- Safe: `command` + `args[]`, e.g. `command: "mysql"`, `args: ["-u", "root", "-p$tr@nge#pwd", "-e", "SELECT 1"]` \u2014 every arg is quoted/encoded for the target OS.\n`stdin` lets you pipe data into the remote process (queries, scripts, secrets) without putting it on the command line.',
|
|
2556
2626
|
inputSchema: {
|
|
2557
2627
|
type: "object",
|
|
2558
2628
|
properties: {
|
|
2559
2629
|
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2560
|
-
command: { type: "string", description:
|
|
2630
|
+
command: { type: "string", description: 'Program/command to run (e.g. "mysql", "Get-Service", "df"). Required.' },
|
|
2631
|
+
args: { type: "array", items: { type: "string" }, description: "Optional argv list. When provided, each entry is safely quoted/encoded for the target OS." },
|
|
2632
|
+
stdin: { type: "string", description: "Optional data piped to the remote process stdin (use for SQL queries, scripts, secrets \u2014 anything you do NOT want on the command line)." },
|
|
2633
|
+
shell: { type: "string", enum: ["auto", "bash", "powershell"], description: `Override shell selection (default "auto" uses the server's os_type).` },
|
|
2561
2634
|
timeout: { type: "number", description: "Timeout in milliseconds (default: 60000)" }
|
|
2562
2635
|
},
|
|
2563
2636
|
required: ["serverId", "command"]
|
|
@@ -2565,12 +2638,16 @@ var TOOLS = [
|
|
|
2565
2638
|
},
|
|
2566
2639
|
{
|
|
2567
2640
|
name: "sftp-list",
|
|
2568
|
-
description:
|
|
2641
|
+
description: 'List files and directories on a remote server via SFTP. Supports recursive traversal and glob filtering, eliminating the need to fall back on `ssh-execute "find ..."`.',
|
|
2569
2642
|
inputSchema: {
|
|
2570
2643
|
type: "object",
|
|
2571
2644
|
properties: {
|
|
2572
2645
|
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2573
|
-
path: { type: "string", description: "Directory path to list (default: /)" }
|
|
2646
|
+
path: { type: "string", description: "Directory path to list (default: /)" },
|
|
2647
|
+
recursive: { type: "boolean", description: "Walk into subdirectories (default false)" },
|
|
2648
|
+
maxDepth: { type: "number", description: "Maximum recursion depth (1-20, default 5)" },
|
|
2649
|
+
pattern: { type: "string", description: 'Glob pattern to filter filenames (e.g. "*.conf", "wp-*.php"). Matches basename only.' },
|
|
2650
|
+
maxResults: { type: "number", description: "Cap total entries returned (default 5000, max 50000)" }
|
|
2574
2651
|
},
|
|
2575
2652
|
required: ["serverId"]
|
|
2576
2653
|
}
|
|
@@ -2615,28 +2692,66 @@ var TOOLS = [
|
|
|
2615
2692
|
},
|
|
2616
2693
|
{
|
|
2617
2694
|
name: "docker-list",
|
|
2618
|
-
description: "List
|
|
2695
|
+
description: "List Docker containers on a remote server. Adds the docker-compose project label as the last column so you can immediately see which compose project a container belongs to.",
|
|
2619
2696
|
inputSchema: {
|
|
2620
2697
|
type: "object",
|
|
2621
2698
|
properties: {
|
|
2622
|
-
serverId: { type: "string", description: "UUID of the SSH server" }
|
|
2699
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2700
|
+
format: { type: "string", enum: ["table", "json"], description: "Output format: human table (default) or NDJSON (one JSON object per line, includes labels)." },
|
|
2701
|
+
composeOnly: { type: "boolean", description: "Only show containers that have a docker-compose project label." }
|
|
2623
2702
|
},
|
|
2624
2703
|
required: ["serverId"]
|
|
2625
2704
|
}
|
|
2626
2705
|
},
|
|
2627
2706
|
{
|
|
2628
2707
|
name: "docker-logs",
|
|
2629
|
-
description: "Get
|
|
2708
|
+
description: "Get logs from a Docker container. Supports time-window (`since`) and server-side `grep` to keep responses small. Always merges stderr into stdout.",
|
|
2630
2709
|
inputSchema: {
|
|
2631
2710
|
type: "object",
|
|
2632
2711
|
properties: {
|
|
2633
2712
|
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2634
2713
|
containerName: { type: "string", description: "Container name or ID" },
|
|
2635
|
-
lines: { type: "number", description: "Number of log lines to retrieve (default: 100)" }
|
|
2714
|
+
lines: { type: "number", description: "Number of log lines to retrieve (default: 100)" },
|
|
2715
|
+
since: { type: "string", description: 'Time window, e.g. "10m", "2h", "24h", or an absolute "2026-05-09T10:00:00".' },
|
|
2716
|
+
grep: { type: "string", description: "Case-insensitive regex/literal filter applied server-side (saves tokens for noisy containers)." }
|
|
2636
2717
|
},
|
|
2637
2718
|
required: ["serverId", "containerName"]
|
|
2638
2719
|
}
|
|
2639
2720
|
},
|
|
2721
|
+
{
|
|
2722
|
+
name: "docker-exec",
|
|
2723
|
+
description: 'Run a command inside a running Docker container. `args[]` are quoted safely (no shell-escape hell with $ or quotes). Optional `stdin` pipes data into the container process (for SQL, scripts, etc.). Use this instead of `ssh-execute "docker exec ..."`.',
|
|
2724
|
+
inputSchema: {
|
|
2725
|
+
type: "object",
|
|
2726
|
+
properties: {
|
|
2727
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2728
|
+
container: { type: "string", description: "Container name or ID" },
|
|
2729
|
+
command: { type: "string", description: 'Program to run inside the container (e.g. "psql", "wp", "node").' },
|
|
2730
|
+
args: { type: "array", items: { type: "string" }, description: "Argument list, each safely quoted." },
|
|
2731
|
+
stdin: { type: "string", description: "Optional data piped to the container process stdin." },
|
|
2732
|
+
workdir: { type: "string", description: "Working directory inside the container (-w)." },
|
|
2733
|
+
user: { type: "string", description: "User to run as inside the container (-u)." },
|
|
2734
|
+
timeout: { type: "number", description: "Timeout in milliseconds (default: 60000)" }
|
|
2735
|
+
},
|
|
2736
|
+
required: ["serverId", "container", "command"]
|
|
2737
|
+
}
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
name: "docker-compose",
|
|
2741
|
+
description: 'Run a docker-compose action against a project on a remote server. Replaces the common `ssh-execute "cd /opt/x && docker compose ..."` pattern. Action enum keeps the surface tiny.',
|
|
2742
|
+
inputSchema: {
|
|
2743
|
+
type: "object",
|
|
2744
|
+
properties: {
|
|
2745
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
2746
|
+
projectPath: { type: "string", description: "Absolute path to the directory containing docker-compose.yml." },
|
|
2747
|
+
action: { type: "string", enum: ["up", "down", "restart", "logs", "ps", "pull", "build"], description: "Compose action." },
|
|
2748
|
+
service: { type: "string", description: 'Optional service name to scope the action to (e.g. "studio"). Omit to act on the whole project.' },
|
|
2749
|
+
tail: { type: "number", description: "For `logs`: number of lines (default 200)." },
|
|
2750
|
+
timeout: { type: "number", description: "Timeout in milliseconds (default: 120000 \u2014 compose ops can be slow)." }
|
|
2751
|
+
},
|
|
2752
|
+
required: ["serverId", "projectPath", "action"]
|
|
2753
|
+
}
|
|
2754
|
+
},
|
|
2640
2755
|
{
|
|
2641
2756
|
name: "db-discover",
|
|
2642
2757
|
description: "Scan /var/www on a server for web applications (WordPress, PrestaShop, Laravel, .env) and list their database credentials. Use this first to find available sites before running other db-* tools.",
|
|
@@ -2888,10 +3003,22 @@ async function executeToolCall(name, a, _serverId) {
|
|
|
2888
3003
|
case "ssh-execute": {
|
|
2889
3004
|
const command = String(a.command);
|
|
2890
3005
|
assertSafeCommand(command);
|
|
2891
|
-
const
|
|
3006
|
+
const args2 = Array.isArray(a.args) ? a.args.map(String) : void 0;
|
|
3007
|
+
const stdin = typeof a.stdin === "string" ? a.stdin : void 0;
|
|
3008
|
+
const shellOverride = typeof a.shell === "string" ? a.shell : "auto";
|
|
3009
|
+
const { conn, proxy, os } = await getServerConnection(String(a.serverId));
|
|
2892
3010
|
if (a.timeout) conn.timeout = Number(a.timeout);
|
|
2893
|
-
const
|
|
2894
|
-
|
|
3011
|
+
const shell = shellOverride === "auto" ? os === "windows" ? "powershell" : "bash" : shellOverride;
|
|
3012
|
+
let finalCmd;
|
|
3013
|
+
if (args2 && args2.length > 0) {
|
|
3014
|
+
finalCmd = shell === "powershell" ? buildPowerShellEncodedCommand(command, args2) : buildPosixCommand(command, args2);
|
|
3015
|
+
} else if (shell === "powershell" && !/^powershell\b/i.test(command.trim())) {
|
|
3016
|
+
finalCmd = buildPowerShellEncodedCommand(command, []);
|
|
3017
|
+
} else {
|
|
3018
|
+
finalCmd = command;
|
|
3019
|
+
}
|
|
3020
|
+
const result = await sshExec(conn, finalCmd, proxy, stdin !== void 0 ? { stdin } : void 0);
|
|
3021
|
+
const output = [`Exit code: ${result.exitCode} (os: ${os}, shell: ${shell})`];
|
|
2895
3022
|
if (result.stdout) output.push(`--- stdout ---
|
|
2896
3023
|
${result.stdout}`);
|
|
2897
3024
|
if (result.stderr) output.push(`--- stderr ---
|
|
@@ -2901,7 +3028,12 @@ ${result.stderr}`);
|
|
|
2901
3028
|
// ----- SFTP -----
|
|
2902
3029
|
case "sftp-list": {
|
|
2903
3030
|
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2904
|
-
const listing = await sftpReaddir(conn, String(a.path || "/"), proxy
|
|
3031
|
+
const listing = await sftpReaddir(conn, String(a.path || "/"), proxy, {
|
|
3032
|
+
recursive: a.recursive === true,
|
|
3033
|
+
maxDepth: typeof a.maxDepth === "number" ? a.maxDepth : void 0,
|
|
3034
|
+
pattern: typeof a.pattern === "string" ? a.pattern : void 0,
|
|
3035
|
+
maxResults: typeof a.maxResults === "number" ? a.maxResults : void 0
|
|
3036
|
+
});
|
|
2905
3037
|
return { content: [{ type: "text", text: listing }] };
|
|
2906
3038
|
}
|
|
2907
3039
|
case "sftp-read": {
|
|
@@ -2922,16 +3054,110 @@ ${result.stderr}`);
|
|
|
2922
3054
|
}
|
|
2923
3055
|
// ----- Docker -----
|
|
2924
3056
|
case "docker-list": {
|
|
3057
|
+
const format = a.format === "json" ? "json" : "table";
|
|
3058
|
+
const composeOnly = a.composeOnly === true;
|
|
3059
|
+
const filterArg = composeOnly ? ' --filter "label=com.docker.compose.project"' : "";
|
|
3060
|
+
const fmtArg = format === "json" ? ` --format '{{json .}}'` : (
|
|
3061
|
+
// Add the compose project as the last column. Docker exposes
|
|
3062
|
+
// labels through the `.Label "key"` method (Labels itself is a
|
|
3063
|
+
// string slice "k=v,k=v" in docker ps, not a map — `index` fails).
|
|
3064
|
+
` --format 'table {{.Names}} {{.Image}} {{.Status}} {{.Ports}} {{.Label "com.docker.compose.project"}}'`
|
|
3065
|
+
);
|
|
3066
|
+
const cmd = `docker ps -a${filterArg}${fmtArg}`;
|
|
2925
3067
|
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2926
|
-
const result = await sshExec(conn,
|
|
3068
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
2927
3069
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
2928
3070
|
}
|
|
2929
3071
|
case "docker-logs": {
|
|
2930
3072
|
const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
2931
3073
|
const lines = Number(a.lines) || 100;
|
|
3074
|
+
const sinceRaw = typeof a.since === "string" ? a.since.trim() : "";
|
|
3075
|
+
const grepRaw = typeof a.grep === "string" ? a.grep : "";
|
|
3076
|
+
let sinceArg = "";
|
|
3077
|
+
if (sinceRaw) {
|
|
3078
|
+
if (!/^\d+[smhd]$/i.test(sinceRaw) && !/^\d{4}-\d{2}-\d{2}/.test(sinceRaw)) {
|
|
3079
|
+
return { content: [{ type: "text", text: 'Error: invalid `since` format (expected e.g. "10m", "2h", or ISO timestamp)' }] };
|
|
3080
|
+
}
|
|
3081
|
+
sinceArg = ` --since ${posixQuote(sinceRaw)}`;
|
|
3082
|
+
}
|
|
3083
|
+
const grepSuffix = grepRaw ? ` | grep -i -E ${posixQuote(grepRaw)}` : "";
|
|
3084
|
+
const cmd = `docker logs --tail ${lines}${sinceArg} ${container} 2>&1${grepSuffix}`;
|
|
2932
3085
|
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
2933
|
-
const result = await sshExec(conn,
|
|
2934
|
-
|
|
3086
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
3087
|
+
if (result.exitCode !== 0 && !(grepRaw && result.exitCode === 1)) {
|
|
3088
|
+
return { content: [{ type: "text", text: `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` }] };
|
|
3089
|
+
}
|
|
3090
|
+
return { content: [{ type: "text", text: result.stdout || "(no log lines matched)" }] };
|
|
3091
|
+
}
|
|
3092
|
+
case "docker-exec": {
|
|
3093
|
+
const container = String(a.container).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
3094
|
+
if (!container) return { content: [{ type: "text", text: "Error: invalid container name" }] };
|
|
3095
|
+
const command = String(a.command);
|
|
3096
|
+
const args2 = Array.isArray(a.args) ? a.args.map(String) : [];
|
|
3097
|
+
const stdin = typeof a.stdin === "string" ? a.stdin : void 0;
|
|
3098
|
+
const workdir = typeof a.workdir === "string" && a.workdir ? ["-w", a.workdir] : [];
|
|
3099
|
+
const user = typeof a.user === "string" && a.user ? ["-u", a.user] : [];
|
|
3100
|
+
const stdinFlag = stdin !== void 0 ? ["-i"] : [];
|
|
3101
|
+
const dockerArgs = [...stdinFlag, ...workdir, ...user, container, command, ...args2];
|
|
3102
|
+
const fullCmd = buildPosixCommand("docker", ["exec", ...dockerArgs]);
|
|
3103
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
3104
|
+
if (a.timeout) conn.timeout = Number(a.timeout);
|
|
3105
|
+
const result = await sshExec(conn, fullCmd, proxy, stdin !== void 0 ? { stdin } : void 0);
|
|
3106
|
+
const output = [`Exit code: ${result.exitCode}`];
|
|
3107
|
+
if (result.stdout) output.push(`--- stdout ---
|
|
3108
|
+
${result.stdout}`);
|
|
3109
|
+
if (result.stderr) output.push(`--- stderr ---
|
|
3110
|
+
${result.stderr}`);
|
|
3111
|
+
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
3112
|
+
}
|
|
3113
|
+
case "docker-compose": {
|
|
3114
|
+
const projectPath = String(a.projectPath);
|
|
3115
|
+
if (!projectPath || !projectPath.startsWith("/")) {
|
|
3116
|
+
return { content: [{ type: "text", text: "Error: projectPath must be an absolute path" }] };
|
|
3117
|
+
}
|
|
3118
|
+
const action = String(a.action);
|
|
3119
|
+
const allowedActions = ["up", "down", "restart", "logs", "ps", "pull", "build"];
|
|
3120
|
+
if (!allowedActions.includes(action)) {
|
|
3121
|
+
return { content: [{ type: "text", text: `Error: invalid action (allowed: ${allowedActions.join(", ")})` }] };
|
|
3122
|
+
}
|
|
3123
|
+
const service = typeof a.service === "string" && a.service ? a.service : "";
|
|
3124
|
+
const tail = Number(a.tail) > 0 ? Number(a.tail) : 200;
|
|
3125
|
+
let composeArgs;
|
|
3126
|
+
switch (action) {
|
|
3127
|
+
case "up":
|
|
3128
|
+
composeArgs = service ? ["up", "-d", service] : ["up", "-d"];
|
|
3129
|
+
break;
|
|
3130
|
+
case "down":
|
|
3131
|
+
composeArgs = service ? ["down", service] : ["down"];
|
|
3132
|
+
break;
|
|
3133
|
+
case "restart":
|
|
3134
|
+
composeArgs = service ? ["restart", service] : ["restart"];
|
|
3135
|
+
break;
|
|
3136
|
+
case "logs":
|
|
3137
|
+
composeArgs = service ? ["logs", "--no-color", `--tail=${tail}`, service] : ["logs", "--no-color", `--tail=${tail}`];
|
|
3138
|
+
break;
|
|
3139
|
+
case "ps":
|
|
3140
|
+
composeArgs = service ? ["ps", service] : ["ps"];
|
|
3141
|
+
break;
|
|
3142
|
+
case "pull":
|
|
3143
|
+
composeArgs = service ? ["pull", service] : ["pull"];
|
|
3144
|
+
break;
|
|
3145
|
+
case "build":
|
|
3146
|
+
composeArgs = service ? ["build", service] : ["build"];
|
|
3147
|
+
break;
|
|
3148
|
+
default:
|
|
3149
|
+
composeArgs = [];
|
|
3150
|
+
}
|
|
3151
|
+
const composeCmd = buildPosixCommand("docker", ["compose", ...composeArgs]);
|
|
3152
|
+
const fullCmd = `cd ${posixQuote(projectPath)} && ${composeCmd} 2>&1`;
|
|
3153
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
3154
|
+
conn.timeout = Number(a.timeout) > 0 ? Number(a.timeout) : 12e4;
|
|
3155
|
+
const result = await sshExec(conn, fullCmd, proxy);
|
|
3156
|
+
const output = [`Exit code: ${result.exitCode} (compose ${action}${service ? ` ${service}` : ""} @ ${projectPath})`];
|
|
3157
|
+
if (result.stdout) output.push(result.stdout);
|
|
3158
|
+
if (result.stderr) output.push(`--- stderr ---
|
|
3159
|
+
${result.stderr}`);
|
|
3160
|
+
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
2935
3161
|
}
|
|
2936
3162
|
// ----- Database -----
|
|
2937
3163
|
case "db-discover": {
|