UNPKG

@deskthing/cli

Version:
1,469 lines (1,442 loc) 48.1 kB
// src/config/deskthing.config.ts import { join, resolve } from "path"; import { createRequire } from "module"; import { readFile } from "fs/promises"; import { pathToFileURL } from "url"; import { existsSync } from "fs"; var defaultConfig = { development: { logging: { level: "info", prefix: "[DeskThing Server]" }, client: { logging: { level: "info", prefix: "[DeskThing Client]", enableRemoteLogging: true }, clientPort: 3e3, viteLocation: "http://localhost", vitePort: 5173, linkPort: 8080 }, server: { editCooldownMs: 1e3, refreshInterval: 0 } } }; async function loadTsConfig(path2, debug = false) { try { const require2 = createRequire(import.meta.url); try { require2("ts-node/register"); } catch (e) { if (debug) console.log(`ts-node not available, continuing anyway...`); } try { const configModule = require2(path2); return configModule.default || configModule; } catch (e) { if (debug) console.error( "\x1B[91mError loading TypeScript config:", path2, e, "\x1B[0m" ); throw e; } } catch (error) { if (debug) console.error("\x1B[91mError loading config:", error, "\x1B[0m"); throw error; } } var manuallyParseConfig = async (path2, debug = false) => { try { if (debug) console.log( `(debug mode enabled) Manually loading config from ${path2} file and parsing manually (may cause errors)` ); const fileContent = await readFile(path2, "utf-8"); const configMatch = fileContent.match(/defineConfig\s*\(\s*({[\s\S]*?})\s*\)/); if (!configMatch) { if (debug) console.log("Could not find config definition in file using regex"); const objectMatch = fileContent.match(/\{[\s\S]*development[\s\S]*\}/); if (objectMatch) { try { let jsonStr = objectMatch[0].replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"').replace(/'/g, '"').replace(/,(\s*[}\]])/g, "$1").replace(/\/\/.*$/gm, ""); jsonStr = jsonStr.replace( /process\.env\.[A-Z_]+/g, '"ENV_PLACEHOLDER"' ); const config = JSON.parse(jsonStr); if (debug) console.log("Successfully extracted config using fallback method"); return config; } catch (e) { if (debug) console.log("Failed to parse extracted object:", e); } } throw new Error("Could not find or parse config definition in file"); } try { const configStr = configMatch[1]?.trim() || configMatch[2]?.trim(); if (debug) console.log("Found config string, attempting to evaluate safely"); if (debug) console.log(configStr); const cleanConfigStr = configStr?.replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"').replace(/,(\s*[}\]])/g, "$1").replace(/\/\/.*$/gm, ""); const config = JSON.parse(cleanConfigStr); return config; } catch (parseError) { if (debug) console.error("\x1B[91mError parsing config:", parseError, "\x1B[0m"); throw parseError; } } catch (e) { if (debug) console.error( "\x1B[91m(debug mode enabled) Error loading config:", e, "\x1B[0m" ); throw e; } }; async function parseTypeScriptConfig(filePath, debug = false) { try { const content = await readFile(filePath, "utf-8"); const configMatch = content.match(/defineConfig\s*\(\s*({[\s\S]*?})\s*\)/); if (!configMatch || !configMatch[1]) { return null; } let configStr = configMatch[1].replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"').replace(/,(\s*[}\]])/g, "$1").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); try { const fn = new Function(`return ${configStr}`); return fn(); } catch (evalError) { if (debug) console.log("Failed to evaluate config as JavaScript:", evalError); try { configStr = configStr.replace(/'/g, '"'); return JSON.parse(configStr); } catch (jsonError) { if (debug) console.log("Failed to parse as JSON:", jsonError); return null; } } } catch (error) { if (debug) console.log("Error reading or parsing config file:", error); return null; } } var directConfigImport = async (path2, debug = false) => { try { if (debug) console.log( `(debug mode enabled) Loading config from ${path2} file...` ); const configModule = await import(`${path2}`); if (debug) console.log(`Config loaded successfully from ${path2}`); return configModule.default || configModule; } catch (error) { if (debug) console.log("Direct import failed:", error); throw error; } }; var directConfigImportUrl = async (path2, debug = false) => { try { const tsConfigPath = pathToFileURL(path2).href; if (debug) console.log( `(debug mode enabled) Loading config from ${tsConfigPath} file...` ); const configModule = await import(tsConfigPath); if (debug) console.log(`Config loaded successfully from ${tsConfigPath}`); return configModule.default || configModule; } catch (error) { if (debug) console.log("Direct import failed:", error); throw error; } }; var getConfigFromFile = async (debug = false) => { try { let rootUrl = resolve(process.cwd(), "deskthing.config.ts"); if (!existsSync(rootUrl)) { if (debug) console.log( "deskthing.config.ts not found, trying deskthing.config.js" ); rootUrl = resolve(process.cwd(), "deskthing.config.js"); if (!existsSync(rootUrl)) { throw new Error("No config file found (tried both TS and JS files)"); } } try { const config = await directConfigImport(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (importError) { if (debug) console.log(`Direct import failed, trying alternative method...`); } try { const config = await directConfigImportUrl(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (importError) { if (debug) console.log(`Direct import failed, trying alternative method...`); } try { const config = await loadTsConfig(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (e) { if (debug) console.error("\x1B[91mError loading TS config:", e, "\x1B[0m"); } if (debug) console.log("Trying to parse config manually..."); try { const config = await manuallyParseConfig(rootUrl, debug); return config; } catch (e) { if (debug) console.error("\x1B[91mError parsing config manually:", e, "\x1B[0m"); } if (debug) console.log("Trying to parse config and run as js..."); try { const config = await parseTypeScriptConfig(rootUrl, debug); return config; } catch (error) { if (debug) console.log("Error running as js:", error); } } catch (e) { if (debug) console.error( "\x1B[91m(debug mode) Error loading config. Does it exist? :", e, "\x1B[0m" ); return defaultConfig; } }; var DeskThingConfig = defaultConfig; function isObject(item) { return item && typeof item === "object" && !Array.isArray(item); } function deepmerge(target, source) { if (!isObject(target) || !isObject(source)) { return source; } const output = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key]) && isObject(target[key])) { output[key] = deepmerge(target[key], source[key]); } else { output[key] = source[key]; } } } return output; } var initConfig = async (options = { silent: false, debug: false }) => { try { const userConfig = await getConfigFromFile(options.debug); DeskThingConfig = deepmerge(defaultConfig, userConfig || {}); if (!options.silent || options.debug) { if (userConfig) { console.log(` \x1B[32m\u2705 Config Loaded\x1B[0m `); } else { console.log(` \x1B[32m\u2705 No Config Found, Using Default\x1B[0m`); if (options.debug) { console.log( `\x1B[3m\x1B[90mPath Checked: ${join( process.cwd(), "deskthing.config.ts" )}\x1B[0m ` ); } } } } catch (e) { console.warn("\x1B[93mWarning: Error loading config, using defaults\x1B[0m"); if (options.debug) console.error("\x1B[91mError loading config:", e, "\x1B[0m"); DeskThingConfig = defaultConfig; } }; var serverConfig = { ...defaultConfig }; // src/emulator/client/client.ts import { createServer } from "http"; import { existsSync as existsSync2, readFileSync, statSync } from "fs"; import { extname, join as join2, normalize } from "path"; import { fileURLToPath } from "url"; // node_modules/@deskthing/types/dist/clients/clientData.js var ClientConnectionMethod; (function(ClientConnectionMethod2) { ClientConnectionMethod2[ClientConnectionMethod2["Unknown"] = 0] = "Unknown"; ClientConnectionMethod2[ClientConnectionMethod2["LAN"] = 1] = "LAN"; ClientConnectionMethod2[ClientConnectionMethod2["Localhost"] = 2] = "Localhost"; ClientConnectionMethod2[ClientConnectionMethod2["ADB"] = 3] = "ADB"; ClientConnectionMethod2[ClientConnectionMethod2["NDIS"] = 4] = "NDIS"; ClientConnectionMethod2[ClientConnectionMethod2["Bluetooth"] = 5] = "Bluetooth"; ClientConnectionMethod2[ClientConnectionMethod2["Internet"] = 6] = "Internet"; })(ClientConnectionMethod || (ClientConnectionMethod = {})); var PlatformIDs; (function(PlatformIDs2) { PlatformIDs2["ADB"] = "adb"; PlatformIDs2["WEBSOCKET"] = "websocket"; PlatformIDs2["BLUETOOTH"] = "bluetooth"; PlatformIDs2["MAIN"] = "main"; })(PlatformIDs || (PlatformIDs = {})); var ClientPlatformIDs; (function(ClientPlatformIDs2) { ClientPlatformIDs2[ClientPlatformIDs2["Unknown"] = 0] = "Unknown"; ClientPlatformIDs2[ClientPlatformIDs2["Desktop"] = 1] = "Desktop"; ClientPlatformIDs2[ClientPlatformIDs2["Tablet"] = 2] = "Tablet"; ClientPlatformIDs2[ClientPlatformIDs2["Iphone"] = 3] = "Iphone"; ClientPlatformIDs2[ClientPlatformIDs2["CarThing"] = 4] = "CarThing"; })(ClientPlatformIDs || (ClientPlatformIDs = {})); var ConnectionState; (function(ConnectionState2) { ConnectionState2[ConnectionState2["Connected"] = 0] = "Connected"; ConnectionState2[ConnectionState2["Established"] = 1] = "Established"; ConnectionState2[ConnectionState2["Connecting"] = 2] = "Connecting"; ConnectionState2[ConnectionState2["Disconnecting"] = 3] = "Disconnecting"; ConnectionState2[ConnectionState2["Disconnected"] = 4] = "Disconnected"; ConnectionState2[ConnectionState2["Failed"] = 5] = "Failed"; })(ConnectionState || (ConnectionState = {})); var ProviderCapabilities; (function(ProviderCapabilities2) { ProviderCapabilities2[ProviderCapabilities2["CONFIGURE"] = 0] = "CONFIGURE"; ProviderCapabilities2[ProviderCapabilities2["PING"] = 1] = "PING"; ProviderCapabilities2[ProviderCapabilities2["COMMUNICATE"] = 2] = "COMMUNICATE"; })(ProviderCapabilities || (ProviderCapabilities = {})); var ViewMode; (function(ViewMode2) { ViewMode2["HIDDEN"] = "hidden"; ViewMode2["PEEK"] = "peek"; ViewMode2["FULL"] = "full"; })(ViewMode || (ViewMode = {})); var VolMode; (function(VolMode2) { VolMode2["WHEEL"] = "wheel"; VolMode2["SLIDER"] = "slider"; VolMode2["BAR"] = "bar"; })(VolMode || (VolMode = {})); // node_modules/@deskthing/types/dist/clients/clientTransit.js var CLIENT_REQUESTS; (function(CLIENT_REQUESTS2) { CLIENT_REQUESTS2["GET"] = "get"; CLIENT_REQUESTS2["ACTION"] = "action"; CLIENT_REQUESTS2["BUTTON"] = "button"; CLIENT_REQUESTS2["KEY"] = "key"; CLIENT_REQUESTS2["LOG"] = "log"; })(CLIENT_REQUESTS || (CLIENT_REQUESTS = {})); var DEVICE_CLIENT; (function(DEVICE_CLIENT2) { DEVICE_CLIENT2["MANIFEST"] = "manifest"; DEVICE_CLIENT2["MUSIC"] = "music"; DEVICE_CLIENT2["SETTINGS"] = "settings"; DEVICE_CLIENT2["APPS"] = "apps"; DEVICE_CLIENT2["ACTION"] = "action"; DEVICE_CLIENT2["TIME"] = "time"; DEVICE_CLIENT2["ICON"] = "icon"; })(DEVICE_CLIENT || (DEVICE_CLIENT = {})); var DEVICE_DESKTHING; (function(DEVICE_DESKTHING2) { DEVICE_DESKTHING2["ACTION"] = "action"; DEVICE_DESKTHING2["SET"] = "set"; DEVICE_DESKTHING2["GET"] = "get"; DEVICE_DESKTHING2["PING"] = "ping"; DEVICE_DESKTHING2["PONG"] = "pong"; DEVICE_DESKTHING2["LOG"] = "log"; DEVICE_DESKTHING2["VIEW"] = "view"; DEVICE_DESKTHING2["APP_PAYLOAD"] = "app_payload"; DEVICE_DESKTHING2["MANIFEST"] = "manifest"; DEVICE_DESKTHING2["SETTINGS"] = "settings"; DEVICE_DESKTHING2["CONFIG"] = "config"; })(DEVICE_DESKTHING || (DEVICE_DESKTHING = {})); // node_modules/@deskthing/types/dist/apps/appTransit.js var APP_REQUESTS; (function(APP_REQUESTS2) { APP_REQUESTS2["DEFAULT"] = "default"; APP_REQUESTS2["GET"] = "get"; APP_REQUESTS2["SET"] = "set"; APP_REQUESTS2["DELETE"] = "delete"; APP_REQUESTS2["OPEN"] = "open"; APP_REQUESTS2["SEND"] = "send"; APP_REQUESTS2["TOAPP"] = "toApp"; APP_REQUESTS2["LOG"] = "log"; APP_REQUESTS2["KEY"] = "key"; APP_REQUESTS2["ACTION"] = "action"; APP_REQUESTS2["TASK"] = "task"; APP_REQUESTS2["STEP"] = "step"; APP_REQUESTS2["SONG"] = "song"; })(APP_REQUESTS || (APP_REQUESTS = {})); // node_modules/@deskthing/types/dist/meta/logging.js var LOGGING_LEVELS; (function(LOGGING_LEVELS2) { LOGGING_LEVELS2["MESSAGE"] = "message"; LOGGING_LEVELS2["LOG"] = "log"; LOGGING_LEVELS2["WARN"] = "warning"; LOGGING_LEVELS2["ERROR"] = "error"; LOGGING_LEVELS2["DEBUG"] = "debugging"; LOGGING_LEVELS2["FATAL"] = "fatal"; })(LOGGING_LEVELS || (LOGGING_LEVELS = {})); // node_modules/@deskthing/types/dist/meta/music.js var SongAbilities; (function(SongAbilities2) { SongAbilities2["LIKE"] = "like"; SongAbilities2["SHUFFLE"] = "shuffle"; SongAbilities2["REPEAT"] = "repeat"; SongAbilities2["PLAY"] = "play"; SongAbilities2["PAUSE"] = "pause"; SongAbilities2["STOP"] = "stop"; SongAbilities2["NEXT"] = "next"; SongAbilities2["PREVIOUS"] = "previous"; SongAbilities2["REWIND"] = "rewind"; SongAbilities2["FAST_FORWARD"] = "fast_forward"; SongAbilities2["CHANGE_VOLUME"] = "change_volume"; SongAbilities2["SET_OUTPUT"] = "set_output"; })(SongAbilities || (SongAbilities = {})); var AUDIO_REQUESTS; (function(AUDIO_REQUESTS2) { AUDIO_REQUESTS2["NEXT"] = "next"; AUDIO_REQUESTS2["PREVIOUS"] = "previous"; AUDIO_REQUESTS2["REWIND"] = "rewind"; AUDIO_REQUESTS2["FAST_FORWARD"] = "fast_forward"; AUDIO_REQUESTS2["PLAY"] = "play"; AUDIO_REQUESTS2["PAUSE"] = "pause"; AUDIO_REQUESTS2["STOP"] = "stop"; AUDIO_REQUESTS2["SEEK"] = "seek"; AUDIO_REQUESTS2["LIKE"] = "like"; AUDIO_REQUESTS2["SONG"] = "song"; AUDIO_REQUESTS2["VOLUME"] = "volume"; AUDIO_REQUESTS2["REPEAT"] = "repeat"; AUDIO_REQUESTS2["SHUFFLE"] = "shuffle"; AUDIO_REQUESTS2["REFRESH"] = "refresh"; })(AUDIO_REQUESTS || (AUDIO_REQUESTS = {})); var SongEvent; (function(SongEvent3) { SongEvent3["GET"] = "get"; SongEvent3["SET"] = "set"; })(SongEvent || (SongEvent = {})); // node_modules/@deskthing/types/dist/deskthing/deskthingTransit.js var DESKTHING_DEVICE; (function(DESKTHING_DEVICE2) { DESKTHING_DEVICE2["GLOBAL_SETTINGS"] = "global_settings"; DESKTHING_DEVICE2["MAPPINGS"] = "button_mappings"; DESKTHING_DEVICE2["CONFIG"] = "configuration"; DESKTHING_DEVICE2["GET"] = "get"; DESKTHING_DEVICE2["ERROR"] = "error"; DESKTHING_DEVICE2["PONG"] = "pong"; DESKTHING_DEVICE2["PING"] = "ping"; DESKTHING_DEVICE2["SETTINGS"] = "settings"; DESKTHING_DEVICE2["APPS"] = "apps"; DESKTHING_DEVICE2["TIME"] = "time"; DESKTHING_DEVICE2["HEARTBEAT"] = "heartbeat"; DESKTHING_DEVICE2["META_DATA"] = "meta_data"; DESKTHING_DEVICE2["MUSIC"] = "music"; DESKTHING_DEVICE2["ICON"] = "icon"; })(DESKTHING_DEVICE || (DESKTHING_DEVICE = {})); var DESKTHING_EVENTS; (function(DESKTHING_EVENTS2) { DESKTHING_EVENTS2["MESSAGE"] = "message"; DESKTHING_EVENTS2["DATA"] = "data"; DESKTHING_EVENTS2["APPDATA"] = "appdata"; DESKTHING_EVENTS2["CALLBACK_DATA"] = "callback-data"; DESKTHING_EVENTS2["START"] = "start"; DESKTHING_EVENTS2["STOP"] = "stop"; DESKTHING_EVENTS2["PURGE"] = "purge"; DESKTHING_EVENTS2["INPUT"] = "input"; DESKTHING_EVENTS2["ACTION"] = "action"; DESKTHING_EVENTS2["CONFIG"] = "config"; DESKTHING_EVENTS2["SETTINGS"] = "settings"; DESKTHING_EVENTS2["TASKS"] = "tasks"; DESKTHING_EVENTS2["CLIENT_STATUS"] = "client_status"; })(DESKTHING_EVENTS || (DESKTHING_EVENTS = {})); // src/emulator/services/logger.ts var Logger = class { static shouldLog(msgLevel) { const levels = ["silent", "error", "warn", "info", "debug"]; const msgLevelIndex = levels.indexOf(msgLevel); const configLevelIndex = levels.indexOf(DeskThingConfig.development.logging.level); if (msgLevelIndex === -1 || configLevelIndex === -1) return true; return msgLevelIndex <= configLevelIndex; } static log(level, ...args) { const config = DeskThingConfig.development.logging; const loggingLevel = config.level; const prefix = config.prefix; if (this.shouldLog(level)) { switch (level) { case "debug": console.debug("\x1B[36m%s\x1B[0m", `${prefix} ${args[0]}`, ...args.slice(1)); break; case "info": console.info("\x1B[90m%s\x1B[0m", `${prefix} ${args[0]}`, ...args.slice(1)); break; case "warn": console.warn("\x1B[33m%s\x1B[0m", `${prefix} ${args[0]}`, ...args.slice(1)); break; case "error": console.error("\x1B[31m%s\x1B[0m", `${prefix} ${args[0]}`, ...args.slice(1)); break; default: console.log(`${prefix} ${args[0]}`, ...args.slice(1)); } } } static debug(...args) { this.log("debug", ...args); } static error(...args) { this.log("error", ...args); } static info(...args) { this.log("info", ...args); } static warn(...args) { this.log("warn", ...args); } static table(data) { console.table(data); } /** * Client logs will always log * @param type * @param message * @param data */ static clientLog(type, message, data) { const prefix = `[App ${type.trim()}] `; const dataStr = data ? ` ${JSON.stringify(data)}` : ""; switch (type) { case LOGGING_LEVELS.LOG: case "info": console.log("\x1B[90m%s\x1B[0m", prefix + message + dataStr); break; case LOGGING_LEVELS.ERROR: console.log("\x1B[31m%s\x1B[0m", prefix + message + dataStr); break; case LOGGING_LEVELS.WARN: case "warn": console.log("\x1B[33m%s\x1B[0m", prefix + message + dataStr); break; case LOGGING_LEVELS.MESSAGE: console.log("\x1B[32m%s\x1B[0m", prefix + message + dataStr); break; case LOGGING_LEVELS.DEBUG: case "debug": console.log("\x1B[36m%s\x1B[0m", prefix + message + dataStr); break; default: console.log("[CLIENT LOGGING: ]", type, message, data); } } }; // src/emulator/server/serverMessageBus.ts import WebSocket, { WebSocketServer } from "ws"; var ServerMessageBus = class { static subscribers = /* @__PURE__ */ new Map(); static ws; static initialize(port = 8080) { if (this.ws) { this.ws.close(); } this.ws = new WebSocketServer({ port }); this.ws.on("connection", (socket) => { socket.on("message", (message) => { const { event, data } = JSON.parse(message.toString()); this.notify(event, data); }); }); } /** * Notifies local listeners * @param event * @param data */ static notify(event, data) { if (this.subscribers.has(event)) { this.subscribers.get(event)?.forEach((callback) => callback(data)); } } static subscribe(event, callback) { if (!this.subscribers.has(event)) { this.subscribers.set(event, []); } this.subscribers.get(event)?.push(callback); return () => this.unsubscribe(event, callback); } /** * Notifies the websocket * @param event * @param data */ static publish(event, data) { this.ws.clients.forEach((client) => { if (client?.readyState === WebSocket.OPEN) { Logger.debug("Sending data through messageBus", event, data); client.send(JSON.stringify({ event, data })); } else { console.error("Unable to send data because readystate of client is ", client?.readyState); } }); } static unsubscribe(event, callback) { const callbacks = this.subscribers.get(event); const index = callbacks?.indexOf(callback); if (callbacks && index !== void 0 && index > -1) { callbacks.splice(index, 1); } } }; // src/emulator/client/callbackService.ts var CallbackService = class { static handleCallback(req, res) { try { const url = new URL(req.url, `http://${req.headers.host}`); Logger.debug("Handling callback with URL: ", url); const code = url.searchParams.get("code"); const appName = url.pathname.split("/callback/")[1]; if (!code || !appName) { res.writeHead(400, { "Content-Type": "application/json" }); res.end( JSON.stringify({ error: "Missing code or app name parameter" }) ); return; } ServerMessageBus.notify("auth:callback", { code, appName }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); } catch (error) { Logger.error("Error handling callback:", error); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Internal server error" })); } } }; // src/emulator/client/client.ts var DevClient = class { async start() { const __dirname = fileURLToPath(new URL(".", import.meta.url)); const staticPath = join2(__dirname, "./template"); Logger.debug(`Static files will be served from: ${staticPath}`); const mimeTypes = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".json": "application/json", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", ".otf": "font/otf" }; const server = createServer((req, res) => { if (req.method !== "GET") { res.statusCode = 405; res.end("Method Not Allowed"); return; } let urlPath = req.url || "/"; if (urlPath === "/") { urlPath = "/index.html"; } if (urlPath.startsWith("/callback")) { CallbackService.handleCallback(req, res); return; } if (urlPath === "/config") { try { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(DeskThingConfig.development.client)); return; } catch (err) { Logger.error("Error serving config:", err); res.statusCode = 500; res.end("Internal Server Error"); return; } } const safePath = normalize(urlPath).replace(/^(\.\.(\/|\\|$))+/, ""); const filePath = join2(staticPath, safePath); Logger.debug(`Serving ${urlPath}`); if (existsSync2(filePath) && statSync(filePath).isFile()) { try { const ext = extname(filePath).toLowerCase(); const contentType = mimeTypes[ext] || "application/octet-stream"; const fileContent = readFileSync(filePath); res.writeHead(200, { "Content-Type": contentType }); res.end(fileContent); } catch (err) { Logger.error(`Error serving ${filePath}:`, err); res.statusCode = 500; res.end("Internal Server Error"); } } else { const indexPath = join2(staticPath, "index.html"); if (existsSync2(indexPath)) { const indexContent = readFileSync(indexPath); res.writeHead(200, { "Content-Type": "text/html" }); res.end(indexContent); } else { res.statusCode = 404; res.end("Not Found"); } } }); const clientPort = DeskThingConfig.development.client.clientPort; server.listen(clientPort, () => { Logger.info( `\x1B[36m\u{1F680} Development Server is running at http://localhost:${clientPort}\x1B[0m` ); Logger.info( `\x1B[33m\u{1F504} Callback Server is running at http://localhost:${clientPort}/callback \x1B[0m` ); }); } }; // src/emulator/server/server.ts import { watch as fsWatch } from "fs"; // src/emulator/server/manifestDetails.ts import path from "path"; import fs from "fs"; var getManifestDetails = () => { const manifestPath = path.join(process.cwd(), "deskthing", "manifest.json"); const altManifestPath = path.join(process.cwd(), "public", "manifest.json"); let finalPath = manifestPath; if (!fs.existsSync(finalPath)) { finalPath = altManifestPath; Logger.error("\u274C Failed to load manifest.json from deskthing/manifest.json, trying public/manifest.json"); if (!fs.existsSync(finalPath)) { throw new Error("\x1B[31m\u274C Failed to load manifest.json from both locations\x1B[0m"); } else { Logger.info("\x1B[32m\u2705 Successfully loaded manifest.json from public/manifest.json\x1B[0m"); } } const manifest = JSON.parse(fs.readFileSync(finalPath, "utf8")); return { id: manifest.id, isWebApp: manifest.isWebApp, requires: manifest.requires, label: manifest.label, version: manifest.version, description: manifest.description, author: manifest.author, platforms: manifest.platforms, homepage: manifest.homepage, version_code: manifest.version_code, compatible_server: manifest.compatible_server, compatible_client: manifest.compatible_client, repository: manifest.repository, tags: manifest.tags, requiredVersions: manifest.requiredVersions }; }; // src/emulator/services/serverService.ts var ServerService = class { constructor() { ServerMessageBus.subscribe("client:request", (data) => { this.handleClientRequest(data); }); } handleClientRequest(data) { Logger.debug(`Received request: ${JSON.stringify(data)}`); switch (data.type) { case "getData": this.sendServerData(); break; case "getManifest": this.sendManifestData(); break; case "getSettings": this.sendSettingsData(); break; case "getClientConfig": this.sendClientConfig(); break; case "log": Logger.clientLog(data.level, "[CLIENT LOG] " + data.message); break; default: if (data.type) { ServerMessageBus.publish("client:response", { type: data.type, payload: data.payload }); } } } sendToClient(data) { ServerMessageBus.publish("client:request", { type: data.type, payload: data.payload, request: data.request, app: data.app, clientId: data.clientId }); } sendServerData() { const data = getServerData(); ServerMessageBus.publish("client:response", { type: "data", payload: data.data }); } sendManifestData() { const data = getManifestDetails(); ServerMessageBus.publish("client:response", { type: "manifest", payload: data }); } sendSettingsData() { const data = getServerData(); ServerMessageBus.publish("client:response", { type: "settings", payload: data.settings }); } async sendClientConfig() { const clientConfig = DeskThingConfig.development.client; ServerMessageBus.publish("client:response", { type: "clientConfig", payload: clientConfig // Send the client section of the config }); } }; // src/emulator/server/coms.ts import { exec } from "child_process"; // src/emulator/services/musicService.ts var MusicService = class { refreshInterval = null; currentSong = null; start() { this.stop(); const interval = DeskThingConfig.development.server.refreshInterval * 1e3; if (interval <= 0) { Logger.debug("Music service refresh disabled (interval <= 0)"); return; } Logger.debug(`Starting music service with ${interval}ms refresh interval`); this.refreshInterval = setInterval(() => { Logger.debug(`Refreshing music data...`); ServerMessageBus.notify("app:data", { type: "get", request: "refresh" }); }, interval); } sendSong() { ServerMessageBus.publish("client:request", { type: DESKTHING_DEVICE.MUSIC, payload: this.currentSong, app: "client" }); } setSong(song) { this.currentSong = song; this.sendSong(); } stop() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; Logger.debug("Music service stopped"); } } }; // src/emulator/server/coms.ts var serverService = new ServerService(); var Data = { data: {}, settings: {} }; var getServerData = () => Data; var handleDataFromApp = async (app, appData) => { if (Object.values(APP_REQUESTS).includes(appData.type)) { try { const handler = handleData[appData.type] || handleData["default"]; const requestHandler = handler[appData.request || "default"] || handler["default"]; if (!requestHandler) { Logger.warn( `No handler found for request ${appData.request} in ${appData.type}` ); handleRequestMissing(app, appData); return; } requestHandler(app, appData); } catch (error) { Logger.error("Error in handleDataFromApp:", error); } } else { Logger.error( "Unknown event type:", appData.type, " with request ", appData.request ); } }; var handleRequestMissing = (app, appData) => { Logger.warn( `[handleComs]: App ${app} sent unknown data type: ${appData.type} and request: ${appData.request}, with payload ${appData.payload ? JSON.stringify(appData.payload).length > 1e3 ? "[Large Payload]" : JSON.stringify(appData.payload) : "undefined"}`, app ); }; var handleRequestSetSettings = async (app, appData) => { Logger.info("Simulating adding settings"); Logger.debug("Settings being added: ", appData.payload); const appSettings = appData.payload; const rebuiltSettings = Object.fromEntries( Object.entries(appSettings).map(([key, setting]) => { return [ key, { ...setting, value: DeskThingConfig.development?.server?.mockData?.settings[key] ?? setting.value } ]; }) ); Logger.debug( "Rebuilt Settings with mocked data. Setting to: ", rebuiltSettings ); Data.settings = { ...Data.settings, ...rebuiltSettings }; await new Promise((resolve3) => setTimeout(resolve3, 5e3)); Logger.debug("Sending settings back to the server"); ServerMessageBus.notify("app:data", { type: "settings", payload: rebuiltSettings }); }; var handleRequestSetData = async (app, appData) => { Logger.info("Simulating adding data"); Logger.debug("Data being added: ", Data.data); Data.data = { ...Data.data, ...appData.payload }; }; var handleRequestSetAppData = async (app, appData) => { if (!appData.payload) return; const { settings, ...data } = appData.payload; Data = { data: { ...Data.data, ...data }, settings: { ...Data.settings, ...settings } }; }; var handleRequestOpen = async (_app, appData) => { Logger.debug(`[handleOpen]: Opening ${appData.payload}`); const encodedUrl = encodeURI(appData.payload); Logger.info( `[openUrl]: If your browser doesn't automatically open, try manually clicking the url: ${encodedUrl} ` ); try { if (process.platform === "win32") { exec(`start "" "${encodedUrl}"`); } else if (process.platform === "darwin") { exec(`open '${encodedUrl}'`); } else { exec(`xdg-open '${encodedUrl}'`); } Logger.debug(`URL opening command executed successfully`); } catch (error) { Logger.error(`Error opening URL: ${error.message}`); } }; var handleRequestLog = (app, appData) => { Logger.clientLog(appData.request, typeof appData.payload === "object" ? JSON.stringify(appData.payload) : appData.payload); }; var handleRequestKeyAdd = async (app, appData) => { Logger.warn("Key data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestKeyRemove = async (app, appData) => { Logger.warn("Key data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestKeyTrigger = async (app, appData) => { Logger.warn("Key data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestActionRun = async (app, appData) => { Logger.warn("Action data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestActionUpdate = async (app, appData) => { Logger.warn("Action data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestActionRemove = async (app, appData) => { Logger.warn("Action data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestActionAdd = async (app, appData) => { Logger.warn("Action data isn't supported"); Logger.debug("Received", appData.payload); }; var handleRequestGetData = async (app) => { Logger.info(`[handleAppData]: App is requesting data`); Logger.debug(`[handleAppData]: Returning Data:`, Data.data); ServerMessageBus.notify("app:data", { type: "data", payload: Data.data }); }; var handleRequestDelData = async (app, appData) => { Logger.info( `[handleAppData]: ${app} is deleting data: ${appData.payload.toString()}` ); if (!appData.payload || typeof appData.payload !== "string" && !Array.isArray(appData.payload)) { Logger.info( `[handleAppData]: Cannot delete data because ${appData.payload.toString()} is not a string or string[]` ); return; } Data.data = Object.fromEntries( Object.entries(Data.data).filter(([key]) => !appData.payload.includes(key)) ); }; var handleRequestGetConfig = async (app) => { ServerMessageBus.notify("app:data", { type: "config", payload: {} }); Logger.warn( `[handleAppData]: ${app} tried accessing "Config" data type which is depreciated and no longer in use!` ); }; var handleRequestGetSettings = async (app) => { Logger.info(`[handleAppData]: App is requesting settings`); Logger.debug(`[handleAppData]: Returning Settings:`, Data.settings); ServerMessageBus.notify("app:data", { type: "settings", payload: Data.settings }); }; var handleRequestDelSettings = async (app, appData) => { Logger.info( `[handleAppData]: ${app} is deleting settings: ${appData.payload.toString()}` ); if (!appData.payload || typeof appData.payload !== "string" && !Array.isArray(appData.payload)) { Logger.warn( `[handleAppData]: Cannot delete settings because ${appData.payload.toString()} is not a string or string[]` ); return; } Data.data = Object.fromEntries( Object.entries(Data.settings).filter( ([key]) => !appData.payload.includes(key) ) ); }; var handleRequestGetInput = async (app, appData) => { const templateData = Object.keys(appData.payload).reduce((acc, key) => { acc[key] = "arbData"; return acc; }, {}); Logger.info(`[handleAppData]: App is requesting input`); Logger.debug(`[handleAppData]: Returning Input:`, templateData); ServerMessageBus.notify("app:data", { type: "input", payload: templateData }); }; var handleConnectionsRequest = async (app) => { Logger.info(`[handleAppData]: App is requesting connections`); const sampleClient = { clientId: "sample-id", connected: false, meta: {}, identifiers: { adb: { id: "sample-provider", capabilities: [], method: ClientConnectionMethod.Unknown, providerId: "sample-provider", active: false } }, connectionState: ConnectionState.Disconnected, timestamp: Date.now(), currentApp: app }; Logger.debug(`[handleAppData]: Returning Connections:`, sampleClient); ServerMessageBus.notify("app:data", { type: DESKTHING_EVENTS.CLIENT_STATUS, request: "connections", payload: [sampleClient] }); }; var handleGet = { data: handleRequestGetData, config: handleRequestGetConfig, settings: handleRequestGetSettings, input: handleRequestGetInput, connections: handleConnectionsRequest }; var handleSet = { settings: handleRequestSetSettings, data: handleRequestSetData, appData: handleRequestSetAppData, default: handleRequestMissing }; var handleDelete = { settings: handleRequestDelSettings, data: handleRequestDelData }; var handleOpen = { default: handleRequestOpen }; var handleSendToClient = { default: async (app, appData) => { serverService.sendToClient({ app: appData.payload.app || app, type: appData.payload.type || "", payload: appData.payload.payload ?? "", request: appData.payload.request || "", clientId: appData.payload.clientId || "" }); } }; var handleSendToApp = { default: async (app, appData) => { Logger.info("Sent data ", appData.payload, " to other app"); } }; var handleRequestSongSet = { [AUDIO_REQUESTS.FAST_FORWARD]: async (app, appData) => { Logger.info("Unable to fast forward (no audio app in emulator)"); }, [AUDIO_REQUESTS.REWIND]: async (app, appData) => { Logger.info("Unable to rewind (no audio app in emulator)"); }, [AUDIO_REQUESTS.PLAY]: async (app, appData) => { Logger.info("Unable to play (no audio app in emulator)"); }, [AUDIO_REQUESTS.PAUSE]: async (app, appData) => { Logger.info("Unable to pause (no audio app in emulator)"); }, [AUDIO_REQUESTS.STOP]: async (app, appData) => { Logger.info("Unable to stop (no audio app in emulator)"); }, [AUDIO_REQUESTS.NEXT]: async (app, appData) => { Logger.info("Unable to next (no audio app in emulator)"); }, [AUDIO_REQUESTS.PREVIOUS]: async (app, appData) => { Logger.info("Unable to previous (no audio app in emulator)"); }, [AUDIO_REQUESTS.SEEK]: async (app, appData) => { Logger.info("Unable to seek (no audio app in emulator)"); }, [AUDIO_REQUESTS.LIKE]: async (app, appData) => { Logger.info("Unable to like (no audio app in emulator)"); }, [AUDIO_REQUESTS.VOLUME]: async (app, appData) => { Logger.info("Unable to change volume (no audio app in emulator)"); }, [AUDIO_REQUESTS.REPEAT]: async (app, appData) => { Logger.info("Unable to change repeat (no audio app in emulator)"); }, [AUDIO_REQUESTS.SHUFFLE]: async (app, appData) => { Logger.info("Unable to change shuffle (no audio app in emulator)"); } }; var handleLog = { [LOGGING_LEVELS.LOG]: handleRequestLog, [LOGGING_LEVELS.DEBUG]: handleRequestLog, [LOGGING_LEVELS.ERROR]: handleRequestLog, [LOGGING_LEVELS.FATAL]: handleRequestLog, [LOGGING_LEVELS.WARN]: handleRequestLog, [LOGGING_LEVELS.MESSAGE]: handleRequestLog, default: handleRequestMissing }; var handleKey = { add: handleRequestKeyAdd, remove: handleRequestKeyRemove, trigger: handleRequestKeyTrigger, default: handleRequestMissing }; var handleAction = { add: handleRequestActionAdd, remove: handleRequestActionRemove, update: handleRequestActionUpdate, run: handleRequestActionRun, default: handleRequestMissing }; var handleDefault = { default: handleRequestMissing }; var handleSong = { "default": (data, payload) => new MusicService().setSong(payload.payload), "data": (data, payload) => new MusicService().setSong(payload.payload) }; var handleData = { [APP_REQUESTS.GET]: handleGet, [APP_REQUESTS.SET]: handleSet, [APP_REQUESTS.DELETE]: handleDelete, [APP_REQUESTS.OPEN]: handleOpen, [APP_REQUESTS.SEND]: handleSendToClient, [APP_REQUESTS.TOAPP]: handleSendToApp, [APP_REQUESTS.LOG]: handleLog, [APP_REQUESTS.KEY]: handleKey, [APP_REQUESTS.ACTION]: handleAction, default: handleDefault, step: { default: () => { } }, task: { default: () => { } }, [APP_REQUESTS.SONG]: handleSong }; // src/emulator/server/server.ts import { Worker } from "node:worker_threads"; import { dirname, resolve as resolve2 } from "path"; import { fileURLToPath as fileURLToPath2 } from "url"; var ServerRunner = class { serverWorker = null; watcher = null; manifest = null; restartTimeout = null; musicService = new MusicService(); async start() { Logger.debug("Starting server wrapper..."); this.startServerProcess(); this.watchWithFsAPI(); this.manifest = getManifestDetails(); this.startServerMessageBus(); } startServerMessageBus() { ServerMessageBus.initialize(DeskThingConfig.development.client.linkPort); ServerMessageBus.subscribe("app:data", (payload) => { if (this.serverWorker) { Logger.debug(`[ServerMessageBus]: Received data from app: ${payload.type}`); this.serverWorker.postMessage({ type: "data", payload }); } }); ServerMessageBus.subscribe("auth:callback", (payload) => { if (this.serverWorker) { this.serverWorker.postMessage({ type: "data", payload: { type: "callback-data", payload: payload.code } }); } }); } async startServerProcess() { Logger.debug("Starting server process..."); try { const projectRoot = process.cwd(); const __dirname = dirname(fileURLToPath2(import.meta.url)); const workerPath = resolve2(__dirname, "serverProcess.js"); const serverPath = resolve2(projectRoot, "server", "index.ts"); const rootPath = resolve2(projectRoot, "server"); if (this.serverWorker) { this.serverWorker.terminate(); this.serverWorker = null; Logger.info("Waiting for server to exit..."); await new Promise((resolve3) => setTimeout(resolve3, 1e3)); } const tsConfigPath = resolve2(process.cwd(), "tsconfig.node.json"); this.serverWorker = new Worker(workerPath, { workerData: { SERVER_INDEX_PATH: serverPath, DESKTHING_ROOT_PATH: rootPath, NODE_ENV: "development", TSM_NODE_PROJECT: tsConfigPath }, execArgv: ["--loader", "tsm"], env: { TSM_NODE_PROJECT: tsConfigPath } }); this.serverWorker.stdout?.on("data", (data) => { Logger.clientLog(LOGGING_LEVELS.LOG, data.toString()); }); this.serverWorker.stderr?.on("data", (data) => { Logger.clientLog(LOGGING_LEVELS.ERROR, data.toString()); }); this.musicService.start(); setTimeout(() => { if (this.serverWorker?.postMessage) { this.serverWorker.postMessage({ type: "start" }); } }, 500); process.env.SERVER_INDEX_PATH = serverPath; Logger.debug("Resolved processPath:", workerPath); this.serverWorker.on( "message", (message) => { switch (message.type) { case "server:log": Logger.debug("[worker]", message.payload); return; case "server:data": case "data": handleDataFromApp( this.manifest?.id || "testapp", message.payload ); return; case "server:error": return; case "started": Logger.debug("[worker]", "started"); return; case "stopped": Logger.debug("[worker]", "stopped"); return; } if (message.log) { Logger.clientLog(LOGGING_LEVELS.LOG, message.log); } else if (message.error) { Logger.clientLog(LOGGING_LEVELS.ERROR, message.error); } else { Logger.error("Unknown message type:", message.type); } } ); this.serverWorker.on("error", (error) => { Logger.error("Experienced an error in the server wrapper:", error); }); this.serverWorker.on("exit", (code) => { if (code !== 0) { Logger.warn(`Server worker exited with code ${code}`); } Logger.debug(`Server worker exited with code ${code}`); this.serverWorker = null; }); Logger.debug("Server worker started"); } catch (error) { Logger.error("Server worker failed to start: ", error); } } watchWithFsAPI() { let isInitialScan = true; const projectRoot = process.cwd(); const serverPath = resolve2(projectRoot, "server"); this.watcher = fsWatch( serverPath, { recursive: true }, (eventType, filename) => { if (filename?.endsWith(".ts")) { if (isInitialScan) return; Logger.info(`\u{1F4DD} File ${filename} changed, queuing server restart...`); this.queueRestart(); } } ); setTimeout(() => { isInitialScan = false; }, 1e3); } async stop() { this.watcher?.close(); if (this.restartTimeout) { clearTimeout(this.restartTimeout); this.restartTimeout = null; } if (this.serverWorker) { this.serverWorker.terminate(); this.serverWorker = null; } } async queueRestart() { if (this.restartTimeout) { clearTimeout(this.restartTimeout); } Logger.info(`\u{1F55B} Queued restart in ${DeskThingConfig.development.server.editCooldownMs || 1e3}ms`); this.restartTimeout = setTimeout(() => { this.restartServer(); this.restartTimeout = null; Logger.info( `\u{1F55B} Waited ${DeskThingConfig.development.server.editCooldownMs || 1e3}ms. Restarting...` ); }, DeskThingConfig.development.server.editCooldownMs || 1e3); } async restartServer() { this.musicService.stop(); if (this.restartTimeout) { clearTimeout(this.restartTimeout); } Logger.info("\u{1F504} Restarting server..."); if (this.serverWorker) { this.serverWorker.terminate(); this.serverWorker = null; } this.startServerProcess(); } }; // src/emulator/services/timeService.ts var TimeService = class { intervalId = null; start() { Logger.debug("Starting time service..."); this.intervalId = setInterval(() => { const now = /* @__PURE__ */ new Date(); const payload = { utcTime: now.getTime(), timezoneOffset: now.getTimezoneOffset() }; const TimeDataPayload = { type: DEVICE_CLIENT.TIME, app: "client", request: "set", payload }; Logger.debug("Sending Time Update"); ServerMessageBus.publish("client:request", TimeDataPayload); }, 3e4); } stop() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } } }; // src/emulator/index.ts async function startDevelopment({ debug } = { debug: false }) { if (debug) console.log("Debug mode enabled"); await initConfig({ debug }); const devServer = new DevClient(); const serverRunner = new ServerRunner(); const timeService = new TimeService(); await Promise.all([ devServer.start(), serverRunner.start(), timeService.start() ]); } if (import.meta.url === `file://${process.argv[1]}`) { startDevelopment(); } export { startDevelopment };