chwellofficial/nt360Slides
0
1/* This script starts the FastAPI and Next.js servers, setting up user configuration if necessary. It reads environment variables to configure API keys and other settings, ensuring that the user configuration file is created if it doesn't exist. The script also handles the starting of both servers and keeps the Node.js process alive until one of the servers exits. */2 3import { join, dirname } from "path";4import { fileURLToPath } from "url";5import { spawn } from "child_process";6import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";7 8const __filename = fileURLToPath(import.meta.url);9const __dirname = dirname(__filename);10 11const fastapiDir = join(__dirname, "servers/fastapi");12const nextjsDir = join(__dirname, "servers/nextjs");13const nextjsStandaloneServer = join(nextjsDir, "server.js");14const exportSyncScript = join(__dirname, "scripts/sync-presentation-export.cjs");15 16const args = process.argv.slice(2);17const hasDevArg = args.includes("--dev") || args.includes("-d");18const isDev = hasDevArg;19const canChangeKeys = process.env.CAN_CHANGE_KEYS !== "false";20 21const fastapiPort = 8000;22const nextjsPort = 3000;23const appmcpPort = 8001;24 25const userConfigPath = join(process.env.APP_DATA_DIRECTORY, "userConfig.json");26const userDataDir = dirname(userConfigPath);27 28// Create user_data directory if it doesn't exist29if (!existsSync(userDataDir)) {30 mkdirSync(userDataDir, { recursive: true });31}32 33// Setup node_modules for development34const setupNodeModules = () => {35 return new Promise((resolve, reject) => {36 console.log("Setting up node_modules for Next.js...");37 const npmProcess = spawn("npm", ["install"], {38 cwd: nextjsDir,39 stdio: "inherit",40 env: process.env,41 });42 43 npmProcess.on("error", (err) => {44 console.error("npm install failed:", err);45 reject(err);46 });47 48 npmProcess.on("exit", (code) => {49 if (code === 0) {50 console.log("npm install completed successfully");51 resolve();52 } else {53 console.error(`npm install failed with exit code: ${code}`);54 reject(new Error(`npm install failed with exit code: ${code}`));55 }56 });57 });58};59 60const runCommand = (command, commandArgs, options = {}) => {61 return new Promise((resolve, reject) => {62 const child = spawn(command, commandArgs, {63 cwd: options.cwd || __dirname,64 stdio: options.stdio || "inherit",65 env: options.env || process.env,66 });67 68 child.on("error", (err) => {69 reject(err);70 });71 72 child.on("exit", (code) => {73 if (code === 0) {74 resolve();75 } else {76 reject(new Error(`${command} exited with code: ${code}`));77 }78 });79 });80};81 82const runNodeScript = (scriptPath, scriptArgs) => {83 return runCommand(process.execPath, [scriptPath, ...scriptArgs], {84 cwd: __dirname,85 });86};87 88const isTruthyEnv = (value) => {89 if (value == null) {90 return false;91 }92 93 return !["", "0", "false", "no", "off"].includes(94 String(value).trim().toLowerCase()95 );96};97 98const isOllamaInstalled = () =>99 existsSync("/usr/bin/ollama") || existsSync("/usr/local/bin/ollama");100 101const shouldStartOllama = () => isTruthyEnv(process.env.START_OLLAMA);102 103const ensureOllamaRuntime = async () => {104 if (!shouldStartOllama() || isOllamaInstalled()) {105 return;106 }107 108 console.log("START_OLLAMA=true; installing Ollama runtime...");109 await runCommand("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], {110 cwd: "/",111 });112};113 114const ensurePresentationExportRuntime = async () => {115 if (process.env.ENSURE_PRESENTATION_EXPORT_RUNTIME === "false") {116 return;117 }118 119 if (!existsSync(exportSyncScript)) {120 console.warn("presentation-export sync script not found; skipping runtime check");121 return;122 }123 124 try {125 await runNodeScript(exportSyncScript, ["--check-only"]);126 } catch (err) {127 if (!isDev) {128 throw new Error(129 "presentation-export runtime is missing in this container image. Rebuild the image so the runtime package is installed."130 );131 }132 133 console.warn("presentation-export runtime missing in dev mount. Syncing runtime package...");134 await runNodeScript(exportSyncScript, ["--force"]);135 }136};137 138process.env.USER_CONFIG_PATH = userConfigPath;139// Let Next.js middleware reach FastAPI over the loopback interface inside the140// container without having to bounce through nginx (the host-facing port is141// not reachable from inside the Next.js process).142if (!process.env.FAST_API_INTERNAL_URL) {143 process.env.FAST_API_INTERNAL_URL = `http://127.0.0.1:${fastapiPort}`;144}145 146//? UserConfig is only setup if API Keys can be changed147const setupUserConfigFromEnv = () => {148 let existingConfig = {};149 150 if (existsSync(userConfigPath)) {151 existingConfig = JSON.parse(readFileSync(userConfigPath, "utf8"));152 }153 154 if (!["ollama", "openai", "google", "anthropic", "custom", "codex"].includes(existingConfig.LLM)) {155 existingConfig.LLM = undefined;156 }157 158 const userConfig = {159 LLM: process.env.LLM || existingConfig.LLM,160 OPENAI_API_KEY: process.env.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,161 OPENAI_MODEL: process.env.OPENAI_MODEL || existingConfig.OPENAI_MODEL,162 GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,163 GOOGLE_MODEL: process.env.GOOGLE_MODEL || existingConfig.GOOGLE_MODEL,164 OLLAMA_URL: process.env.OLLAMA_URL || existingConfig.OLLAMA_URL,165 OLLAMA_MODEL: process.env.OLLAMA_MODEL || existingConfig.OLLAMA_MODEL,166 ANTHROPIC_API_KEY:167 process.env.ANTHROPIC_API_KEY || existingConfig.ANTHROPIC_API_KEY,168 ANTHROPIC_MODEL:169 process.env.ANTHROPIC_MODEL || existingConfig.ANTHROPIC_MODEL,170 CUSTOM_LLM_URL: process.env.CUSTOM_LLM_URL || existingConfig.CUSTOM_LLM_URL,171 CUSTOM_LLM_API_KEY:172 process.env.CUSTOM_LLM_API_KEY || existingConfig.CUSTOM_LLM_API_KEY,173 CUSTOM_MODEL: process.env.CUSTOM_MODEL || existingConfig.CUSTOM_MODEL,174 PEXELS_API_KEY: process.env.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,175 PIXABAY_API_KEY:176 process.env.PIXABAY_API_KEY || existingConfig.PIXABAY_API_KEY,177 IMAGE_PROVIDER: process.env.IMAGE_PROVIDER || existingConfig.IMAGE_PROVIDER,178 DISABLE_THINKING:179 process.env.DISABLE_THINKING || existingConfig.DISABLE_THINKING,180 EXTENDED_REASONING:181 process.env.EXTENDED_REASONING || existingConfig.EXTENDED_REASONING,182 WEB_GROUNDING: process.env.WEB_GROUNDING || existingConfig.WEB_GROUNDING,183 USE_CUSTOM_URL: process.env.USE_CUSTOM_URL || existingConfig.USE_CUSTOM_URL,184 COMFYUI_URL: process.env.COMFYUI_URL || existingConfig.COMFYUI_URL,185 COMFYUI_WORKFLOW:186 process.env.COMFYUI_WORKFLOW || existingConfig.COMFYUI_WORKFLOW,187 DALL_E_3_QUALITY:188 process.env.DALL_E_3_QUALITY || existingConfig.DALL_E_3_QUALITY,189 GPT_IMAGE_1_5_QUALITY:190 process.env.GPT_IMAGE_1_5_QUALITY || existingConfig.GPT_IMAGE_1_5_QUALITY,191 CODEX_MODEL: process.env.CODEX_MODEL || existingConfig.CODEX_MODEL,192 CODEX_ACCESS_TOKEN: existingConfig.CODEX_ACCESS_TOKEN,193 CODEX_REFRESH_TOKEN: existingConfig.CODEX_REFRESH_TOKEN,194 CODEX_TOKEN_EXPIRES: existingConfig.CODEX_TOKEN_EXPIRES,195 CODEX_ACCOUNT_ID: existingConfig.CODEX_ACCOUNT_ID,196 AUTH_USERNAME: existingConfig.AUTH_USERNAME,197 AUTH_PASSWORD_HASH: existingConfig.AUTH_PASSWORD_HASH,198 AUTH_SECRET_KEY: existingConfig.AUTH_SECRET_KEY,199 };200 201 writeFileSync(userConfigPath, JSON.stringify(userConfig));202};203 204const startServers = async () => {205 const fastApiProcess = spawn(206 "python",207 [208 "server.py",209 "--port",210 fastapiPort.toString(),211 "--reload",212 isDev ? "true" : "false",213 ],214 {215 cwd: fastapiDir,216 stdio: "inherit",217 env: { ...process.env, PYTHONPATH: fastapiDir },218 }219 );220 221 fastApiProcess.on("error", (err) => {222 console.error("FastAPI process failed to start:", err);223 });224 225 const appmcpProcess = spawn(226 "python",227 ["mcp_server.py", "--port", appmcpPort.toString()],228 {229 cwd: fastapiDir,230 stdio: "ignore",231 env: process.env,232 }233 );234 235 appmcpProcess.on("error", (err) => {236 console.error("App MCP process failed to start:", err);237 });238 239 const useStandaloneNextjs = !isDev && existsSync(nextjsStandaloneServer);240 241 const nextjsProcess = spawn(242 useStandaloneNextjs ? process.execPath : "npm",243 useStandaloneNextjs244 ? [nextjsStandaloneServer]245 : [246 "run",247 isDev ? "dev" : "start",248 "--",249 "-H",250 "127.0.0.1",251 "-p",252 nextjsPort.toString(),253 ],254 {255 cwd: nextjsDir,256 stdio: "inherit",257 env:258 useStandaloneNextjs259 ? {260 ...process.env,261 HOSTNAME: "127.0.0.1",262 PORT: nextjsPort.toString(),263 }264 : process.env,265 }266 );267 268 nextjsProcess.on("error", (err) => {269 console.error("Next.js process failed to start:", err);270 });271 272 const shouldStartOllamaRuntime = shouldStartOllama();273 const ollamaInstalled = isOllamaInstalled();274 275 const exitPromises = [276 new Promise((resolve) => fastApiProcess.on("exit", resolve)),277 new Promise((resolve) => nextjsProcess.on("exit", resolve)),278 ];279 280 if (shouldStartOllamaRuntime && ollamaInstalled) {281 const ollamaProcess = spawn("ollama", ["serve"], {282 cwd: "/",283 stdio: "inherit",284 env: process.env,285 });286 ollamaProcess.on("error", (err) => {287 console.error("Ollama process failed to start:", err);288 });289 exitPromises.push(new Promise((resolve) => ollamaProcess.on("exit", resolve)));290 } else if (shouldStartOllamaRuntime) {291 console.log(292 "Ollama requested, but the binary is not installed. Set START_OLLAMA=true to install it at startup, or set OLLAMA_URL to a remote daemon."293 );294 } else {295 console.log(296 "Ollama disabled (START_OLLAMA=false); use OLLAMA_URL for a remote daemon if needed."297 );298 }299 300 // Keep the Node process alive until one of the servers exits301 const exitCode = await Promise.race(exitPromises);302 303 console.log(`One of the processes exited. Exit code: ${exitCode}`);304 process.exit(exitCode);305};306 307// Start nginx service308const startNginx = () => {309 const nginxProcess = spawn("service", ["nginx", "start"], {310 stdio: "inherit",311 env: process.env,312 });313 314 nginxProcess.on("error", (err) => {315 console.error("Nginx process failed to start:", err);316 });317 318 nginxProcess.on("exit", (code) => {319 if (code === 0) {320 console.log("Nginx started successfully");321 } else {322 console.error(`Nginx failed to start with exit code: ${code}`);323 }324 });325};326 327const main = async () => {328 await ensurePresentationExportRuntime();329 await ensureOllamaRuntime();330 331 if (isDev) {332 await setupNodeModules();333 }334 335 if (canChangeKeys) {336 setupUserConfigFromEnv();337 }338 339 startServers();340 startNginx();341};342 343main();344 