diff --git a/src/Projects/WebSocketCLI/cli-client.html b/src/Projects/WebSocketCLI/cli-client.html
new file mode 100644
index 00000000..0c2fc7db
--- /dev/null
+++ b/src/Projects/WebSocketCLI/cli-client.html
@@ -0,0 +1,156 @@
+
+
+
+ WebSocket CLI
+
+
+
+ WebSocket CLI Runner
+
+
+
+ Disconnected
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Projects/WebSocketCLI/cli-client.py b/src/Projects/WebSocketCLI/cli-client.py
new file mode 100644
index 00000000..dacc518a
--- /dev/null
+++ b/src/Projects/WebSocketCLI/cli-client.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+"""
+WebSocket CLI Client
+Usage: python cli-client.py [server-url]
+Default: ws://localhost:8080/cli
+"""
+
+import asyncio
+import websockets
+import json
+import sys
+
+DEFAULT_URL = "ws://localhost:8080/cli"
+
+async def cli_client(url):
+ print(f"Connecting to {url}...")
+
+ try:
+ async with websockets.connect(url) as ws:
+ print("Connected! Type commands (or 'exit' to quit)\n")
+
+ # Task to receive messages
+ async def receiver():
+ try:
+ async for message in ws:
+ data = json.loads(message)
+ msg_type = data.get('type', '')
+ content = data.get('data', '')
+
+ if msg_type == 'output':
+ print(f" {content}")
+ elif msg_type == 'completed':
+ print(f"\n[Done]\n> ", end='', flush=True)
+ elif msg_type == 'error':
+ print(f"[ERROR] {content}")
+ elif msg_type == 'connected':
+ print(f"[Server] {content}\n> ", end='', flush=True)
+ else:
+ print(f"[{msg_type}] {content}")
+ except websockets.exceptions.ConnectionClosed:
+ print("\nConnection closed")
+
+ # Start receiver task
+ recv_task = asyncio.create_task(receiver())
+
+ # Input loop
+ loop = asyncio.get_event_loop()
+ while True:
+ try:
+ cmd = await loop.run_in_executor(None, input, "")
+
+ if cmd.lower() == 'exit':
+ print("Bye!")
+ break
+
+ if cmd.strip():
+ await ws.send(json.dumps({"command": cmd}))
+
+ except EOFError:
+ break
+
+ recv_task.cancel()
+
+ except ConnectionRefusedError:
+ print(f"Cannot connect to {url}. Is the server running?")
+ except Exception as e:
+ print(f"Error: {e}")
+
+if __name__ == "__main__":
+ url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL
+ asyncio.run(cli_client(url))
diff --git a/src/Projects/WebSocketCLI/pom.xml b/src/Projects/WebSocketCLI/pom.xml
new file mode 100644
index 00000000..0c6f566f
--- /dev/null
+++ b/src/Projects/WebSocketCLI/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.0
+
+
+
+ org.csystem
+ websocket-cli
+ 1.0.0
+ WebSocket CLI Runner
+ WebSocket based remote CLI runner
+
+
+ 17
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java
new file mode 100644
index 00000000..d2d6aba9
--- /dev/null
+++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/Application.java
@@ -0,0 +1,12 @@
+package org.csystem.app;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ System.out.println("WebSocket CLI Runner started on ws://localhost:8080/cli");
+ }
+}
diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java
new file mode 100644
index 00000000..9f2e17ae
--- /dev/null
+++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/config/WebSocketConfig.java
@@ -0,0 +1,35 @@
+package org.csystem.app.config;
+
+import org.csystem.app.handler.CLIWebSocketHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig implements WebSocketConfigurer {
+
+ private final CLIWebSocketHandler cliWebSocketHandler;
+
+ public WebSocketConfig(CLIWebSocketHandler cliWebSocketHandler) {
+ this.cliWebSocketHandler = cliWebSocketHandler;
+ }
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry.addHandler(cliWebSocketHandler, "/cli")
+ .setAllowedOrigins("*");
+ }
+
+ @Bean
+ public ServletServerContainerFactoryBean createWebSocketContainer() {
+ ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
+ container.setMaxTextMessageBufferSize(65536);
+ container.setMaxBinaryMessageBufferSize(65536);
+ container.setMaxSessionIdleTimeout(600000L);
+ return container;
+ }
+}
diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java
new file mode 100644
index 00000000..b336dc88
--- /dev/null
+++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/handler/CLIWebSocketHandler.java
@@ -0,0 +1,88 @@
+package org.csystem.app.handler;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.csystem.app.service.CommandExecutorService;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class CLIWebSocketHandler extends TextWebSocketHandler {
+
+ private final CommandExecutorService commandExecutorService;
+ private final Gson gson = new Gson();
+ private final Map sessions = new ConcurrentHashMap<>();
+
+ public CLIWebSocketHandler(CommandExecutorService commandExecutorService) {
+ this.commandExecutorService = commandExecutorService;
+ }
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ sessions.put(session.getId(), session);
+ sendMessage(session, "connected", "WebSocket CLI Ready. Send commands as: {\"command\": \"your-command\"}");
+ System.out.println("[+] Client connected: " + session.getId());
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+ try {
+ String payload = message.getPayload();
+ JsonObject json = gson.fromJson(payload, JsonObject.class);
+
+ if (json.has("command")) {
+ String command = json.get("command").getAsString();
+ System.out.println("[>] Executing: " + command);
+
+ sendMessage(session, "executing", "Running: " + command);
+
+ commandExecutorService.executeCommand(command, line -> {
+ sendMessage(session, "output", line);
+ }).thenAccept(result -> {
+ sendMessage(session, "completed", result);
+ System.out.println("[<] Command completed");
+ });
+
+ } else if (json.has("ping")) {
+ sendMessage(session, "pong", "alive");
+ } else {
+ sendMessage(session, "error", "Unknown command. Use {\"command\": \"...\"}");
+ }
+
+ } catch (Exception e) {
+ sendMessage(session, "error", "Parse error: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ sessions.remove(session.getId());
+ System.out.println("[-] Client disconnected: " + session.getId());
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) {
+ System.err.println("[!] Transport error: " + exception.getMessage());
+ }
+
+ private void sendMessage(WebSocketSession session, String type, String data) {
+ try {
+ if (session.isOpen()) {
+ JsonObject response = new JsonObject();
+ response.addProperty("type", type);
+ response.addProperty("data", data);
+ response.addProperty("timestamp", System.currentTimeMillis());
+ session.sendMessage(new TextMessage(gson.toJson(response)));
+ }
+ } catch (IOException e) {
+ System.err.println("[!] Failed to send message: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java
new file mode 100644
index 00000000..1b8d5245
--- /dev/null
+++ b/src/Projects/WebSocketCLI/src/main/java/org/csystem/app/service/CommandExecutorService.java
@@ -0,0 +1,61 @@
+package org.csystem.app.service;
+
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@Service
+public class CommandExecutorService {
+
+ private static final long TIMEOUT_SECONDS = 300;
+
+ public CompletableFuture executeCommand(String command, Consumer outputCallback) {
+ return CompletableFuture.supplyAsync(() -> {
+ StringBuilder output = new StringBuilder();
+
+ try {
+ ProcessBuilder processBuilder = new ProcessBuilder();
+
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ processBuilder.command("cmd.exe", "/c", command);
+ } else {
+ processBuilder.command("sh", "-c", command);
+ }
+
+ processBuilder.redirectErrorStream(true);
+ Process process = processBuilder.start();
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ output.append(line).append("\n");
+ if (outputCallback != null) {
+ outputCallback.accept(line);
+ }
+ }
+ }
+
+ boolean finished = process.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ if (!finished) {
+ process.destroyForcibly();
+ output.append("\n[TIMEOUT] Command exceeded ").append(TIMEOUT_SECONDS).append(" seconds");
+ }
+
+ int exitCode = process.exitValue();
+ output.append("\n[EXIT CODE: ").append(exitCode).append("]");
+
+ } catch (Exception e) {
+ output.append("\n[ERROR] ").append(e.getMessage());
+ }
+
+ return output.toString();
+ });
+ }
+}
diff --git a/src/Projects/WebSocketCLI/src/main/resources/application.properties b/src/Projects/WebSocketCLI/src/main/resources/application.properties
new file mode 100644
index 00000000..2c2a86a2
--- /dev/null
+++ b/src/Projects/WebSocketCLI/src/main/resources/application.properties
@@ -0,0 +1,6 @@
+server.port=8080
+spring.application.name=websocket-cli
+
+# WebSocket settings
+spring.websocket.max-text-message-size=65536
+spring.websocket.max-binary-message-size=65536