2025-10-04 11:02:27 +02:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
import { exec, spawn } from "child_process";
|
2025-10-04 11:02:27 +02:00
|
|
|
|
import { networkInterfaces } from "os";
|
|
|
|
|
|
import path from "path";
|
|
|
|
|
|
import { fileURLToPath } from "url";
|
2025-10-04 11:16:42 +02:00
|
|
|
|
import { readdir, stat, access } from "fs/promises";
|
|
|
|
|
|
import { createHash } from "crypto";
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 🎨 STYLING & ANIMATIONS
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
reset: "\x1b[0m",
|
|
|
|
|
|
bold: "\x1b[1m",
|
|
|
|
|
|
dim: "\x1b[2m",
|
|
|
|
|
|
italic: "\x1b[3m",
|
|
|
|
|
|
underline: "\x1b[4m",
|
|
|
|
|
|
|
|
|
|
|
|
// Standard colors
|
|
|
|
|
|
black: "\x1b[30m",
|
|
|
|
|
|
red: "\x1b[31m",
|
|
|
|
|
|
green: "\x1b[32m",
|
|
|
|
|
|
yellow: "\x1b[33m",
|
|
|
|
|
|
blue: "\x1b[34m",
|
|
|
|
|
|
magenta: "\x1b[35m",
|
|
|
|
|
|
cyan: "\x1b[36m",
|
|
|
|
|
|
white: "\x1b[37m",
|
|
|
|
|
|
|
|
|
|
|
|
// Bright colors
|
|
|
|
|
|
brightRed: "\x1b[91m",
|
|
|
|
|
|
brightGreen: "\x1b[92m",
|
|
|
|
|
|
brightYellow: "\x1b[93m",
|
|
|
|
|
|
brightBlue: "\x1b[94m",
|
|
|
|
|
|
brightMagenta: "\x1b[95m",
|
|
|
|
|
|
brightCyan: "\x1b[96m",
|
|
|
|
|
|
brightWhite: "\x1b[97m",
|
|
|
|
|
|
|
|
|
|
|
|
// Backgrounds
|
|
|
|
|
|
bgRed: "\x1b[41m",
|
|
|
|
|
|
bgGreen: "\x1b[42m",
|
|
|
|
|
|
bgYellow: "\x1b[43m",
|
|
|
|
|
|
bgBlue: "\x1b[44m",
|
|
|
|
|
|
bgMagenta: "\x1b[45m",
|
|
|
|
|
|
bgCyan: "\x1b[46m",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const symbols = {
|
|
|
|
|
|
// Status indicators
|
|
|
|
|
|
success: "✓",
|
|
|
|
|
|
error: "✗",
|
|
|
|
|
|
warning: "⚠",
|
|
|
|
|
|
info: "ℹ",
|
|
|
|
|
|
|
|
|
|
|
|
// Activities
|
|
|
|
|
|
server: "🚀",
|
|
|
|
|
|
folder: "📁",
|
|
|
|
|
|
network: "🌐",
|
|
|
|
|
|
local: "🏠",
|
|
|
|
|
|
external: "📡",
|
|
|
|
|
|
|
|
|
|
|
|
lightning: "⚡",
|
|
|
|
|
|
fire: "🔥",
|
|
|
|
|
|
sparkles: "✨",
|
|
|
|
|
|
eyes: "👀",
|
|
|
|
|
|
stop: "🛑",
|
|
|
|
|
|
gear: "⚙",
|
|
|
|
|
|
globe: "🌍",
|
|
|
|
|
|
|
|
|
|
|
|
// UI elements
|
|
|
|
|
|
arrow: "→",
|
|
|
|
|
|
bullet: "•",
|
|
|
|
|
|
star: "★",
|
|
|
|
|
|
diamond: "◆",
|
|
|
|
|
|
|
|
|
|
|
|
// Box drawing
|
|
|
|
|
|
boxLight: "─",
|
|
|
|
|
|
boxHeavy: "━",
|
|
|
|
|
|
boxDouble: "═",
|
|
|
|
|
|
cornerTL: "┌",
|
|
|
|
|
|
cornerTR: "┐",
|
|
|
|
|
|
cornerBL: "└",
|
|
|
|
|
|
cornerBR: "┘",
|
|
|
|
|
|
cross: "┼",
|
|
|
|
|
|
teeLeft: "├",
|
|
|
|
|
|
teeRight: "┤",
|
|
|
|
|
|
vertical: "│",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 🛠 UTILITIES
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
function createGradient(text, startColor, endColor) {
|
|
|
|
|
|
return text
|
|
|
|
|
|
.split("")
|
|
|
|
|
|
.map((char, i) => {
|
|
|
|
|
|
const color = i % 2 === 0 ? startColor : endColor;
|
|
|
|
|
|
return `${color}${char}${colors.reset}`;
|
|
|
|
|
|
})
|
|
|
|
|
|
.join("");
|
|
|
|
|
|
}
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
function createBox(content, title = "", style = "light") {
|
|
|
|
|
|
const lines = content.split("\n");
|
|
|
|
|
|
const maxLength = Math.max(
|
|
|
|
|
|
...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length),
|
|
|
|
|
|
title.length,
|
|
|
|
|
|
);
|
|
|
|
|
|
const width = maxLength + 4;
|
|
|
|
|
|
|
|
|
|
|
|
const horizontal =
|
|
|
|
|
|
style === "heavy"
|
|
|
|
|
|
? symbols.boxHeavy
|
|
|
|
|
|
: style === "double"
|
|
|
|
|
|
? symbols.boxDouble
|
|
|
|
|
|
: symbols.boxLight;
|
|
|
|
|
|
|
|
|
|
|
|
let result = "";
|
|
|
|
|
|
|
|
|
|
|
|
// Top border
|
|
|
|
|
|
result += `${symbols.cornerTL}${horizontal.repeat(width - 2)}${symbols.cornerTR}\n`;
|
|
|
|
|
|
|
|
|
|
|
|
// Title
|
|
|
|
|
|
if (title) {
|
|
|
|
|
|
const padding = Math.floor((width - title.length - 4) / 2);
|
|
|
|
|
|
result += `${symbols.vertical} ${" ".repeat(padding)}${colors.bold}${title}${colors.reset}${" ".repeat(padding)} ${symbols.vertical}\n`;
|
|
|
|
|
|
result += `${symbols.teeLeft}${horizontal.repeat(width - 2)}${symbols.teeRight}\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Content
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
|
|
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
|
|
|
|
const padding = width - cleanLine.length - 4;
|
|
|
|
|
|
result += `${symbols.vertical} ${line}${" ".repeat(padding)} ${symbols.vertical}\n`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Bottom border
|
|
|
|
|
|
result += `${symbols.cornerBL}${horizontal.repeat(width - 2)}${symbols.cornerBR}`;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatFileSize(bytes) {
|
|
|
|
|
|
if (bytes === 0) return "0 B";
|
|
|
|
|
|
const k = 1024;
|
|
|
|
|
|
const sizes = ["B", "KB", "MB", "GB"];
|
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 🌐 NETWORK UTILITIES
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
function getAllNetworkInterfaces() {
|
2025-10-04 11:02:27 +02:00
|
|
|
|
const nets = networkInterfaces();
|
2025-10-04 11:16:42 +02:00
|
|
|
|
const results = [];
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
|
|
|
|
|
for (const name of Object.keys(nets)) {
|
|
|
|
|
|
for (const net of nets[name]) {
|
|
|
|
|
|
if (net.family === "IPv4" && !net.internal) {
|
2025-10-04 11:16:42 +02:00
|
|
|
|
results.push({
|
|
|
|
|
|
interface: name,
|
|
|
|
|
|
address: net.address,
|
|
|
|
|
|
netmask: net.netmask,
|
|
|
|
|
|
mac: net.mac,
|
|
|
|
|
|
});
|
2025-10-04 11:02:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
return results;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getPrimaryIP() {
|
|
|
|
|
|
const interfaces = getAllNetworkInterfaces();
|
|
|
|
|
|
|
|
|
|
|
|
// Prefer WiFi/Ethernet interfaces
|
|
|
|
|
|
const preferred = interfaces.find(
|
|
|
|
|
|
(iface) =>
|
|
|
|
|
|
iface.interface.toLowerCase().includes("wlan") ||
|
|
|
|
|
|
iface.interface.toLowerCase().includes("wifi") ||
|
|
|
|
|
|
iface.interface.toLowerCase().includes("eth") ||
|
|
|
|
|
|
iface.interface.toLowerCase().includes("en"),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return preferred ? preferred.address : interfaces[0]?.address || "localhost";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 📊 FILE SYSTEM UTILITIES
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
async function analyzeDistDirectory(distPath) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await access(distPath);
|
|
|
|
|
|
const entries = await readdir(distPath);
|
|
|
|
|
|
|
|
|
|
|
|
const plugins = [];
|
|
|
|
|
|
let totalSize = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const entryPath = path.join(distPath, entry);
|
|
|
|
|
|
const entryStats = await stat(entryPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (entryStats.isDirectory()) {
|
|
|
|
|
|
// Analyze plugin directory
|
|
|
|
|
|
const pluginFiles = await readdir(entryPath);
|
|
|
|
|
|
let pluginSize = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of pluginFiles) {
|
|
|
|
|
|
const filePath = path.join(entryPath, file);
|
|
|
|
|
|
const fileStats = await stat(filePath);
|
|
|
|
|
|
if (fileStats.isFile()) {
|
|
|
|
|
|
pluginSize += fileStats.size;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
plugins.push({
|
|
|
|
|
|
name: entry,
|
|
|
|
|
|
size: pluginSize,
|
|
|
|
|
|
files: pluginFiles.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
totalSize += pluginSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Skip entries that can't be read
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
plugins,
|
|
|
|
|
|
totalSize,
|
|
|
|
|
|
totalPlugins: plugins.length,
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
plugins: [],
|
|
|
|
|
|
totalSize: 0,
|
|
|
|
|
|
totalPlugins: 0,
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
};
|
2025-10-04 11:02:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 🎭 LOGGER
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
const logger = {
|
|
|
|
|
|
banner() {
|
|
|
|
|
|
const title = createGradient(
|
|
|
|
|
|
"VENDETTA DEV SERVER",
|
|
|
|
|
|
colors.brightCyan,
|
|
|
|
|
|
colors.brightMagenta,
|
|
|
|
|
|
);
|
|
|
|
|
|
const subtitle = `${colors.dim}Professional development server for Vendetta plugins${colors.reset}`;
|
|
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
console.log(` ${symbols.server} ${title}`);
|
|
|
|
|
|
console.log(` ${subtitle}`);
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
section(title) {
|
|
|
|
|
|
const line = symbols.boxHeavy.repeat(60);
|
|
|
|
|
|
console.log(`\n${colors.brightBlue}${line}${colors.reset}`);
|
|
|
|
|
|
console.log(`${colors.bold}${colors.brightBlue} ${title}${colors.reset}`);
|
|
|
|
|
|
console.log(`${colors.brightBlue}${line}${colors.reset}\n`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
info(message, icon = symbols.info) {
|
|
|
|
|
|
console.log(`${colors.brightBlue}${icon}${colors.reset} ${message}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
success(message, icon = symbols.success) {
|
|
|
|
|
|
console.log(`${colors.brightGreen}${icon}${colors.reset} ${message}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
warning(message, icon = symbols.warning) {
|
|
|
|
|
|
console.log(`${colors.brightYellow}${icon}${colors.reset} ${message}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
error(message, icon = symbols.error) {
|
|
|
|
|
|
console.log(`${colors.brightRed}${icon}${colors.reset} ${message}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
table(headers, rows) {
|
|
|
|
|
|
const colWidths = headers.map((header, i) =>
|
|
|
|
|
|
Math.max(
|
|
|
|
|
|
header.length,
|
|
|
|
|
|
...rows.map(
|
|
|
|
|
|
(row) =>
|
|
|
|
|
|
(row[i] || "").toString().replace(/\x1b\[[0-9;]*m/g, "").length,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
|
const headerRow = headers
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(header, i) =>
|
|
|
|
|
|
`${colors.bold}${header.padEnd(colWidths[i])}${colors.reset}`,
|
|
|
|
|
|
)
|
|
|
|
|
|
.join(` ${colors.dim}│${colors.reset} `);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(` ${headerRow}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Separator
|
|
|
|
|
|
const separator = colWidths
|
|
|
|
|
|
.map((width) => symbols.boxLight.repeat(width))
|
|
|
|
|
|
.join(`${colors.dim}─┼─${colors.reset}`);
|
|
|
|
|
|
console.log(` ${colors.dim}${separator}${colors.reset}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Rows
|
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
|
const formattedRow = row
|
|
|
|
|
|
.map((cell, i) => {
|
|
|
|
|
|
const cleanCell = (cell || "")
|
|
|
|
|
|
.toString()
|
|
|
|
|
|
.replace(/\x1b\[[0-9;]*m/g, "");
|
|
|
|
|
|
const padding = colWidths[i] - cleanCell.length;
|
|
|
|
|
|
return cell + " ".repeat(padding);
|
|
|
|
|
|
})
|
|
|
|
|
|
.join(` ${colors.dim}│${colors.reset} `);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(` ${formattedRow}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 🚀 MAIN EXECUTION
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
console.clear();
|
|
|
|
|
|
|
|
|
|
|
|
// Configuration
|
|
|
|
|
|
const distPath = path.join(__dirname, "dist");
|
|
|
|
|
|
const port = process.env.PORT || 3000;
|
|
|
|
|
|
const host = "0.0.0.0";
|
|
|
|
|
|
const primaryIP = getPrimaryIP();
|
|
|
|
|
|
const allInterfaces = getAllNetworkInterfaces();
|
|
|
|
|
|
|
|
|
|
|
|
// Display banner
|
|
|
|
|
|
logger.banner();
|
|
|
|
|
|
|
|
|
|
|
|
// Analyze distribution directory
|
|
|
|
|
|
logger.section("Analyzing Distribution");
|
|
|
|
|
|
|
|
|
|
|
|
const analysis = await analyzeDistDirectory(distPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (analysis.error) {
|
|
|
|
|
|
logger.error(`Cannot access dist directory: ${analysis.error}`);
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`Make sure to run ${colors.bold}npm run build${colors.reset} first`,
|
|
|
|
|
|
);
|
2025-10-04 11:02:27 +02:00
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
logger.info(
|
|
|
|
|
|
`${colors.dim}Serving from${colors.reset} ${colors.bold}${distPath}${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.success(
|
|
|
|
|
|
`Found ${colors.bold}${analysis.totalPlugins}${colors.reset} plugin(s) ${colors.dim}(${formatFileSize(analysis.totalSize)} total)${colors.reset}`,
|
|
|
|
|
|
);
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
// Plugin table
|
|
|
|
|
|
if (analysis.plugins.length > 0) {
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
const pluginRows = analysis.plugins.map((plugin) => [
|
|
|
|
|
|
plugin.name,
|
|
|
|
|
|
plugin.files.toString(),
|
|
|
|
|
|
formatFileSize(plugin.size),
|
|
|
|
|
|
]);
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
logger.table(["Plugin", "Files", "Size"], pluginRows);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Network information
|
|
|
|
|
|
logger.section("Network Configuration");
|
|
|
|
|
|
|
|
|
|
|
|
const localUrl = `http://localhost:${port}`;
|
|
|
|
|
|
const networkUrl = `http://${primaryIP}:${port}`;
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`${colors.dim}Binding to${colors.reset} ${colors.bold}${host}:${port}${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Network interfaces table
|
|
|
|
|
|
if (allInterfaces.length > 0) {
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
const networkRows = allInterfaces.map((iface) => [
|
|
|
|
|
|
iface.interface,
|
|
|
|
|
|
iface.address,
|
|
|
|
|
|
`http://${iface.address}:${port}`,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
logger.table(["Interface", "IP Address", "URL"], networkRows);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Access URLs
|
|
|
|
|
|
logger.section("Access Information");
|
|
|
|
|
|
|
|
|
|
|
|
logger.success(
|
|
|
|
|
|
`${symbols.local} Local: ${colors.bold}${colors.underline}${localUrl}${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.success(
|
|
|
|
|
|
`${symbols.network} Network: ${colors.bold}${colors.underline}${networkUrl}${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`${symbols.globe} External: ${colors.dim}Accessible from any device on your network${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Server startup
|
|
|
|
|
|
logger.section("Starting Server");
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`${symbols.lightning} Initializing development server...`);
|
|
|
|
|
|
|
|
|
|
|
|
// Check if serve is available
|
|
|
|
|
|
try {
|
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
|
|
exec("npx serve --version", (error) => {
|
|
|
|
|
|
if (error) reject(error);
|
|
|
|
|
|
else resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logger.warning("Installing serve package...");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const serverProcess = spawn(
|
|
|
|
|
|
"npx",
|
|
|
|
|
|
["serve", "-s", distPath, "-l", port.toString()],
|
|
|
|
|
|
{
|
|
|
|
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
|
|
|
|
shell: process.platform === "win32",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Handle server output
|
|
|
|
|
|
let serverStarted = false;
|
|
|
|
|
|
|
|
|
|
|
|
serverProcess.stdout.on("data", (data) => {
|
|
|
|
|
|
const output = data.toString();
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
!serverStarted &&
|
|
|
|
|
|
(output.includes("Accepting connections") || output.includes("Local:"))
|
|
|
|
|
|
) {
|
|
|
|
|
|
serverStarted = true;
|
|
|
|
|
|
|
|
|
|
|
|
logger.success(`${symbols.fire} Server is running!`);
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
|
|
|
|
// Instructions
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`${symbols.eyes} ${colors.dim}Watching for file changes...${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
`${symbols.stop} ${colors.dim}Press ${colors.reset}${colors.bold}Ctrl+C${colors.reset}${colors.dim} to stop the server${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`${colors.brightGreen}${symbols.sparkles} Ready to develop! ${symbols.sparkles}${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter out serve's own output to avoid duplication
|
|
|
|
|
|
if (
|
|
|
|
|
|
!output.includes("Local:") &&
|
|
|
|
|
|
!output.includes("On Your Network:") &&
|
|
|
|
|
|
!output.includes("Copied local address") &&
|
|
|
|
|
|
!output.includes("Accepting connections") &&
|
|
|
|
|
|
output.trim()
|
|
|
|
|
|
) {
|
|
|
|
|
|
console.log(`${colors.dim}[serve] ${output.trim()}${colors.reset}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
serverProcess.stderr.on("data", (data) => {
|
|
|
|
|
|
const error = data.toString().trim();
|
|
|
|
|
|
if (error && !error.includes("WARNING") && !error.includes("deprecated")) {
|
|
|
|
|
|
logger.error(`Server error: ${error}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
serverProcess.on("error", (error) => {
|
|
|
|
|
|
logger.error(`Failed to start server: ${error.message}`);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
serverProcess.on("exit", (code) => {
|
|
|
|
|
|
if (code !== 0 && code !== null) {
|
|
|
|
|
|
logger.error(`Server exited with code ${code}`);
|
|
|
|
|
|
process.exit(code);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Graceful shutdown
|
|
|
|
|
|
const shutdown = (signal) => {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`\n${colors.brightYellow}${symbols.stop} Received ${signal}, shutting down gracefully...${colors.reset}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (serverProcess && !serverProcess.killed) {
|
|
|
|
|
|
serverProcess.kill("SIGTERM");
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!serverProcess.killed) {
|
|
|
|
|
|
logger.warning("Force killing server process...");
|
|
|
|
|
|
serverProcess.kill("SIGKILL");
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
logger.success("Server stopped successfully");
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
|
|
|
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
|
|
|
|
|
|
|
|
|
|
// Handle uncaught exceptions
|
|
|
|
|
|
process.on("uncaughtException", (error) => {
|
|
|
|
|
|
logger.error(`Uncaught exception: ${error.message}`);
|
|
|
|
|
|
console.error(error.stack);
|
|
|
|
|
|
shutdown("UNCAUGHT_EXCEPTION");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
process.on("unhandledRejection", (reason, promise) => {
|
|
|
|
|
|
logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
|
|
|
|
|
|
shutdown("UNHANDLED_REJECTION");
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-04 11:02:27 +02:00
|
|
|
|
|
2025-10-04 11:16:42 +02:00
|
|
|
|
// Start the server
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
|
console.error(
|
|
|
|
|
|
`${colors.brightRed}${symbols.error} Fatal error:${colors.reset}`,
|
|
|
|
|
|
error.message,
|
|
|
|
|
|
);
|
|
|
|
|
|
console.error(error.stack);
|
|
|
|
|
|
process.exit(1);
|
2025-10-04 11:02:27 +02:00
|
|
|
|
});
|