UNPKG

image-asset-manager

Version:

A comprehensive image asset management tool for frontend projects

916 lines (907 loc) β€’ 40.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebServer = void 0; const express_1 = __importDefault(require("express")); const http_1 = require("http"); const ws_1 = require("ws"); const path_1 = __importDefault(require("path")); const types_1 = require("../types"); // Import embedded web assets (will be generated during build) let webAssets = null; let getAsset = null; let getAssetMimeType = null; try { const embeddedAssets = require("../web-assets"); webAssets = embeddedAssets.webAssets; getAsset = embeddedAssets.getAsset; getAssetMimeType = embeddedAssets.getAssetMimeType; } catch (error) { console.warn("Embedded web assets not found, falling back to development mode"); } class WebServer { constructor() { this.server = null; this.wss = null; this.port = 3000; this.data = null; this.clients = new Set(); this.app = (0, express_1.default)(); this.setupMiddleware(); this.setupAPIRoutes(); // Note: Static file routes and catch-all routes will be set up in start() method } setupMiddleware() { // Enable CORS for development this.app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); }); // Parse JSON bodies this.app.use(express_1.default.json()); // Serve static files (images) - will be configured when server starts // This will be set up in the start method with the actual project path } setupAPIRoutes() { // Health check endpoint this.app.get("/api/health", (req, res) => { // Add diagnostic info to health check let diagnosticInfo = {}; // TODO: Re-implement consistency validation when needed // if (this.data) { // try { // const validation = // this.consistencyValidator.validateCategoryStatistics(this.data); // diagnosticInfo = { // categoryConsistency: { // isConsistent: validation.isConsistent, // totalIssues: validation.totalIssues, // discrepancies: validation.discrepancies.length, // }, // }; // } catch (error) { // diagnosticInfo = { // diagnosticError: // error instanceof Error ? error.message : "Unknown error", // }; // } // } res.json({ status: "ok", timestamp: new Date().toISOString(), diagnostic: diagnosticInfo, }); }); // Get all images with pagination and filtering this.app.get("/api/images", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } const { page = "1", limit = "20", category, type, unused } = req.query; const pageNum = parseInt(page, 10); const limitNum = parseInt(limit, 10); let filteredImages = [...this.data.images]; // Apply filters if (category && category !== "all") { filteredImages = filteredImages.filter((img) => img.category === category); } if (type && type !== "all") { filteredImages = filteredImages.filter((img) => img.extension === type); } if (unused === "true") { const unusedIds = new Set(this.data.usage.unusedFiles.map((f) => f.id)); filteredImages = filteredImages.filter((img) => unusedIds.has(img.id)); } // Pagination const startIndex = (pageNum - 1) * limitNum; const endIndex = startIndex + limitNum; const paginatedImages = filteredImages.slice(startIndex, endIndex); res.json({ images: paginatedImages, pagination: { page: pageNum, limit: limitNum, total: filteredImages.length, totalPages: Math.ceil(filteredImages.length / limitNum), }, }); }); // Get single image details this.app.get("/api/images/:id", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } const { id } = req.params; const image = this.data.images.find((img) => img.id === id); if (!image) { return res.status(404).json({ error: "Image not found" }); } // Get usage information const usageInfo = this.data.usage.usedFiles.get(id); const isUnused = this.data.usage.unusedFiles.some((f) => f.id === id); res.json({ image, usage: usageInfo || null, isUnused, duplicates: this.data.duplicates.find((group) => group.files.some((f) => f.id === id)) || null, }); }); // Get project statistics this.app.get("/api/stats", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } res.json(this.data.stats); }); // Get duplicate groups this.app.get("/api/duplicates", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } res.json(this.data.duplicates); }); // Get project configuration this.app.get("/api/config", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } res.json(this.data.config); }); // Generate code for specific image and framework this.app.post("/api/generate-code", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } const { imageId, framework, codeType = "usage" } = req.body; if (!imageId || !framework) { return res.status(400).json({ error: "Missing required parameters: imageId and framework", }); } const image = this.data.images.find((img) => img.id === imageId); if (!image) { return res.status(404).json({ error: "Image not found" }); } try { const generatedCode = this.generateCodeForImage(image, framework, codeType); res.json({ image: { id: image.id, name: image.name, path: image.relativePath, }, framework, codeType, code: generatedCode, }); } catch (error) { res.status(500).json({ error: "Failed to generate code", details: error instanceof Error ? error.message : "Unknown error", }); } }); // Get usage analysis for specific image this.app.get("/api/usage/:id", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } const { id } = req.params; // Check if the image exists first const image = this.data.images.find((img) => img.id === id); if (!image) { return res.status(404).json({ error: "Image not found" }); } const usageInfo = this.data.usage.usedFiles.get(id); const isUnused = this.data.usage.unusedFiles.some((f) => f.id === id); res.json({ imageId: id, isUnused, usageInfo: usageInfo || null, references: usageInfo?.references || [], }); }); // Search images with advanced filtering this.app.get("/api/search", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } const { q: query, category, type, unused, minSize, maxSize, sortBy = "name", sortOrder = "asc", page = "1", limit = "20", } = req.query; let filteredImages = [...this.data.images]; // Text search if (query) { const searchTerm = query.toLowerCase(); filteredImages = filteredImages.filter((img) => img.name.toLowerCase().includes(searchTerm) || img.relativePath.toLowerCase().includes(searchTerm) || img.category.toLowerCase().includes(searchTerm)); } // Category filter if (category && category !== "all") { filteredImages = filteredImages.filter((img) => img.category === category); } // Type filter if (type && type !== "all") { filteredImages = filteredImages.filter((img) => img.extension === type); } // Unused filter if (unused === "true") { const unusedIds = new Set(this.data.usage.unusedFiles.map((f) => f.id)); filteredImages = filteredImages.filter((img) => unusedIds.has(img.id)); } // Size filters if (minSize) { const minSizeNum = parseInt(minSize, 10); filteredImages = filteredImages.filter((img) => img.size >= minSizeNum); } if (maxSize) { const maxSizeNum = parseInt(maxSize, 10); filteredImages = filteredImages.filter((img) => img.size <= maxSizeNum); } // Sorting filteredImages.sort((a, b) => { let aValue, bValue; switch (sortBy) { case "name": aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); break; case "size": aValue = a.size; bValue = b.size; break; case "modified": aValue = a.modifiedAt.getTime(); bValue = b.modifiedAt.getTime(); break; case "category": aValue = a.category.toLowerCase(); bValue = b.category.toLowerCase(); break; default: aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); } if (aValue < bValue) return sortOrder === "asc" ? -1 : 1; if (aValue > bValue) return sortOrder === "asc" ? 1 : -1; return 0; }); // Pagination const pageNum = parseInt(page, 10); const limitNum = parseInt(limit, 10); const startIndex = (pageNum - 1) * limitNum; const endIndex = startIndex + limitNum; const paginatedImages = filteredImages.slice(startIndex, endIndex); res.json({ query: { text: query || "", category, type, unused, minSize, maxSize, sortBy, sortOrder, }, results: paginatedImages, pagination: { page: pageNum, limit: limitNum, total: filteredImages.length, totalPages: Math.ceil(filteredImages.length / limitNum), }, }); }); // Diagnostic endpoints for category consistency this.app.get("/api/diagnostic/categories", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } try { // TODO: Re-implement consistency validation when needed // const validation = // this.consistencyValidator.validateCategoryStatistics(this.data); // const audit = this.consistencyValidator.auditCategoryAssignments( // this.data.images // ); res.json({ // validation, // audit, timestamp: new Date().toISOString(), summary: { totalImages: this.data.images.length, categoriesInStats: Object.keys(this.data.stats.categoryBreakdown || {}).length, // actualCategories: audit.categoriesAudited.length, // isConsistent: validation.isConsistent, // issuesFound: validation.totalIssues + audit.issues.length, }, }); } catch (error) { res.status(500).json({ error: "Failed to run diagnostic", details: error instanceof Error ? error.message : "Unknown error", }); } }); // Diagnostic report endpoint this.app.get("/api/diagnostic/report", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } try { // TODO: Re-implement diagnostic report when needed // const report = this.consistencyValidator.createDiagnosticReport( // this.data // ); res.setHeader("Content-Type", "text/plain"); res.send("Diagnostic report temporarily unavailable"); } catch (error) { res.status(500).json({ error: "Failed to generate diagnostic report", details: error instanceof Error ? error.message : "Unknown error", }); } }); // Manual reconciliation endpoint this.app.post("/api/diagnostic/reconcile", (req, res) => { if (!this.data) { return res.status(503).json({ error: "Server data not available" }); } try { // TODO: Re-implement reconciliation when needed // const reconciledData = this.consistencyValidator.reconcileStatistics( // this.data // ); // this.data = reconciledData; // Broadcast updated data to WebSocket clients this.broadcast("data-updated", { stats: this.data.stats, imageCount: this.data.images.length, }); res.json({ success: true, message: "Statistics reconciliation temporarily disabled", // updatedStats: reconciledData.stats.categoryBreakdown, }); } catch (error) { res.status(500).json({ error: "Failed to reconcile statistics", details: error instanceof Error ? error.message : "Unknown error", }); } }); } setupFrontendRoutes() { // Serve embedded web assets this.app.get("*", (req, res) => { // Skip API routes and static assets if (req.path.startsWith("/api/") || req.path.startsWith("/project-assets/")) { return res.status(404).json({ error: "Endpoint not found" }); } if (webAssets && getAsset && getAssetMimeType) { // Production mode: serve embedded assets let assetPath = req.path === "/" ? "index.html" : req.path.slice(1); // Try to get the asset let asset = getAsset(assetPath); // If not found and it's not a file extension, try index.html (SPA routing) if (!asset && !path_1.default.extname(assetPath)) { assetPath = "index.html"; asset = getAsset(assetPath); } if (asset) { const mimeType = getAssetMimeType(assetPath); res.setHeader("Content-Type", mimeType); if (mimeType.startsWith("text/") || mimeType.includes("javascript") || mimeType.includes("json")) { res.setHeader("Cache-Control", "no-cache"); } else { res.setHeader("Cache-Control", "public, max-age=31536000"); } return res.send(asset); } } // Development mode or asset not found: try to serve from web/dist const webDistPath = path_1.default.join(__dirname, "../../web/dist"); if (req.path === "/") { // Try to serve index.html from web/dist const indexPath = path_1.default.join(webDistPath, "index.html"); if (require("fs").existsSync(indexPath)) { return res.sendFile(indexPath); } } else { // Try to serve static assets from web/dist const assetPath = path_1.default.join(webDistPath, req.path); if (require("fs").existsSync(assetPath)) { return res.sendFile(assetPath); } } // If web/dist doesn't exist, serve a simple placeholder that loads the React app if (req.path === "/") { res.send(` <!DOCTYPE html> <html> <head> <title>Image Asset Manager</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="https://unpkg.com/react@18/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script src="https://cdn.tailwindcss.com"></script> <style> body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } </style> </head> <body> <div id="root"></div> <script type="text/babel"> const { useState, useEffect } = React; function ImageGrid({ images }) { const [selectedImage, setSelectedImage] = useState(null); if (!images || images.length === 0) { return ( <div className="text-center py-12"> <div className="text-gray-400 mb-4"> <svg className="w-24 h-24 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> </svg> </div> <h3 className="text-lg font-medium text-gray-900 mb-2">No images found</h3> <p className="text-gray-500">Loading images...</p> </div> ); } return ( <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 p-6"> {images.map((image) => ( <div key={image.id} className="group relative bg-white rounded-lg border border-gray-200 hover:border-gray-300 hover:shadow-md transition-all duration-200 cursor-pointer" onClick={() => setSelectedImage(image)}> <div className="aspect-square bg-gray-50 rounded-t-lg overflow-hidden"> <img src={image.url || image.path} alt={image.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200" onError={(e) => { e.target.style.display = 'none'; e.target.parentElement.innerHTML = '<div class="w-full h-full flex items-center justify-center bg-gray-100"><span class="text-gray-400 text-xs">' + image.extension.toUpperCase() + '</span></div>'; }} /> </div> <div className="p-3"> <div className="flex items-center justify-between mb-2"> <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> {image.extension.toUpperCase()} </span> </div> <h3 className="text-sm font-medium text-gray-900 truncate mb-1" title={image.name}> {image.name} </h3> <div className="text-xs text-gray-500"> {Math.round(image.size / 1024)} KB </div> </div> </div> ))} </div> ); } function App() { const [images, setImages] = useState([]); const [loading, setLoading] = useState(true); const [stats, setStats] = useState(null); useEffect(() => { Promise.all([ fetch('/api/images?limit=100').then(r => r.json()), fetch('/api/stats').then(r => r.json()) ]).then(([imagesData, statsData]) => { setImages(imagesData.images || []); setStats(statsData); setLoading(false); }).catch(error => { console.error('Error loading data:', error); setLoading(false); }); }, []); if (loading) { return ( <div className="flex items-center justify-center h-screen"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <span className="ml-3 text-gray-600">Loading images...</span> </div> ); } return ( <div className="min-h-screen bg-gray-50"> <header className="bg-white shadow-sm border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between items-center py-4"> <div className="flex items-center"> <h1 className="text-2xl font-bold text-gray-900">πŸ–ΌοΈ Image Asset Manager</h1> </div> {stats && ( <div className="flex items-center space-x-6 text-sm text-gray-600"> <span>{stats.totalImages} images</span> <span>{Math.round(stats.totalSize / 1024 / 1024)} MB</span> </div> )} </div> </div> </header> <main> <ImageGrid images={images} /> </main> </div> ); } ReactDOM.render(<App />, document.getElementById('root')); </script> </body> </html> `); } else { res.status(404).json({ error: "Asset not found" }); } }); } generateCodeForImage(image, framework, codeType) { const relativePath = image.relativePath; const name = image.name.replace(/\.[^/.]+$/, ""); // Remove extension const extension = image.extension.toLowerCase(); // Generate different types of code based on framework and codeType switch (framework.toLowerCase()) { case "react": return this.generateReactCode(image, codeType); case "vue": return this.generateVueCode(image, codeType); case "html": return this.generateHtmlCode(image, codeType); case "angular": return this.generateAngularCode(image, codeType); case "svelte": return this.generateSvelteCode(image, codeType); default: throw new Error(`Unsupported framework: ${framework}`); } } generateReactCode(image, codeType) { const importName = this.toCamelCase(image.name.replace(/\.[^/.]+$/, "")); const relativePath = image.relativePath; switch (codeType) { case "import": return { import: `import ${importName} from './${relativePath}';`, usage: `<img src={${importName}} alt="${image.name}" />`, }; case "component": if (image.extension.toLowerCase() === "svg") { return { import: `import { ReactComponent as ${importName}Icon } from './${relativePath}';`, usage: `<${importName}Icon />`, component: `const ${importName}Icon = () => <${importName}Icon />;`, }; } return this.generateReactCode(image, "import"); case "inline": if (image.extension.toLowerCase() === "svg") { return { usage: `// SVG content would be inlined here`, note: "SVG content should be copied directly into JSX", }; } return this.generateReactCode(image, "import"); default: return this.generateReactCode(image, "import"); } } generateVueCode(image, codeType) { const relativePath = image.relativePath; switch (codeType) { case "import": return { import: `import imageUrl from './${relativePath}';`, usage: `<img :src="imageUrl" alt="${image.name}" />`, }; case "component": if (image.extension.toLowerCase() === "svg") { const componentName = this.toPascalCase(image.name.replace(/\.[^/.]+$/, "")); return { import: `import ${componentName} from './${relativePath}';`, usage: `<${componentName} />`, component: `<template>\n <${componentName} />\n</template>`, }; } return this.generateVueCode(image, "import"); case "inline": return { usage: `<img src="./${relativePath}" alt="${image.name}" />`, }; default: return this.generateVueCode(image, "import"); } } generateHtmlCode(image, codeType) { const relativePath = image.relativePath; return { usage: `<img src="./${relativePath}" alt="${image.name}" />`, inline: image.extension.toLowerCase() === "svg" ? `<!-- SVG content would be inlined here -->` : `<img src="./${relativePath}" alt="${image.name}" />`, }; } generateAngularCode(image, codeType) { const relativePath = image.relativePath; switch (codeType) { case "import": return { usage: `<img src="assets/${relativePath}" alt="${image.name}" />`, note: "Place image in src/assets/ directory", }; case "component": if (image.extension.toLowerCase() === "svg") { return { usage: `<img src="assets/${relativePath}" alt="${image.name}" />`, note: "For SVG icons, consider using Angular Material Icons or custom SVG components", }; } return this.generateAngularCode(image, "import"); default: return this.generateAngularCode(image, "import"); } } generateSvelteCode(image, codeType) { const relativePath = image.relativePath; switch (codeType) { case "import": return { import: `import imageUrl from './${relativePath}';`, usage: `<img src={imageUrl} alt="${image.name}" />`, }; case "inline": return { usage: `<img src="./${relativePath}" alt="${image.name}" />`, }; default: return this.generateSvelteCode(image, "import"); } } toCamelCase(str) { return str.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : "")); } toPascalCase(str) { const camelCase = this.toCamelCase(str); return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); } async start(port, data) { //ε¦‚ζžœη«―ε£θ’«ε η”¨, εˆ™ζ€ζŽ‰εŽŸθΏ›η¨‹ this.port = await this.findAvailablePort(port); this.data = data; // Set up static file serving for the project images if (data.config && data.config.projectPath) { // Add static file serving for project assets this.app.use("/project-assets", express_1.default.static(data.config.projectPath, { setHeaders: (res, filePath) => { // Set appropriate headers for image files if (filePath.match(/\.(jpg|jpeg|png|gif|svg|webp|ico)$/i)) { res.setHeader("Cache-Control", "public, max-age=3600"); res.setHeader("Access-Control-Allow-Origin", "*"); // Set correct MIME type for SVG files if (filePath.match(/\.svg$/i)) { res.setHeader("Content-Type", "image/svg+xml"); } } }, })); } // Set up frontend routes (must be last to avoid conflicts) this.setupFrontendRoutes(); return new Promise((resolve, reject) => { try { this.server = (0, http_1.createServer)(this.app); this.server.listen(this.port, () => { this.setupWebSocket(); resolve(); }); this.server.on("error", (error) => { if (error.code === "EADDRINUSE") { reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${this.port} is already in use`, { port: this.port }, true)); } else { reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to start server: ${error.message}`, error, false)); } }); } catch (error) { reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to create server: ${error instanceof Error ? error.message : "Unknown error"}`, error, false)); } }); } async stop() { return new Promise((resolve) => { // Close WebSocket connections if (this.wss) { this.clients.forEach((client) => { if (client.readyState === ws_1.WebSocket.OPEN) { client.close(); } }); this.wss.close(); this.wss = null; } // Close HTTP server if (this.server) { this.server.close(() => { this.server = null; resolve(); }); } else { resolve(); } }); } setupWebSocket() { if (!this.server) return; this.wss = new ws_1.WebSocketServer({ server: this.server }); this.wss.on("connection", (ws) => { this.clients.add(ws); // Send initial data if (this.data) { ws.send(JSON.stringify({ type: "initial-data", data: { stats: this.data.stats, imageCount: this.data.images.length, }, })); } ws.on("close", () => { this.clients.delete(ws); }); ws.on("error", (error) => { console.error("πŸ“‘ WebSocket error:", error); this.clients.delete(ws); }); }); } broadcast(event, data) { if (!this.wss) return; const message = JSON.stringify({ type: event, data }); this.clients.forEach((client) => { if (client.readyState === ws_1.WebSocket.OPEN) { try { client.send(message); } catch (error) { console.error("πŸ“‘ Failed to send WebSocket message:", error); this.clients.delete(client); } } }); } async findAvailablePort(startPort) { const net = await Promise.resolve().then(() => __importStar(require("net"))); const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process"))); return new Promise((resolve, reject) => { const server = net.createServer(); server.listen(startPort, () => { const port = server.address()?.port || startPort; server.close(() => resolve(port)); }); server.on("error", async (error) => { if (error.code === "EADDRINUSE") { console.log(`πŸ” Port ${startPort} is in use, attempting to kill existing process...`); try { // Find process using the port const lsofOutput = execSync(`lsof -ti :${startPort}`, { encoding: "utf8", }).trim(); if (lsofOutput) { const pids = lsofOutput.split("\n").filter((pid) => pid.trim()); for (const pid of pids) { console.log(`πŸ’€ Killing process ${pid} using port ${startPort}`); execSync(`kill -9 ${pid}`); } // Wait a moment for the process to be killed await new Promise((resolve) => setTimeout(resolve, 1000)); // Try to use the port again const retryServer = net.createServer(); retryServer.listen(startPort, () => { const port = retryServer.address()?.port || startPort; retryServer.close(() => { console.log(`βœ… Successfully freed and using port ${startPort}`); resolve(port); }); }); retryServer.on("error", (retryError) => { // If still can't use the port after killing, reject with error console.log(`❌ Port ${startPort} still in use after killing process`); reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${startPort} is still in use after attempting to kill existing process`, { port: startPort }, true)); }); } else { // No process found but port still in use reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Port ${startPort} is in use but no process found`, { port: startPort }, true)); } } catch (killError) { console.log(`⚠️ Could not kill process on port ${startPort}:`, killError); reject(new types_1.ImageAssetError(types_1.ErrorCode.SERVER_START_FAILED, `Failed to kill process using port ${startPort}: ${killError instanceof Error ? killError.message : "Unknown error"}`, { port: startPort, killError }, true)); } } else { reject(error); } }); }); } // Getter for current port (useful for testing) getPort() { return this.port; } // Getter for server status isRunning() { return this.server !== null && this.server.listening; } // Update server data (for real-time updates) updateData(data) { this.data = data; this.broadcast("data-updated", { stats: data.stats, imageCount: data.images.length, }); } } exports.WebServer = WebServer; //# sourceMappingURL=WebServer.js.map