@@ -116,7 +151,17 @@ export const ShowServers = () => {
-
+
diff --git a/apps/dokploy/components/dashboard/settings/servers/show-traefik-file-system-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-traefik-file-system-modal.tsx
new file mode 100644
index 00000000..cff9d214
--- /dev/null
+++ b/apps/dokploy/components/dashboard/settings/servers/show-traefik-file-system-modal.tsx
@@ -0,0 +1,48 @@
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
+import { FileTextIcon } from "lucide-react";
+import { useState } from "react";
+import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
+
+interface Props {
+ serverId: string;
+}
+
+export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx
index 2e6eede6..7fc4166b 100644
--- a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx
@@ -33,12 +33,15 @@ type Schema = z.infer
;
interface Props {
children?: React.ReactNode;
+ serverId?: string;
}
-export const EditTraefikEnv = ({ children }: Props) => {
+export const EditTraefikEnv = ({ children, serverId }: Props) => {
const [canEdit, setCanEdit] = useState(true);
- const { data } = api.settings.readTraefikEnv.useQuery();
+ const { data } = api.settings.readTraefikEnv.useQuery({
+ serverId,
+ });
const { mutateAsync, isLoading, error, isError } =
api.settings.writeTraefikEnv.useMutation();
diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
index 0956da10..607ff7b2 100644
--- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
@@ -36,12 +36,14 @@ export const DockerLogsId = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
+ serverId?: string;
}
-export const ShowModalLogs = ({ appName, children }: Props) => {
+export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
{
appName,
+ serverId,
},
{
enabled: !!appName,
@@ -96,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
-
+
diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts
index 54fe00db..27671310 100644
--- a/apps/dokploy/server/api/routers/docker.ts
+++ b/apps/dokploy/server/api/routers/docker.ts
@@ -55,9 +55,10 @@ export const dockerRouter = createTRPCRouter({
.input(
z.object({
appName: z.string().min(1),
+ serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
- return await getContainersByAppLabel(input.appName);
+ return await getContainersByAppLabel(input.appName, input.serverId);
}),
});
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index f47edab5..878882b9 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -6,6 +6,7 @@ import {
apiReadStatsLogs,
apiReadTraefikConfig,
apiSaveSSHKey,
+ apiStorage,
apiTraefikConfig,
apiUpdateDockerCleanup,
} from "@/server/db/schema";
@@ -20,11 +21,13 @@ import {
cleanUpUnusedVolumes,
prepareEnvironmentVariables,
startService,
+ startServiceRemote,
stopService,
+ stopServiceRemote,
} from "@/server/utils/docker/utils";
import { recreateDirectory } from "@/server/utils/filesystem/directory";
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
-import { execAsync } from "@/server/utils/process/execAsync";
+import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
import { spawnAsync } from "@/server/utils/process/spawnAsync";
import {
readConfig,
@@ -63,16 +66,23 @@ export const settingsRouter = createTRPCRouter({
await execAsync(`docker service update --force ${stdout.trim()}`);
return true;
}),
- reloadTraefik: adminProcedure.mutation(async () => {
- try {
- await stopService("dokploy-traefik");
- await startService("dokploy-traefik");
- } catch (err) {
- console.error(err);
- }
+ reloadTraefik: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ try {
+ if (input?.serverId) {
+ await stopServiceRemote(input.serverId, "dokploy-traefik");
+ await startServiceRemote(input.serverId, "dokploy-traefik");
+ } else {
+ await stopService("dokploy-traefik");
+ await startService("dokploy-traefik");
+ }
+ } catch (err) {
+ console.error(err);
+ }
- return true;
- }),
+ return true;
+ }),
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
@@ -82,31 +92,42 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
- cleanUnusedImages: adminProcedure.mutation(async () => {
- await cleanUpUnusedImages();
- return true;
- }),
- cleanUnusedVolumes: adminProcedure.mutation(async () => {
- await cleanUpUnusedVolumes();
- return true;
- }),
- cleanStoppedContainers: adminProcedure.mutation(async () => {
- await cleanStoppedContainers();
- return true;
- }),
- cleanDockerBuilder: adminProcedure.mutation(async () => {
- await cleanUpDockerBuilder();
- }),
- cleanDockerPrune: adminProcedure.mutation(async () => {
- await cleanUpSystemPrune();
- await cleanUpDockerBuilder();
+ cleanUnusedImages: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ await cleanUpUnusedImages(input?.serverId);
+ return true;
+ }),
+ cleanUnusedVolumes: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ await cleanUpUnusedVolumes(input?.serverId);
+ return true;
+ }),
+ cleanStoppedContainers: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ await cleanStoppedContainers(input?.serverId);
+ return true;
+ }),
+ cleanDockerBuilder: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ await cleanUpDockerBuilder(input?.serverId);
+ }),
+ cleanDockerPrune: adminProcedure
+ .input(apiStorage)
+ .mutation(async ({ input }) => {
+ await cleanUpSystemPrune(input?.serverId);
+ await cleanUpDockerBuilder(input?.serverId);
- return true;
- }),
- cleanAll: adminProcedure.mutation(async () => {
- await cleanUpUnusedImages();
- await cleanUpDockerBuilder();
- await cleanUpSystemPrune();
+ return true;
+ }),
+ cleanAll: adminProcedure.input(apiStorage).mutation(async ({ input }) => {
+ await cleanUpUnusedImages(input?.serverId);
+ await cleanStoppedContainers(input?.serverId);
+ await cleanUpDockerBuilder(input?.serverId);
+ await cleanUpSystemPrune(input?.serverId);
return true;
}),
@@ -230,18 +251,20 @@ export const settingsRouter = createTRPCRouter({
getDokployVersion: adminProcedure.query(() => {
return getDokployVersion();
}),
- readDirectories: protectedProcedure.query(async ({ ctx }) => {
- if (ctx.user.rol === "user") {
- const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
+ readDirectories: protectedProcedure
+ .input(apiStorage)
+ .query(async ({ ctx, input }) => {
+ if (ctx.user.rol === "user") {
+ const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
- if (!canAccess) {
- throw new TRPCError({ code: "UNAUTHORIZED" });
+ if (!canAccess) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
}
- }
- const { MAIN_TRAEFIK_PATH } = paths();
- const result = readDirectory(MAIN_TRAEFIK_PATH);
- return result || [];
- }),
+ const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
+ const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
+ return result || [];
+ }),
updateTraefikFile: protectedProcedure
.input(apiModifyTraefikConfig)
@@ -253,7 +276,11 @@ export const settingsRouter = createTRPCRouter({
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
- writeTraefikConfigInPath(input.path, input.traefikConfig);
+ await writeTraefikConfigInPath(
+ input.path,
+ input.traefikConfig,
+ input?.serverId,
+ );
return true;
}),
@@ -267,7 +294,7 @@ export const settingsRouter = createTRPCRouter({
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
- return readConfigInPath(input.path);
+ return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async () => {
const admin = await findAdmin();
@@ -324,16 +351,20 @@ export const settingsRouter = createTRPCRouter({
return openApiDocument;
},
),
- readTraefikEnv: adminProcedure.query(async () => {
- const { stdout } = await execAsync(
- "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik",
- );
+ readTraefikEnv: adminProcedure.input(apiStorage).query(async ({ input }) => {
+ const command =
+ "docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
- return stdout.trim();
+ if (input?.serverId) {
+ const result = await execAsyncRemote(input.serverId, command);
+ return result.stdout.trim();
+ }
+ const result = await execAsync(command);
+ return result.stdout.trim();
}),
writeTraefikEnv: adminProcedure
- .input(z.object({ env: z.string() }))
+ .input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
diff --git a/apps/dokploy/server/api/services/docker.ts b/apps/dokploy/server/api/services/docker.ts
index fea9a7e2..ba142ef8 100644
--- a/apps/dokploy/server/api/services/docker.ts
+++ b/apps/dokploy/server/api/services/docker.ts
@@ -126,12 +126,25 @@ export const getContainersByAppNameMatch = async (
return [];
};
-export const getContainersByAppLabel = async (appName: string) => {
+export const getContainersByAppLabel = async (
+ appName: string,
+ serverId?: string,
+) => {
try {
- const { stdout, stderr } = await execAsync(
- `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`,
- );
+ let stdout = "";
+ let stderr = "";
+ const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
+ console.log(command);
+ if (serverId) {
+ const result = await execAsyncRemote(serverId, command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ } else {
+ const result = await execAsync(command);
+ stdout = result.stdout;
+ stderr = result.stderr;
+ }
if (stderr) {
console.error(`Error: ${stderr}`);
return;
diff --git a/apps/dokploy/server/api/services/settings.ts b/apps/dokploy/server/api/services/settings.ts
index a3f658d9..bdb5fc1f 100644
--- a/apps/dokploy/server/api/services/settings.ts
+++ b/apps/dokploy/server/api/services/settings.ts
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { docker } from "@/server/constants";
import { getServiceContainer } from "@/server/utils/docker/utils";
import packageInfo from "../../../package.json";
+import { execAsyncRemote } from "@/server/utils/process/execAsync";
const updateIsAvailable = async () => {
try {
@@ -44,8 +45,66 @@ interface TreeDataItem {
children?: TreeDataItem[];
}
-export const readDirectory = (dirPath: string): TreeDataItem[] => {
+export const readDirectory = async (
+ dirPath: string,
+ serverId?: string,
+): Promise
=> {
+ if (serverId) {
+ const { stdout } = await execAsyncRemote(
+ serverId,
+ `
+process_items() {
+ local parent_dir="$1"
+ local __resultvar=$2
+
+ local items_json=""
+ local first=true
+ for item in "$parent_dir"/*; do
+ [ -e "$item" ] || continue
+ process_item "$item" item_json
+ if [ "$first" = true ]; then
+ first=false
+ items_json="$item_json"
+ else
+ items_json="$items_json,$item_json"
+ fi
+ done
+
+ eval $__resultvar="'[$items_json]'"
+}
+
+process_item() {
+ local item_path="$1"
+ local __resultvar=$2
+
+ local item_name=$(basename "$item_path")
+ local escaped_name=$(echo "$item_name" | sed 's/"/\\"/g')
+ local escaped_path=$(echo "$item_path" | sed 's/"/\\"/g')
+
+ if [ -d "$item_path" ]; then
+ # Is directory
+ process_items "$item_path" children_json
+ local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"directory","children":'"$children_json"'}'
+ else
+ # Is file
+ local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"file"}'
+ fi
+
+ eval $__resultvar="'$json'"
+}
+
+root_dir=${dirPath}
+
+process_items "$root_dir" json_output
+
+echo "$json_output"
+ `,
+ );
+ const result = JSON.parse(stdout);
+ return result;
+ }
const items = readdirSync(dirPath, { withFileTypes: true });
+
return items.map((item) => {
const fullPath = join(dirPath, item.name);
if (item.isDirectory()) {
@@ -61,5 +120,5 @@ export const readDirectory = (dirPath: string): TreeDataItem[] => {
name: item.name,
type: "file",
};
- });
+ }) as unknown as Promise;
};
diff --git a/apps/dokploy/server/db/schema/admin.ts b/apps/dokploy/server/db/schema/admin.ts
index 957db8a7..61ac0f20 100644
--- a/apps/dokploy/server/db/schema/admin.ts
+++ b/apps/dokploy/server/db/schema/admin.ts
@@ -72,15 +72,24 @@ export const apiTraefikConfig = z.object({
export const apiModifyTraefikConfig = z.object({
path: z.string().min(1),
traefikConfig: z.string().min(1),
+ serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
path: z.string().min(1),
+ serverId: z.string().optional(),
});
export const apiEnableDashboard = z.object({
enableDashboard: z.boolean().optional(),
+ serverId: z.string().optional(),
});
+export const apiStorage = z
+ .object({
+ serverId: z.string().optional(),
+ })
+ .optional();
+
export const apiReadStatsLogs = z.object({
page: z
.object({
diff --git a/apps/dokploy/server/utils/docker/utils.ts b/apps/dokploy/server/utils/docker/utils.ts
index e4e8ca17..3dcc27ac 100644
--- a/apps/dokploy/server/utils/docker/utils.ts
+++ b/apps/dokploy/server/utils/docker/utils.ts
@@ -149,27 +149,39 @@ export const getContainerByName = (name: string): Promise => {
});
});
};
-export const cleanUpUnusedImages = async () => {
+export const cleanUpUnusedImages = async (serverId?: string) => {
try {
- await execAsync("docker image prune --all --force");
+ if (serverId) {
+ await execAsyncRemote(serverId, "docker image prune --all --force");
+ } else {
+ await execAsync("docker image prune --all --force");
+ }
} catch (error) {
console.error(error);
throw error;
}
};
-export const cleanStoppedContainers = async () => {
+export const cleanStoppedContainers = async (serverId?: string) => {
try {
- await execAsync("docker container prune --force");
+ if (serverId) {
+ await execAsyncRemote(serverId, "docker container prune --force");
+ } else {
+ await execAsync("docker container prune --force");
+ }
} catch (error) {
console.error(error);
throw error;
}
};
-export const cleanUpUnusedVolumes = async () => {
+export const cleanUpUnusedVolumes = async (serverId?: string) => {
try {
- await execAsync("docker volume prune --all --force");
+ if (serverId) {
+ await execAsyncRemote(serverId, "docker volume prune --all --force");
+ } else {
+ await execAsync("docker volume prune --all --force");
+ }
} catch (error) {
console.error(error);
throw error;
@@ -193,12 +205,23 @@ export const cleanUpInactiveContainers = async () => {
}
};
-export const cleanUpDockerBuilder = async () => {
- await execAsync("docker builder prune --all --force");
+export const cleanUpDockerBuilder = async (serverId?: string) => {
+ if (serverId) {
+ await execAsyncRemote(serverId, "docker builder prune --all --force");
+ } else {
+ await execAsync("docker builder prune --all --force");
+ }
};
-export const cleanUpSystemPrune = async () => {
- await execAsync("docker system prune --all --force --volumes");
+export const cleanUpSystemPrune = async (serverId?: string) => {
+ if (serverId) {
+ await execAsyncRemote(
+ serverId,
+ "docker system prune --all --force --volumes",
+ );
+ } else {
+ await execAsync("docker system prune --all --force --volumes");
+ }
};
export const startService = async (appName: string) => {
diff --git a/apps/dokploy/server/utils/traefik/application.ts b/apps/dokploy/server/utils/traefik/application.ts
index 86185dc6..d40f447d 100644
--- a/apps/dokploy/server/utils/traefik/application.ts
+++ b/apps/dokploy/server/utils/traefik/application.ts
@@ -5,6 +5,7 @@ import { paths } from "@/server/constants";
import { dump, load } from "js-yaml";
import { execAsyncRemote } from "../process/execAsync";
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
+import { encodeBase64 } from "../docker/utils";
export const createTraefikConfig = (appName: string) => {
const defaultPort = 3000;
@@ -146,8 +147,14 @@ export const readMonitoringConfig = () => {
return null;
};
-export const readConfigInPath = (pathFile: string) => {
+export const readConfigInPath = async (pathFile: string, serverId?: string) => {
const configPath = path.join(pathFile);
+
+ if (serverId) {
+ const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
+ if (!stdout) return null;
+ return stdout;
+ }
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
@@ -179,12 +186,22 @@ export const writeConfigRemote = async (
}
};
-export const writeTraefikConfigInPath = (
+export const writeTraefikConfigInPath = async (
pathFile: string,
traefikConfig: string,
+ serverId?: string,
) => {
try {
const configPath = path.join(pathFile);
+ if (serverId) {
+ const encoded = encodeBase64(traefikConfig);
+ await execAsyncRemote(
+ serverId,
+ `echo "${encoded}" | base64 -d > "${configPath}"`,
+ );
+ } else {
+ fs.writeFileSync(configPath, traefikConfig, "utf8");
+ }
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts
index 3af9ad2b..ad7c8137 100644
--- a/apps/dokploy/server/wss/docker-container-terminal.ts
+++ b/apps/dokploy/server/wss/docker-container-terminal.ts
@@ -3,6 +3,9 @@ import { spawn } from "node-pty";
import { WebSocketServer } from "ws";
import { validateWebSocketRequest } from "../auth/auth";
import { getShell } from "./utils";
+import { Client } from "ssh2";
+import { readSSHKey } from "../utils/filesystem/ssh";
+import { findServerById } from "../api/services/server";
export const setupDockerContainerTerminalWebSocketServer = (
server: http.Server,
@@ -44,7 +47,123 @@ export const setupDockerContainerTerminalWebSocketServer = (
}
try {
if (serverId) {
- // const server = await findServerById(serverId);
+ const server = await findServerById(serverId);
+ if (!server.sshKeyId)
+ throw new Error("No SSH key available for this server");
+
+ const keys = await readSSHKey(server.sshKeyId);
+ const conn = new Client();
+ let stdout = "";
+ let stderr = "";
+ // conn
+ // .once("ready", () => {
+ // console.log("Client :: ready");
+ // conn.exec(
+ // `docker run -i ${containerId} ${activeWay}`,
+ // (err, stream) => {
+ // if (err) throw err;
+
+ // stream
+ // .on("close", (code: number, signal: string) => {
+ // console.log(
+ // `Stream :: close :: code: ${code}, signal: ${signal}`,
+ // );
+ // conn.end();
+ // })
+ // .on("data", (data: string) => {
+ // stdout += data.toString();
+ // ws.send(data.toString());
+ // })
+ // .stderr.on("data", (data) => {
+ // stderr += data.toString();
+ // ws.send(data.toString());
+ // console.error("Error: ", data.toString());
+ // });
+
+ // // Maneja la entrada de comandos desde WebSocket
+ // ws.on("message", (message) => {
+ // try {
+ // let command: string | Buffer[] | Buffer | ArrayBuffer;
+ // if (Buffer.isBuffer(message)) {
+ // command = message.toString("utf8");
+ // } else {
+ // command = message;
+ // }
+ // stream.write(command.toString());
+ // } catch (error) {
+ // // @ts-ignore
+ // const errorMessage = error?.message as unknown as string;
+ // ws.send(errorMessage);
+ // }
+ // });
+
+ // // Cuando se cierra la conexión WebSocket
+ // ws.on("close", () => {
+ // stream.end();
+ // });
+ // },
+ // );
+ // })p
+ // .connect({
+ // host: server.ipAddress,
+ // port: server.port,
+ // username: server.username,
+ // privateKey: keys.privateKey,
+ // timeout: 99999,
+ // });
+ // conn
+ // .once("ready", () => {
+ // console.log("Client :: ready");
+ // conn.shell((err, stream) => {
+ // if (err) throw err;
+
+ // stream
+ // .on("close", (code: number, signal: string) => {
+ // console.log(
+ // `Stream :: close :: code: ${code}, signal: ${signal}`,
+ // );
+ // conn.end();
+ // })
+ // .on("data", (data: string) => {
+ // stdout += data.toString();
+ // ws.send(data.toString());
+ // })
+ // .stderr.on("data", (data) => {
+ // stderr += data.toString();
+ // ws.send(data.toString());
+ // console.error("Error: ", data.toString());
+ // });
+ // stream.write(`docker exec -it ${containerId} ${activeWay}\n`);
+ // // Maneja la entrada de comandos desde WebSocket
+ // ws.on("message", (message) => {
+ // try {
+ // let command: string | Buffer[] | Buffer | ArrayBuffer;
+ // if (Buffer.isBuffer(message)) {
+ // command = message.toString("utf8");
+ // } else {
+ // command = message;
+ // }
+ // stream.write(command.toString());
+ // } catch (error) {
+ // // @ts-ignore
+ // const errorMessage = error?.message as unknown as string;
+ // ws.send(errorMessage);
+ // }
+ // });
+
+ // // Cuando se cierra la conexión WebSocket
+ // ws.on("close", () => {
+ // stream.end();
+ // });
+ // });
+ // })
+ // .connect({
+ // host: server.ipAddress,
+ // port: server.port,
+ // username: server.username,
+ // privateKey: keys.privateKey,
+ // timeout: 99999,
+ // });
} else {
const shell = getShell();
const ptyProcess = spawn(