@woopsy/mcpanel 1.0.0

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,243 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ServerManager = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const helpers_1 = require("../utils/helpers");
40
+ const logger_1 = require("../utils/logger");
41
+ class ServerManager {
42
+ configManager;
43
+ constructor(configManager) {
44
+ this.configManager = configManager;
45
+ }
46
+ /**
47
+ * Helper to ensure server name is safe for directories (alphanumeric, dashes, underscores)
48
+ */
49
+ cleanServerName(name) {
50
+ return name.replace(/[^a-zA-Z0-9_\-]/g, '');
51
+ }
52
+ /**
53
+ * Inspects a server folder and best-effort detects the server software
54
+ * (Paper, Fabric, Purpur, Vanilla, ...) and the Minecraft version.
55
+ */
56
+ detectServerInfo(dir) {
57
+ let software = 'Vanilla';
58
+ let version = 'Unknown';
59
+ let files = [];
60
+ try {
61
+ files = fs.readdirSync(dir);
62
+ }
63
+ catch {
64
+ return { software, version };
65
+ }
66
+ const lowerFiles = files.map(f => f.toLowerCase());
67
+ const jars = lowerFiles.filter(f => f.endsWith('.jar'));
68
+ const jarBlob = jars.join(' ');
69
+ // --- Software detection ---
70
+ const hasFabric = lowerFiles.includes('fabric-server-launch.jar') ||
71
+ lowerFiles.includes('fabric-server-launcher.properties') ||
72
+ lowerFiles.includes('.fabric') ||
73
+ lowerFiles.some(f => f.startsWith('fabric-server')) ||
74
+ jarBlob.includes('fabric');
75
+ if (hasFabric)
76
+ software = 'Fabric';
77
+ else if (jarBlob.includes('purpur'))
78
+ software = 'Purpur';
79
+ else if (jarBlob.includes('paper'))
80
+ software = 'Paper';
81
+ else if (jarBlob.includes('spigot'))
82
+ software = 'Spigot';
83
+ else if (jarBlob.includes('forge') || lowerFiles.some(f => f.includes('forge')))
84
+ software = 'Forge';
85
+ else if (jarBlob.includes('velocity'))
86
+ software = 'Velocity';
87
+ else if (jarBlob.includes('waterfall'))
88
+ software = 'Waterfall';
89
+ else if (jarBlob.includes('bukkit'))
90
+ software = 'CraftBukkit';
91
+ // --- Version detection ---
92
+ // 1) Paper/Purpur write a version_history.json with the real MC version.
93
+ const vhPath = path.join(dir, 'version_history.json');
94
+ if (fs.existsSync(vhPath)) {
95
+ try {
96
+ const data = JSON.parse(fs.readFileSync(vhPath, 'utf-8'));
97
+ const cur = data.currentVersion || '';
98
+ const m = cur.match(/MC:\s*([\d.]+)/) || cur.match(/(\d+\.\d+(?:\.\d+)?)/);
99
+ if (m)
100
+ version = m[1];
101
+ }
102
+ catch { /* ignore */ }
103
+ }
104
+ // 2) Fabric/modern servers store the MC version as a `versions/<ver>` folder.
105
+ if (version === 'Unknown') {
106
+ const versionsDir = path.join(dir, 'versions');
107
+ try {
108
+ const subdirs = fs.readdirSync(versionsDir, { withFileTypes: true })
109
+ .filter(d => d.isDirectory() && /\d+\.\d+/.test(d.name))
110
+ .map(d => d.name);
111
+ if (subdirs.length > 0)
112
+ version = subdirs[0];
113
+ }
114
+ catch { /* no versions dir */ }
115
+ }
116
+ // 3) Fall back to a version number embedded in a jar filename.
117
+ if (version === 'Unknown') {
118
+ const m = jarBlob.match(/(\d+\.\d+(?:\.\d+)?)/);
119
+ if (m)
120
+ version = m[1];
121
+ }
122
+ return { software, version };
123
+ }
124
+ /**
125
+ * Validates and connects an existing Minecraft server directory as THE
126
+ * single server this CLI manages. Persists it to config.
127
+ */
128
+ syncServer(dirPath) {
129
+ // Translate Windows paths (e.g. C:\...) into WSL mount paths when needed.
130
+ const raw = (0, helpers_1.normalizeInputPath)(dirPath);
131
+ const resolvedPath = path.resolve(raw);
132
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
133
+ throw new Error(`Folder "${resolvedPath}" does not exist.`);
134
+ }
135
+ // Verify it looks like a Minecraft server directory.
136
+ const files = fs.readdirSync(resolvedPath);
137
+ const hasProperties = files.includes('server.properties');
138
+ const hasJar = files.some(f => f.toLowerCase().endsWith('.jar'));
139
+ if (!hasProperties && !hasJar) {
140
+ throw new Error('Not a valid Minecraft server folder — no server.properties or .jar file was found.');
141
+ }
142
+ const { software, version } = this.detectServerInfo(resolvedPath);
143
+ const name = this.cleanServerName(path.basename(resolvedPath)) || 'server';
144
+ // Ensure the EULA is accepted so the server won't refuse to launch.
145
+ const eulaPath = path.join(resolvedPath, 'eula.txt');
146
+ if (!fs.existsSync(eulaPath)) {
147
+ try {
148
+ fs.writeFileSync(eulaPath, 'eula=true\n', 'utf-8');
149
+ }
150
+ catch { /* ignore */ }
151
+ }
152
+ // Preserve a previously chosen RAM allocation if re-syncing the same folder.
153
+ const existing = this.configManager.getServer();
154
+ const ram = existing && path.resolve(existing.path) === resolvedPath
155
+ ? existing.ram
156
+ : this.configManager.getConfig().defaultRam;
157
+ const meta = {
158
+ name,
159
+ path: resolvedPath,
160
+ version,
161
+ software,
162
+ ram,
163
+ };
164
+ this.configManager.setServer(meta);
165
+ logger_1.logger.info(`Synced Minecraft server "${name}" at ${resolvedPath} (${software} ${version})`);
166
+ return meta;
167
+ }
168
+ /**
169
+ * Helper to parse server.properties file.
170
+ */
171
+ readPropertiesFile(filePath) {
172
+ if (!fs.existsSync(filePath)) {
173
+ return {};
174
+ }
175
+ const content = fs.readFileSync(filePath, 'utf-8');
176
+ const lines = content.split(/\r?\n/);
177
+ const properties = {};
178
+ for (const line of lines) {
179
+ const trimmed = line.trim();
180
+ if (trimmed.startsWith('#') || !trimmed.includes('=')) {
181
+ continue;
182
+ }
183
+ const eqIdx = trimmed.indexOf('=');
184
+ const key = trimmed.substring(0, eqIdx).trim();
185
+ const value = trimmed.substring(eqIdx + 1).trim();
186
+ properties[key] = value;
187
+ }
188
+ return properties;
189
+ }
190
+ /**
191
+ * Helper to write server.properties file.
192
+ */
193
+ writePropertiesFile(filePath, properties) {
194
+ let content = '';
195
+ // If file exists, we want to preserve comments and update keys
196
+ if (fs.existsSync(filePath)) {
197
+ const fileLines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
198
+ const writtenKeys = new Set();
199
+ for (const line of fileLines) {
200
+ const trimmed = line.trim();
201
+ if (trimmed.startsWith('#') || !trimmed.includes('=')) {
202
+ content += line + '\n';
203
+ continue;
204
+ }
205
+ const eqIdx = trimmed.indexOf('=');
206
+ const key = trimmed.substring(0, eqIdx).trim();
207
+ if (properties[key] !== undefined) {
208
+ content += `${key}=${properties[key]}\n`;
209
+ writtenKeys.add(key);
210
+ }
211
+ else {
212
+ content += line + '\n';
213
+ }
214
+ }
215
+ // Append any new properties that were not in the file originally
216
+ for (const [key, value] of Object.entries(properties)) {
217
+ if (!writtenKeys.has(key)) {
218
+ content += `${key}=${value}\n`;
219
+ }
220
+ }
221
+ }
222
+ else {
223
+ // Just write new properties
224
+ content += '#Minecraft server properties\n';
225
+ for (const [key, value] of Object.entries(properties)) {
226
+ content += `${key}=${value}\n`;
227
+ }
228
+ }
229
+ fs.writeFileSync(filePath, content, 'utf-8');
230
+ }
231
+ /**
232
+ * Updates server.properties of the managed server.
233
+ */
234
+ updateServerProperties(updates) {
235
+ const server = this.configManager.getServer();
236
+ if (!server) {
237
+ throw new Error('No server is connected.');
238
+ }
239
+ const propertiesPath = path.join(server.path, 'server.properties');
240
+ this.writePropertiesFile(propertiesPath, updates);
241
+ }
242
+ }
243
+ exports.ServerManager = ServerManager;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DownloadService = void 0;
37
+ exports.downloadFile = downloadFile;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const https = __importStar(require("https"));
41
+ const configManager_1 = require("../config/configManager");
42
+ const logger_1 = require("../utils/logger");
43
+ const DOWNLOADS_DIR = path.join(configManager_1.APP_ROOT, 'downloads');
44
+ /**
45
+ * Downloads a file from a URL with redirection support and reports progress.
46
+ */
47
+ function downloadFile(url, destPath, onProgress) {
48
+ return new Promise((resolve, reject) => {
49
+ https.get(url, { headers: { 'User-Agent': 'mcpanel-agent' } }, (res) => {
50
+ // Handle redirects
51
+ if ([301, 302, 307, 308].includes(res.statusCode || 0)) {
52
+ if (res.headers.location) {
53
+ downloadFile(res.headers.location, destPath, onProgress).then(resolve).catch(reject);
54
+ return;
55
+ }
56
+ }
57
+ if (res.statusCode !== 200) {
58
+ reject(new Error(`Server returned HTTP ${res.statusCode}: ${res.statusMessage}`));
59
+ return;
60
+ }
61
+ const totalBytes = Number(res.headers['content-length'] || 0);
62
+ const fileStream = fs.createWriteStream(destPath);
63
+ let downloadedBytes = 0;
64
+ res.on('data', (chunk) => {
65
+ downloadedBytes += chunk.length;
66
+ fileStream.write(chunk);
67
+ if (onProgress) {
68
+ onProgress(downloadedBytes, totalBytes);
69
+ }
70
+ });
71
+ res.on('end', () => {
72
+ fileStream.end();
73
+ resolve();
74
+ });
75
+ res.on('error', (err) => {
76
+ fileStream.close();
77
+ fs.unlink(destPath, () => { });
78
+ reject(err);
79
+ });
80
+ }).on('error', (err) => {
81
+ fs.unlink(destPath, () => { });
82
+ reject(err);
83
+ });
84
+ });
85
+ }
86
+ /**
87
+ * Fetches JSON response from a URL (used for API queries)
88
+ */
89
+ function fetchJSON(url) {
90
+ return new Promise((resolve, reject) => {
91
+ https.get(url, { headers: { 'User-Agent': 'mcpanel-agent' } }, (res) => {
92
+ if ([301, 302, 307, 308].includes(res.statusCode || 0)) {
93
+ if (res.headers.location) {
94
+ fetchJSON(res.headers.location).then(resolve).catch(reject);
95
+ return;
96
+ }
97
+ }
98
+ if (res.statusCode !== 200) {
99
+ reject(new Error(`Failed to fetch JSON: HTTP ${res.statusCode}`));
100
+ return;
101
+ }
102
+ let data = '';
103
+ res.on('data', chunk => { data += chunk; });
104
+ res.on('end', () => {
105
+ try {
106
+ resolve(JSON.parse(data));
107
+ }
108
+ catch (e) {
109
+ reject(e);
110
+ }
111
+ });
112
+ }).on('error', reject);
113
+ });
114
+ }
115
+ class DownloadService {
116
+ /**
117
+ * Resolves the download URL for the specified Minecraft software and version.
118
+ */
119
+ async getDownloadUrl(software, version) {
120
+ const sw = software.toLowerCase();
121
+ if (sw === 'paper' || sw === 'velocity' || sw === 'waterfall') {
122
+ const project = sw;
123
+ const versionUrl = `https://api.papermc.io/v2/projects/${project}/versions/${version}`;
124
+ try {
125
+ const versionData = await fetchJSON(versionUrl);
126
+ const builds = versionData.builds;
127
+ if (!builds || builds.length === 0) {
128
+ throw new Error(`No builds found for version ${version}`);
129
+ }
130
+ const latestBuild = builds[builds.length - 1];
131
+ const buildUrl = `https://api.papermc.io/v2/projects/${project}/versions/${version}/builds/${latestBuild}`;
132
+ const buildData = await fetchJSON(buildUrl);
133
+ const downloadFile = buildData.downloads.application.name;
134
+ return `https://api.papermc.io/v2/projects/${project}/versions/${version}/builds/${latestBuild}/downloads/${downloadFile}`;
135
+ }
136
+ catch (err) {
137
+ logger_1.logger.error(`Error resolving PaperMC API download URL for ${software} ${version}`, err);
138
+ throw new Error(`Failed to resolve PaperMC download link: ${err.message}`);
139
+ }
140
+ }
141
+ else if (sw === 'purpur') {
142
+ // Purpur supports a simple redirecting link for the latest build of a version
143
+ return `https://api.purpurmc.org/v2/purpur/${version}/latest/download`;
144
+ }
145
+ throw new Error(`Unsupported software type: ${software}`);
146
+ }
147
+ /**
148
+ * Downloads the server jar file if not already cached.
149
+ * Returns the absolute path to the downloaded jar file.
150
+ */
151
+ async downloadServerJar(software, version, onProgress) {
152
+ const sw = software.toLowerCase();
153
+ const jarName = `${sw}-${version}.jar`;
154
+ const cachedPath = path.join(DOWNLOADS_DIR, jarName);
155
+ if (fs.existsSync(cachedPath)) {
156
+ logger_1.logger.info(`Using cached server jar: ${cachedPath}`);
157
+ if (onProgress)
158
+ onProgress(100);
159
+ return cachedPath;
160
+ }
161
+ logger_1.logger.info(`Fetching download URL for ${software} version ${version}...`);
162
+ const downloadUrl = await this.getDownloadUrl(software, version);
163
+ logger_1.logger.info(`Downloading server jar from: ${downloadUrl}`);
164
+ const tempPath = `${cachedPath}.tmp`;
165
+ await downloadFile(downloadUrl, tempPath, (downloaded, total) => {
166
+ if (onProgress && total > 0) {
167
+ const pct = parseFloat(((downloaded / total) * 100).toFixed(1));
168
+ onProgress(pct);
169
+ }
170
+ });
171
+ fs.renameSync(tempPath, cachedPath);
172
+ logger_1.logger.info(`Server jar downloaded and saved to: ${cachedPath}`);
173
+ return cachedPath;
174
+ }
175
+ }
176
+ exports.DownloadService = DownloadService;
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProcessManager = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const logger_1 = require("../utils/logger");
40
+ const helpers_1 = require("../utils/helpers");
41
+ class ProcessManager {
42
+ activeServers = new Map();
43
+ consoleCallbacks = new Map();
44
+ /**
45
+ * Starts a server process.
46
+ */
47
+ startServer(name, serverDir, jarPath, ram, javaPath = 'java') {
48
+ return new Promise((resolve, reject) => {
49
+ const key = name.toLowerCase();
50
+ if (this.activeServers.has(key)) {
51
+ reject(new Error(`Server "${name}" is already running.`));
52
+ return;
53
+ }
54
+ // Check if Java is installed
55
+ const javaCheck = (0, helpers_1.checkJava)(javaPath);
56
+ if (!javaCheck.installed) {
57
+ reject(new Error(`Java was not found at "${javaPath}". Please ensure Java is installed.`));
58
+ return;
59
+ }
60
+ if (!fs.existsSync(jarPath)) {
61
+ reject(new Error(`Server jar was not found at "${jarPath}".`));
62
+ return;
63
+ }
64
+ logger_1.logger.logServerStart(name, `Launching Java process in ${serverDir} with ${ram} RAM`);
65
+ // Prepare JVM arguments. Ensure RAM syntax (e.g. 4G -> -Xmx4G)
66
+ const cleanedRam = ram.replace(/[^0-9a-zA-Z]/g, '');
67
+ const args = [
68
+ `-Xmx${cleanedRam}`,
69
+ `-Xms${cleanedRam}`,
70
+ '-jar',
71
+ jarPath,
72
+ 'nogui'
73
+ ];
74
+ // Spawn process inside the server directory
75
+ const child = (0, child_process_1.spawn)(javaPath, args, {
76
+ cwd: serverDir,
77
+ shell: false,
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
80
+ const serverInfo = {
81
+ name,
82
+ process: child,
83
+ status: 'Starting',
84
+ startTime: Date.now(),
85
+ pid: child.pid || 0,
86
+ };
87
+ this.activeServers.set(key, serverInfo);
88
+ // Clean/reset console log on start
89
+ const logFilePath = logger_1.logger.getServerLogPath(name);
90
+ try {
91
+ if (fs.existsSync(logFilePath)) {
92
+ fs.writeFileSync(logFilePath, '', 'utf-8');
93
+ }
94
+ }
95
+ catch (err) {
96
+ logger_1.logger.error(`Failed to clear console log for ${name}`, err);
97
+ }
98
+ // Handle stdout
99
+ child.stdout.on('data', (data) => {
100
+ const chunk = data.toString();
101
+ logger_1.logger.writeServerConsoleLog(name, chunk);
102
+ // Check for startup completion keywords
103
+ if (serverInfo.status === 'Starting') {
104
+ if (chunk.includes('Done (') ||
105
+ chunk.includes('For help, type "help"') ||
106
+ chunk.includes('Starting velocity server') || // velocity starts quickly
107
+ chunk.includes('Ready for connections')) {
108
+ serverInfo.status = 'Running';
109
+ logger_1.logger.logServerStart(name, `Server fully loaded (PID: ${child.pid})`);
110
+ }
111
+ }
112
+ // Forward to console streaming callbacks
113
+ const callback = this.consoleCallbacks.get(key);
114
+ if (callback) {
115
+ callback(chunk);
116
+ }
117
+ });
118
+ // Handle stderr
119
+ child.stderr.on('data', (data) => {
120
+ const chunk = data.toString();
121
+ logger_1.logger.writeServerConsoleLog(name, `[STDERR] ${chunk}`);
122
+ const callback = this.consoleCallbacks.get(key);
123
+ if (callback) {
124
+ callback(`[STDERR] ${chunk}`);
125
+ }
126
+ });
127
+ // Handle process exit
128
+ child.on('close', (code) => {
129
+ logger_1.logger.logServerStop(name, `Process exited with code ${code}`);
130
+ this.activeServers.delete(key);
131
+ this.consoleCallbacks.delete(key);
132
+ });
133
+ child.on('error', (err) => {
134
+ logger_1.logger.error(`Process error for server "${name}"`, err);
135
+ reject(err);
136
+ });
137
+ // Wait a moment to ensure it spawns without immediate crash
138
+ setTimeout(() => {
139
+ if (child.pid) {
140
+ resolve();
141
+ }
142
+ else {
143
+ reject(new Error('Process failed to spawn'));
144
+ }
145
+ }, 500);
146
+ });
147
+ }
148
+ /**
149
+ * Stops a server process gracefully, falling back to SIGKILL.
150
+ */
151
+ async stopServer(name) {
152
+ const key = name.toLowerCase();
153
+ const server = this.activeServers.get(key);
154
+ if (!server) {
155
+ return false;
156
+ }
157
+ logger_1.logger.logServerStop(name, 'Graceful shutdown initiated.');
158
+ server.status = 'Offline'; // Update state immediately
159
+ return new Promise((resolve) => {
160
+ // Send "stop" command to standard input
161
+ try {
162
+ if (server.process.stdin) {
163
+ server.process.stdin.write('stop\n');
164
+ }
165
+ else {
166
+ logger_1.logger.error(`Cannot stop server ${name}: stdin is not available.`);
167
+ }
168
+ }
169
+ catch (err) {
170
+ logger_1.logger.error(`Failed to write stop command to stdin of ${name}`, err);
171
+ }
172
+ // Check if process has exited within 15 seconds, otherwise kill it
173
+ const timeout = setTimeout(() => {
174
+ if (this.activeServers.has(key)) {
175
+ logger_1.logger.warn(`Server ${name} did not stop gracefully. Force killing process (PID: ${server.pid})`);
176
+ try {
177
+ server.process.kill('SIGKILL');
178
+ }
179
+ catch (err) {
180
+ logger_1.logger.error(`Error killing server ${name} process`, err);
181
+ }
182
+ }
183
+ resolve(true);
184
+ }, 15000);
185
+ // Listen for process exit to resolve immediately
186
+ server.process.on('exit', () => {
187
+ clearTimeout(timeout);
188
+ this.activeServers.delete(key);
189
+ resolve(true);
190
+ });
191
+ });
192
+ }
193
+ /**
194
+ * Sends terminal command input to a running server console.
195
+ */
196
+ sendCommand(name, command) {
197
+ const key = name.toLowerCase();
198
+ const server = this.activeServers.get(key);
199
+ if (!server) {
200
+ return false;
201
+ }
202
+ try {
203
+ if (server.process.stdin) {
204
+ server.process.stdin.write(command + '\n');
205
+ return true;
206
+ }
207
+ else {
208
+ logger_1.logger.error(`Failed to send command to ${name}: stdin is null.`);
209
+ return false;
210
+ }
211
+ }
212
+ catch (err) {
213
+ logger_1.logger.error(`Failed to send command to ${name}: ${command}`, err);
214
+ return false;
215
+ }
216
+ }
217
+ /**
218
+ * Registers a console logging callback for active log streaming.
219
+ */
220
+ registerConsoleStream(name, callback) {
221
+ this.consoleCallbacks.set(name.toLowerCase(), callback);
222
+ }
223
+ /**
224
+ * Unregisters console logging callback.
225
+ */
226
+ unregisterConsoleStream(name) {
227
+ this.consoleCallbacks.delete(name.toLowerCase());
228
+ }
229
+ /**
230
+ * Returns list of running server PIDs and states.
231
+ */
232
+ getActiveServers() {
233
+ return this.activeServers;
234
+ }
235
+ /**
236
+ * Gets details for a specific active server.
237
+ */
238
+ getActiveServer(name) {
239
+ return this.activeServers.get(name.toLowerCase());
240
+ }
241
+ }
242
+ exports.ProcessManager = ProcessManager;