UNPKG

@deskthing/cli

Version:
1,511 lines (1,486 loc) 60.1 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // 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"; import { URL as URL2 } 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 { deskthingPath; constructor() { this.deskthingPath = join2(process.cwd(), "deskthing"); if (!existsSync2(this.deskthingPath)) { Logger.warn(`DeskThing path does not exist: ${this.deskthingPath}`); Logger.warn("Please run `npx @deskthing/cli update --noOverwrite` to set up the environment."); } } async start() { const __dirname = fileURLToPath(new URL2(".", 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 (!["GET", "POST"].includes(req.method || "")) { res.statusCode = 405; res.end("Method Not Allowed"); return; } const url = new URL2(req.url || "/", `http://${req.headers.host}`); let urlPath = url.pathname; const queryParams = url.searchParams; Logger.debug(`${req.method} ${urlPath}`); if (this.handleSpecialRoutes(req, res, urlPath, queryParams)) { return; } if (this.handleClientRoutes(req, res, urlPath, staticPath, mimeTypes)) { return; } if (this.handleResourceRoutes(req, res, urlPath)) { return; } if (this.handleProxyRoutes(req, res, urlPath, queryParams)) { return; } if (urlPath === "/") { urlPath = "/index.html"; } const safePath = normalize(urlPath).replace(/^(\.\.(\/|\\|$))+/, ""); const filePath = join2(staticPath, safePath); Logger.debug(`Serving ${urlPath} from template`); this.serveStaticFile(res, filePath, mimeTypes, () => { const indexPath = join2(staticPath, "index.html"); this.serveStaticFile(res, indexPath, mimeTypes, () => { 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` ); }); } handleSpecialRoutes(req, res, urlPath, queryParams) { if (urlPath.startsWith("/callback")) { CallbackService.handleCallback(req, res); return true; } if (urlPath === "/config") { try { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(DeskThingConfig.development.client)); return true; } catch (err) { Logger.error("Error serving config:", err); res.statusCode = 500; res.end("Internal Server Error"); return true; } } return false; } handleClientRoutes(req, res, urlPath, staticPath, mimeTypes) { if (urlPath === "/manifest.json") { res.writeHead(302, { "Location": "/client/manifest.json" }); res.end(); return true; } if (urlPath === "/client/manifest.json") { const clientIp = req.headers.host?.split(":")[0] || "localhost"; const clientPort = DeskThingConfig.development.client.clientPort; const mockManifest = { id: "mock-client", name: "Mock DeskThing Client", short_name: "MockClient", description: "Development mock client for DeskThing", version: "1.0.0", author: "DeskThing Dev", repository: "https://github.com/deskthing/deskthing", context: { ip: clientIp, port: clientPort, method: "LAN", id: "Desktop", name: "Desktop" }, connectionId: this.generateConnectionId(), reactive: true, compatibility: { server: "1.0.0", app: "1.0.0" } }; Logger.info("Client connected via manifest request"); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(mockManifest)); return true; } if (urlPath.startsWith("/client")) { const subPath = urlPath.substring(7) || "/index.html"; const templateFilePath = join2(staticPath, subPath); this.serveStaticFile(res, templateFilePath, mimeTypes, () => { const templateIndexPath = join2(staticPath, "index.html"); this.serveStaticFile(res, templateIndexPath, mimeTypes, () => { res.statusCode = 404; res.end("App not found"); }); }); return true; } return false; } handleResourceRoutes(req, res, urlPath) { if (urlPath.startsWith("/icons")) { const iconPath = urlPath.substring(6); const fullIconPath = join2(this.deskthingPath, "icons", iconPath); this.serveStaticFile(res, fullIconPath, {}, () => { res.statusCode = 404; res.end("Icon not found"); }, { maxAge: "1d", immutable: true, etag: true, lastModified: true }); return true; } if (urlPath.startsWith("/resource/icons")) { const iconPath = urlPath.substring(15); const fullIconPath = join2(this.deskthingPath, "icons", iconPath); this.serveStaticFile(res, fullIconPath, {}, () => { res.statusCode = 404; res.end("Icon not found"); }, { maxAge: "1d", immutable: true, etag: true, lastModified: true }); return true; } const imageMatch = urlPath.match(/^\/resource\/image\/([^\/]+)\/([^\/]+)$/); if (imageMatch) { const [, , imageName] = imageMatch; if (!imageName) { res.statusCode = 400; res.end("Image name is required"); return true; } const imagePath = join2(this.deskthingPath, "images", imageName); this.serveStaticFile(res, imagePath, {}, () => { res.statusCode = 404; res.end("Image not found"); }); return true; } return false; } handleProxyRoutes(req, res, urlPath, queryParams) { const proxyFetchMatch = urlPath.match(/^\/proxy\/fetch\/(.+)$/); if (proxyFetchMatch) { const targetUrl = decodeURIComponent(proxyFetchMatch[1]); this.proxyRequest(targetUrl, res); return true; } if (urlPath === "/proxy/v1") { const url = queryParams.get("url"); if (!url) { res.statusCode = 400; res.end("Missing url query parameter"); return true; } Logger.debug(`Proxying resource from: ${url}`); this.proxyRequest(url, res); return true; } return false; } serveStaticFile(res, filePath, mimeTypes, onNotFound, cacheOptions) { if (existsSync2(filePath) && statSync(filePath).isFile()) { try { const ext = extname(filePath).toLowerCase(); const contentType = mimeTypes[ext] || this.getDefaultMimeType(ext); const fileContent = readFileSync(filePath); const headers = { "Content-Type": contentType }; if (cacheOptions) { if (cacheOptions.maxAge) { headers["Cache-Control"] = `max-age=${this.parseCacheMaxAge(cacheOptions.maxAge)}${cacheOptions.immutable ? ", immutable" : ""}`; } if (cacheOptions.etag) { headers["ETag"] = `"${this.generateETag(fileContent)}"`; } if (cacheOptions.lastModified) { const stats = statSync(filePath); headers["Last-Modified"] = stats.mtime.toUTCString(); } } res.writeHead(200, headers); res.end(fileContent); } catch (err) { Logger.error(`Error serving ${filePath}:`, err); res.statusCode = 500; res.end("Internal Server Error"); } } else { onNotFound(); } } async proxyRequest(targetUrl, res) { try { Logger.debug(`Proxying request to: ${targetUrl}`); const response = await fetch(targetUrl); if (!response.ok) { res.statusCode = response.status; res.end(`Upstream resource responded with ${response.status}`); return; } const contentType = response.headers.get("content-type"); if (contentType) { res.setHeader("Content-Type", contentType); } ["content-length", "content-encoding", "cache-control"].forEach((header) => { const value = response.headers.get(header); if (value) { res.setHeader(header, value); } }); if (!response.body) { res.statusCode = 204; res.end(); return; } const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; res.write(value); } res.end(); } catch (error) { Logger.error("Error proxying resource:", error); res.statusCode = 500; res.end(`Error fetching resource: ${error instanceof Error ? error.message : String(error)}`); } } getDefaultMimeType(ext) { const defaultMimeTypes = { ".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" }; return defaultMimeTypes[ext] || "application/octet-stream"; } parseCacheMaxAge(maxAge) { if (maxAge.endsWith("d")) { return parseInt(maxAge.slice(0, -1)) * 24 * 60 * 60; } if (maxAge.endsWith("h")) { return parseInt(maxAge.slice(0, -1)) * 60 * 60; } if (maxAge.endsWith("m")) { return parseInt(maxAge.slice(0, -1)) * 60; } return parseInt(maxAge) || 0; } generateETag(content) { const crypto = __require("crypto"); return crypto.createHash("md5").update(content).digest("hex"); } generateConnectionId() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == "x" ? r : r & 3 | 8; return v.toString(16); }); } getDeviceType(userAgent, ip, port) { if (!userAgent) { return { method: "LAN" /* LAN */, ip, port, id: "Unknown" /* Unknown */, name: "unknown" }; } userAgent = userAgent.toLowerCase(); const deviceMap = { // Desktops linux: { id: "Desktop" /* Desktop */, name: "linux" }, win: { id: "Desktop" /* Desktop */, name: "windows" }, mac: { id: "Desktop" /* Desktop */, name: "mac" }, chromebook: { id: "Desktop" /* Desktop */, name: "chromebook" }, // Tablets ipad: { id: "Tablet" /* Tablet */, name: "tablet" }, webos: { id: "Tablet" /* Tablet */, name: "webos" }, kindle: { id: "Tablet" /* Tablet */, name: "kindle" }, // Mobile iphone: { id: "Iphone" /* Iphone */, name: "iphone" }, "firefox os": { id: "Iphone" /* Iphone */, name: "firefox-os" }, blackberry: { id: "Iphone" /* Iphone */, name: "blackberry" }, "windows phone": { id: "Iphone" /* Iphone */, name: "windows-phone" } }; if (userAgent.includes("android")) { return { method: "LAN" /* LAN */, ip, port, id: userAgent.includes("mobile") ? "Iphone" /* Iphone */ : "Tablet" /* Tablet */, name: userAgent.includes("mobile") ? "android" : "tablet" }; } const matchedDevice = Object.entries(deviceMap).find(([key]) => userAgent.includes(key)); if (matchedDevice) { return { method: "LAN" /* LAN */, ip, port, ...matchedDevice[1] }; } return { method: "LAN" /* LAN */, ip, port, id: "Unknown" /* Unknown */, name: "unknown" }; } }; // 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 { ...manifest, // catch all for any new fields added in the future 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/settingService.ts var SettingService = class { static currentSettings = {}; static sendSettings() { ServerMessageBus.publish("client:request", { type: DESKTHING_DEVICE.SETTINGS, payload: this.currentSettings, app: "client" }); ServerMessageBus.notify("app:data", { type: "settings", payload: this.currentSettings }); } static getSettings() { return this.currentSettings || {}; } static async setSettings(appSettings) { if (!this.currentSettings) { this.currentSettings = {}; } this.currentSettings = { ...this.currentSettings, ...appSettings }; const rebuiltSettings = Object.fromEntries( Object.entries(appSettings).map(([key, setting]) => { return [ key, { ...setting, value: DeskThingConfig.development?.server?.mockData?.settings[key] ?? setting.value } ]; }) ); this.currentSettings = { ...this.currentSettings, ...rebuiltSettings }; await new Promise((resolve3) => setTimeout(resolve3, 1e3)); Logger.debug( "Rebuilt Settings with mocked data. Setting to: ", rebuiltSettings ); this.sendSettings(); } static async initSettings(settings) { if (!this.currentSettings) { this.currentSettings = {}; } for (const key in settings) { const newSetting = settings[key]; const existingSetting = this.currentSettings[key]; if (!existingSetting) { this.currentSettings[key] = newSetting; continue; } if (newSetting && typeof newSetting.version !== "undefined") { if (!existingSetting.version || existingSetting.version !== newSetting.version) { this.currentSettings[key] = newSetting; } } else { } } this.setSettings(this.currentSettings); } static delSettings(settingIds) { if (!this.currentSettings) { Logger.warn("No settings to delete from"); return; } settingIds.forEach((id) => { delete this.currentSettings[id]; }); this.sendSettings(); } static updateSettings(settings) { this.currentSettings = { ...this.currentSettings, ...settings }; this.sendSettings(); } }; // 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 "setSettings": SettingService.updateSettings(data.payload); 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 settings = SettingService.getSettings(); ServerMessageBus.publish("client:response", { type: "settings", payload: 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: {} }; 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; await SettingService.setSettings(appSettings); }; var handleRequestInitSettings = async (app, appData) => { Logger.info("Simulating initializing settings"); Logger.debug("Settings being initialized: ", appData.payload); const appSettings = appData.payload; await SettingService.initSettings(appSettings); }; 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; SettingService.updateSettings(settings); Data = { data: { ...Data.data, ...data } }; }; 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`); const settings = SettingService.getSettings(); Logger.debug(`[handleAppData]: Returning Settings:`, settings); ServerMessageBus.notify("app:data", { type: "settings", payload: 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; } SettingService.delSettings(Array.isArray(appData.payload) ? appData.payload : [appData.payload]); }; 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", connectionState: ConnectionState.Established, 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, "settings-init": handleRequestInitSettings }; 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 a