import * as ChildProcess from "node:child_process";import * as Crypto from "node:crypto";import * as FS from "node:fs";import * as OS from "node:os";import * as Path from "node:path"; import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, shell,} from "electron";import type { MenuItemConstructorOptions } from "electron";import * as Effect from "effect/Effect";import type { DesktopTheme, DesktopUpdateActionResult, DesktopUpdateCheckResult, DesktopUpdateState,} from "@t3tools/contracts";import { autoUpdater } from "electron-updater"; import type { ContextMenuItem } from "@t3tools/contracts";import { NetService } from "@t3tools/shared/Net";import { RotatingFileSink } from "@t3tools/shared/logging";import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";import { showDesktopConfirmDialog } from "./confirmDialog";import { syncShellEnvironment } from "./syncShellEnvironment";import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable,} from "./updateMachine";import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder";const CONFIRM_CHANNEL = "desktop:confirm";const SET_THEME_CHANNEL = "desktop:set-theme";const CONTEXT_MENU_CHANNEL = "desktop:context-menu";const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";const MENU_ACTION_CHANNEL = "desktop:menu-action";const UPDATE_STATE_CHANNEL = "desktop:update-state";const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";const UPDATE_INSTALL_CHANNEL = "desktop:update-install";const UPDATE_CHECK_CHANNEL = "desktop:update-check";const GET_WS_URL_CHANNEL = "desktop:get-ws-url";const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");const STATE_DIR = Path.join(BASE_DIR, "userdata");const DESKTOP_SCHEME = "t3";const ROOT_DIR = Path.resolve(__dirname, "../../..");const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const APP_USER_MODEL_ID = "com.t3tools.t3code";const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop";const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code";const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code";const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;const COMMIT_HASH_DISPLAY_LENGTH = 12;const LOG_DIR = Path.join(STATE_DIR, "logs");const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024;const LOG_FILE_MAX_FILES = 10;const APP_RUN_ID = Crypto.randomBytes(6).toString("hex");const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json");const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000;const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000;const DESKTOP_UPDATE_CHANNEL = "latest";const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"];type LinuxDesktopNamedApp = Electron.App & { setDesktopName?: (desktopName: string) => void;}; let mainWindow: BrowserWindow | null = null;let backendProcess: ChildProcess.ChildProcess | null = null;let backendPort = 0;let backendAuthToken = "";let backendWsUrl = "";let restartAttempt = 0;let restartTimer: ReturnType<typeof setTimeout> | null = null;let isQuitting = false;let desktopProtocolRegistered = false;let aboutCommitHashCache: string | null | undefined;let desktopLogSink: RotatingFileSink | null = null;let backendLogSink: RotatingFileSink | null = null;let restoreStdIoCapture: (() => void) | null = null;let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); let destructiveMenuIconCache: Electron.NativeImage | null | undefined;const expectedBackendExitChildren = new WeakSet<ChildProcess.ChildProcess>();const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ platform: process.platform, processArch: process.arch, runningUnderArm64Translation: app.runningUnderARM64Translation === true,});const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString();} function logScope(scope: string): string { return `${scope} run=${APP_RUN_ID}`;} function sanitizeLogValue(value: string): string { return value.replace(/\s+/g, " ").trim();} function readPersistedBackendObservabilitySettings(): { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined;} { try { if (!FS.existsSync(SERVER_SETTINGS_PATH)) { return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8")); } catch (error) { console.warn("[desktop] failed to read persisted backend observability settings", error); return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; }} function backendChildEnv(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.T3CODE_PORT; delete env.T3CODE_AUTH_TOKEN; delete env.T3CODE_MODE; delete env.T3CODE_NO_BROWSER; delete env.T3CODE_HOST; delete env.T3CODE_DESKTOP_WS_URL; return env;} function writeDesktopLogHeader(message: string): void { if (!desktopLogSink) return; desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`);} function writeBackendSessionBoundary(phase: "START" | "END", details: string): void { if (!backendLogSink) return; const normalizedDetails = sanitizeLogValue(details); backendLogSink.write( `[${logTimestamp()}] ---- APP SESSION ${phase} run=${APP_RUN_ID} ${normalizedDetails} ----\n`, );} function formatErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error);} function getSafeExternalUrl(rawUrl: unknown): string | null { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return null; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return null; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return null; } return parsedUrl.toString();} function getSafeTheme(rawTheme: unknown): DesktopTheme | null { if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { return rawTheme; } return null;} function writeDesktopStreamChunk( streamName: "stdout" | "stderr", chunk: unknown, encoding: BufferEncoding | undefined,): void { if (!desktopLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof chunk === "string" ? encoding : undefined); desktopLogSink.write(`[${logTimestamp()}] [${logScope(streamName)}] `); desktopLogSink.write(buffer); if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) { desktopLogSink.write("\n"); }} function installStdIoCapture(): void { if (!app.isPackaged || desktopLogSink === null || restoreStdIoCapture !== null) { return; } const originalStdoutWrite = process.stdout.write.bind(process.stdout); const originalStderrWrite = process.stderr.write.bind(process.stderr); const patchWrite = (streamName: "stdout" | "stderr", originalWrite: typeof process.stdout.write) => ( chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): boolean => { const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined; writeDesktopStreamChunk(streamName, chunk, encoding); if (typeof encodingOrCallback === "function") { return originalWrite(chunk, encodingOrCallback); } if (callback !== undefined) { return originalWrite(chunk, encoding, callback); } if (encoding !== undefined) { return originalWrite(chunk, encoding); } return originalWrite(chunk); }; process.stdout.write = patchWrite("stdout", originalStdoutWrite); process.stderr.write = patchWrite("stderr", originalStderrWrite); restoreStdIoCapture = () => { process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; restoreStdIoCapture = null; };} function initializePackagedLogging(): void { if (!app.isPackaged) return; try { desktopLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "desktop-main.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); backendLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "server-child.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); installStdIoCapture(); writeDesktopLogHeader(`runtime log capture enabled logDir=${LOG_DIR}`); } catch (error) { // Logging setup should never block app startup. console.error("[desktop] failed to initialize packaged logging", error); }} function captureBackendOutput(child: ChildProcess.ChildProcess): void { if (!app.isPackaged || backendLogSink === null) return; const writeChunk = (chunk: unknown): void => { if (!backendLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink.write(buffer); }; child.stdout?.on("data", writeChunk); child.stderr?.on("data", writeChunk);} initializePackagedLogging(); if (process.platform === "linux") { app.commandLine.appendSwitch("class", LINUX_WM_CLASS);} function getDestructiveMenuIcon(): Electron.NativeImage | undefined { if (process.platform !== "darwin") return undefined; if (destructiveMenuIconCache !== undefined) { return destructiveMenuIconCache ?? undefined; } try { const icon = nativeImage.createFromNamedImage("trash").resize({ width: 14, height: 14, }); if (icon.isEmpty()) { destructiveMenuIconCache = null; return undefined; } icon.setTemplateImage(true); destructiveMenuIconCache = icon; return icon; } catch { destructiveMenuIconCache = null; return undefined; }}let updatePollTimer: ReturnType<typeof setInterval> | null = null;let updateStartupTimer: ReturnType<typeof setTimeout> | null = null;let updateCheckInFlight = false;let updateDownloadInFlight = false;let updateInstallInFlight = false;let updaterConfigured = false;let updateState: DesktopUpdateState = initialUpdateState(); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateInstallInFlight) return "install"; if (updateDownloadInFlight) return "download"; if (updateCheckInFlight) return "check"; return updateState.errorContext;} protocol.registerSchemesAsPrivileged([ { scheme: DESKTOP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, },]); function resolveAppRoot(): string { if (!app.isPackaged) { return ROOT_DIR; } return app.getAppPath();} /** Read the baked-in app-update.yml config (if applicable). */function readAppUpdateYml(): Record<string, string> | null { try { // electron-updater reads from process.resourcesPath in packaged builds, // or dev-app-update.yml via app.getAppPath() in dev. const ymlPath = app.isPackaged ? Path.join(process.resourcesPath, "app-update.yml") : Path.join(app.getAppPath(), "dev-app-update.yml"); const raw = FS.readFileSync(ymlPath, "utf-8"); // The YAML is simple key-value pairs — avoid pulling in a YAML parser by // doing a line-based parse (fields: provider, owner, repo, releaseType, …). const entries: Record<string, string> = {}; for (const line of raw.split("\n")) { const match = line.match(/^(\w+):\s*(.+)$/); if (match?.[1] && match[2]) entries[match[1]] = match[2].trim(); } return entries.provider ? entries : null; } catch { return null; }} function normalizeCommitHash(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); if (!COMMIT_HASH_PATTERN.test(trimmed)) { return null; } return trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase();} function resolveEmbeddedCommitHash(): string | null { const packageJsonPath = Path.join(resolveAppRoot(), "package.json"); if (!FS.existsSync(packageJsonPath)) { return null; } try { const raw = FS.readFileSync(packageJsonPath, "utf8"); const parsed = JSON.parse(raw) as { t3codeCommitHash?: unknown }; return normalizeCommitHash(parsed.t3codeCommitHash); } catch { return null; }} function resolveAboutCommitHash(): string | null { if (aboutCommitHashCache !== undefined) { return aboutCommitHashCache; } const envCommitHash = normalizeCommitHash(process.env.T3CODE_COMMIT_HASH); if (envCommitHash) { aboutCommitHashCache = envCommitHash; return aboutCommitHashCache; } // Only packaged builds are required to expose commit metadata. if (!app.isPackaged) { aboutCommitHashCache = null; return aboutCommitHashCache; } aboutCommitHashCache = resolveEmbeddedCommitHash(); return aboutCommitHashCache;} function resolveBackendEntry(): string { return Path.join(resolveAppRoot(), "apps/server/dist/bin.mjs");} function resolveBackendCwd(): string { if (!app.isPackaged) { return resolveAppRoot(); } return OS.homedir();} function resolveDesktopStaticDir(): string | null { const appRoot = resolveAppRoot(); const candidates = [ Path.join(appRoot, "apps/server/dist/client"), Path.join(appRoot, "apps/web/dist"), ]; for (const candidate of candidates) { if (FS.existsSync(Path.join(candidate, "index.html"))) { return candidate; } } return null;} function resolveDesktopStaticPath(staticRoot: string, requestUrl: string): string { const url = new URL(requestUrl); const rawPath = decodeURIComponent(url.pathname); const normalizedPath = Path.posix.normalize(rawPath).replace(/^\/+/, ""); if (normalizedPath.includes("..")) { return Path.join(staticRoot, "index.html"); } const requestedPath = normalizedPath.length > 0 ? normalizedPath : "index.html"; const resolvedPath = Path.join(staticRoot, requestedPath); if (Path.extname(resolvedPath)) { return resolvedPath; } const nestedIndex = Path.join(resolvedPath, "index.html"); if (FS.existsSync(nestedIndex)) { return nestedIndex; } return Path.join(staticRoot, "index.html");} function isStaticAssetRequest(requestUrl: string): boolean { try { const url = new URL(requestUrl); return Path.extname(url.pathname).length > 0; } catch { return false; }} function handleFatalStartupError(stage: string, error: unknown): void { const message = formatErrorMessage(error); const detail = error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : ""; writeDesktopLogHeader(`fatal startup error stage=${stage} message=${message}`); console.error(`[desktop] fatal startup error (${stage})`, error); if (!isQuitting) { isQuitting = true; dialog.showErrorBox("T3 Code failed to start", `Stage: ${stage}\n${message}${detail}`); } stopBackend(); restoreStdIoCapture?.(); app.quit();} function registerDesktopProtocol(): void { if (isDevelopment || desktopProtocolRegistered) return; const staticRoot = resolveDesktopStaticDir(); if (!staticRoot) { throw new Error( "Desktop static bundle missing. Build apps/server (with bundled client) first.",Lines 1–500 of 1509. Load more
import * as ChildProcess from "node:child_process"; import * as Crypto from "node:crypto"; import * as FS from "node:fs"; import * as OS from "node:os"; import * as Path from "node:path"; import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, nativeTheme, protocol, shell, } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import * as Effect from "effect/Effect"; import type { DesktopTheme, DesktopUpdateActionResult, DesktopUpdateCheckResult, DesktopUpdateState, } from "@t3tools/contracts"; import { autoUpdater } from "electron-updater"; import type { ContextMenuItem } from "@t3tools/contracts"; import { NetService } from "@t3tools/shared/Net"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { syncShellEnvironment } from "./syncShellEnvironment"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, reduceDesktopUpdateStateOnCheckStart, reduceDesktopUpdateStateOnDownloadComplete, reduceDesktopUpdateStateOnDownloadFailure, reduceDesktopUpdateStateOnDownloadProgress, reduceDesktopUpdateStateOnDownloadStart, reduceDesktopUpdateStateOnInstallFailure, reduceDesktopUpdateStateOnNoUpdate, reduceDesktopUpdateStateOnUpdateAvailable, } from "./updateMachine"; import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const SET_THEME_CHANNEL = "desktop:set-theme"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; const MENU_ACTION_CHANNEL = "desktop:menu-action"; const UPDATE_STATE_CHANNEL = "desktop:update-state"; const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download"; const UPDATE_INSTALL_CHANNEL = "desktop:update-install"; const UPDATE_CHECK_CHANNEL = "desktop:update-check"; const GET_WS_URL_CHANNEL = "desktop:get-ws-url"; const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3"); const STATE_DIR = Path.join(BASE_DIR, "userdata"); const DESKTOP_SCHEME = "t3"; const ROOT_DIR = Path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const APP_USER_MODEL_ID = "com.t3tools.t3code"; const LINUX_DESKTOP_ENTRY_NAME = isDevelopment ? "t3code-dev.desktop" : "t3code.desktop"; const LINUX_WM_CLASS = isDevelopment ? "t3code-dev" : "t3code"; const USER_DATA_DIR_NAME = isDevelopment ? "t3code-dev" : "t3code"; const LEGACY_USER_DATA_DIR_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i; const COMMIT_HASH_DISPLAY_LENGTH = 12; const LOG_DIR = Path.join(STATE_DIR, "logs"); const LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; const LOG_FILE_MAX_FILES = 10; const APP_RUN_ID = Crypto.randomBytes(6).toString("hex"); const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json"); const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000; const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000; const DESKTOP_UPDATE_CHANNEL = "latest"; const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; type LinuxDesktopNamedApp = Electron.App & { setDesktopName?: (desktopName: string) => void; }; let mainWindow: BrowserWindow | null = null; let backendProcess: ChildProcess.ChildProcess | null = null; let backendPort = 0; let backendAuthToken = ""; let backendWsUrl = ""; let restartAttempt = 0; let restartTimer: ReturnType<typeof setTimeout> | null = null; let isQuitting = false; let desktopProtocolRegistered = false; let aboutCommitHashCache: string | null | undefined; let desktopLogSink: RotatingFileSink | null = null; let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); let destructiveMenuIconCache: Electron.NativeImage | null | undefined; const expectedBackendExitChildren = new WeakSet<ChildProcess.ChildProcess>(); const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ platform: process.platform, processArch: process.arch, runningUnderArm64Translation: app.runningUnderARM64Translation === true, }); const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); function logTimestamp(): string { return new Date().toISOString(); } function logScope(scope: string): string { return `${scope} run=${APP_RUN_ID}`; } function sanitizeLogValue(value: string): string { return value.replace(/\s+/g, " ").trim(); } function readPersistedBackendObservabilitySettings(): { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined; } { try { if (!FS.existsSync(SERVER_SETTINGS_PATH)) { return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } return parsePersistedServerObservabilitySettings(FS.readFileSync(SERVER_SETTINGS_PATH, "utf8")); } catch (error) { console.warn("[desktop] failed to read persisted backend observability settings", error); return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } } function backendChildEnv(): NodeJS.ProcessEnv { const env = { ...process.env }; delete env.T3CODE_PORT; delete env.T3CODE_AUTH_TOKEN; delete env.T3CODE_MODE; delete env.T3CODE_NO_BROWSER; delete env.T3CODE_HOST; delete env.T3CODE_DESKTOP_WS_URL; return env; } function writeDesktopLogHeader(message: string): void { if (!desktopLogSink) return; desktopLogSink.write(`[${logTimestamp()}] [${logScope("desktop")}] ${message}\n`); } function writeBackendSessionBoundary(phase: "START" | "END", details: string): void { if (!backendLogSink) return; const normalizedDetails = sanitizeLogValue(details); backendLogSink.write( `[${logTimestamp()}] ---- APP SESSION ${phase} run=${APP_RUN_ID} ${normalizedDetails} ----\n`, ); } function formatErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } function getSafeExternalUrl(rawUrl: unknown): string | null { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return null; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return null; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return null; } return parsedUrl.toString(); } function getSafeTheme(rawTheme: unknown): DesktopTheme | null { if (rawTheme === "light" || rawTheme === "dark" || rawTheme === "system") { return rawTheme; } return null; } function writeDesktopStreamChunk( streamName: "stdout" | "stderr", chunk: unknown, encoding: BufferEncoding | undefined, ): void { if (!desktopLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), typeof chunk === "string" ? encoding : undefined); desktopLogSink.write(`[${logTimestamp()}] [${logScope(streamName)}] `); desktopLogSink.write(buffer); if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) { desktopLogSink.write("\n"); } } function installStdIoCapture(): void { if (!app.isPackaged || desktopLogSink === null || restoreStdIoCapture !== null) { return; } const originalStdoutWrite = process.stdout.write.bind(process.stdout); const originalStderrWrite = process.stderr.write.bind(process.stderr); const patchWrite = (streamName: "stdout" | "stderr", originalWrite: typeof process.stdout.write) => ( chunk: string | Uint8Array, encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void, ): boolean => { const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined; writeDesktopStreamChunk(streamName, chunk, encoding); if (typeof encodingOrCallback === "function") { return originalWrite(chunk, encodingOrCallback); } if (callback !== undefined) { return originalWrite(chunk, encoding, callback); } if (encoding !== undefined) { return originalWrite(chunk, encoding); } return originalWrite(chunk); }; process.stdout.write = patchWrite("stdout", originalStdoutWrite); process.stderr.write = patchWrite("stderr", originalStderrWrite); restoreStdIoCapture = () => { process.stdout.write = originalStdoutWrite; process.stderr.write = originalStderrWrite; restoreStdIoCapture = null; }; } function initializePackagedLogging(): void { if (!app.isPackaged) return; try { desktopLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "desktop-main.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); backendLogSink = new RotatingFileSink({ filePath: Path.join(LOG_DIR, "server-child.log"), maxBytes: LOG_FILE_MAX_BYTES, maxFiles: LOG_FILE_MAX_FILES, }); installStdIoCapture(); writeDesktopLogHeader(`runtime log capture enabled logDir=${LOG_DIR}`); } catch (error) { // Logging setup should never block app startup. console.error("[desktop] failed to initialize packaged logging", error); } } function captureBackendOutput(child: ChildProcess.ChildProcess): void { if (!app.isPackaged || backendLogSink === null) return; const writeChunk = (chunk: unknown): void => { if (!backendLogSink) return; const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"); backendLogSink.write(buffer); }; child.stdout?.on("data", writeChunk); child.stderr?.on("data", writeChunk); } initializePackagedLogging(); if (process.platform === "linux") { app.commandLine.appendSwitch("class", LINUX_WM_CLASS); } function getDestructiveMenuIcon(): Electron.NativeImage | undefined { if (process.platform !== "darwin") return undefined; if (destructiveMenuIconCache !== undefined) { return destructiveMenuIconCache ?? undefined; } try { const icon = nativeImage.createFromNamedImage("trash").resize({ width: 14, height: 14, }); if (icon.isEmpty()) { destructiveMenuIconCache = null; return undefined; } icon.setTemplateImage(true); destructiveMenuIconCache = icon; return icon; } catch { destructiveMenuIconCache = null; return undefined; } } let updatePollTimer: ReturnType<typeof setInterval> | null = null; let updateStartupTimer: ReturnType<typeof setTimeout> | null = null; let updateCheckInFlight = false; let updateDownloadInFlight = false; let updateInstallInFlight = false; let updaterConfigured = false; let updateState: DesktopUpdateState = initialUpdateState(); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateInstallInFlight) return "install"; if (updateDownloadInFlight) return "download"; if (updateCheckInFlight) return "check"; return updateState.errorContext; } protocol.registerSchemesAsPrivileged([ { scheme: DESKTOP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, }, ]); function resolveAppRoot(): string { if (!app.isPackaged) { return ROOT_DIR; } return app.getAppPath(); } /** Read the baked-in app-update.yml config (if applicable). */ function readAppUpdateYml(): Record<string, string> | null { try { // electron-updater reads from process.resourcesPath in packaged builds, // or dev-app-update.yml via app.getAppPath() in dev. const ymlPath = app.isPackaged ? Path.join(process.resourcesPath, "app-update.yml") : Path.join(app.getAppPath(), "dev-app-update.yml"); const raw = FS.readFileSync(ymlPath, "utf-8"); // The YAML is simple key-value pairs — avoid pulling in a YAML parser by // doing a line-based parse (fields: provider, owner, repo, releaseType, …). const entries: Record<string, string> = {}; for (const line of raw.split("\n")) { const match = line.match(/^(\w+):\s*(.+)$/); if (match?.[1] && match[2]) entries[match[1]] = match[2].trim(); } return entries.provider ? entries : null; } catch { return null; } } function normalizeCommitHash(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); if (!COMMIT_HASH_PATTERN.test(trimmed)) { return null; } return trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase(); } function resolveEmbeddedCommitHash(): string | null { const packageJsonPath = Path.join(resolveAppRoot(), "package.json"); if (!FS.existsSync(packageJsonPath)) { return null; } try { const raw = FS.readFileSync(packageJsonPath, "utf8"); const parsed = JSON.parse(raw) as { t3codeCommitHash?: unknown }; return normalizeCommitHash(parsed.t3codeCommitHash); } catch { return null; } } function resolveAboutCommitHash(): string | null { if (aboutCommitHashCache !== undefined) { return aboutCommitHashCache; } const envCommitHash = normalizeCommitHash(process.env.T3CODE_COMMIT_HASH); if (envCommitHash) { aboutCommitHashCache = envCommitHash; return aboutCommitHashCache; } // Only packaged builds are required to expose commit metadata. if (!app.isPackaged) { aboutCommitHashCache = null; return aboutCommitHashCache; } aboutCommitHashCache = resolveEmbeddedCommitHash(); return aboutCommitHashCache; } function resolveBackendEntry(): string { return Path.join(resolveAppRoot(), "apps/server/dist/bin.mjs"); } function resolveBackendCwd(): string { if (!app.isPackaged) { return resolveAppRoot(); } return OS.homedir(); } function resolveDesktopStaticDir(): string | null { const appRoot = resolveAppRoot(); const candidates = [ Path.join(appRoot, "apps/server/dist/client"), Path.join(appRoot, "apps/web/dist"), ]; for (const candidate of candidates) { if (FS.existsSync(Path.join(candidate, "index.html"))) { return candidate; } } return null; } function resolveDesktopStaticPath(staticRoot: string, requestUrl: string): string { const url = new URL(requestUrl); const rawPath = decodeURIComponent(url.pathname); const normalizedPath = Path.posix.normalize(rawPath).replace(/^\/+/, ""); if (normalizedPath.includes("..")) { return Path.join(staticRoot, "index.html"); } const requestedPath = normalizedPath.length > 0 ? normalizedPath : "index.html"; const resolvedPath = Path.join(staticRoot, requestedPath); if (Path.extname(resolvedPath)) { return resolvedPath; } const nestedIndex = Path.join(resolvedPath, "index.html"); if (FS.existsSync(nestedIndex)) { return nestedIndex; } return Path.join(staticRoot, "index.html"); } function isStaticAssetRequest(requestUrl: string): boolean { try { const url = new URL(requestUrl); return Path.extname(url.pathname).length > 0; } catch { return false; } } function handleFatalStartupError(stage: string, error: unknown): void { const message = formatErrorMessage(error); const detail = error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : ""; writeDesktopLogHeader(`fatal startup error stage=${stage} message=${message}`); console.error(`[desktop] fatal startup error (${stage})`, error); if (!isQuitting) { isQuitting = true; dialog.showErrorBox("T3 Code failed to start", `Stage: ${stage}\n${message}${detail}`); } stopBackend(); restoreStdIoCapture?.(); app.quit(); } function registerDesktopProtocol(): void { if (isDevelopment || desktopProtocolRegistered) return; const staticRoot = resolveDesktopStaticDir(); if (!staticRoot) { throw new Error( "Desktop static bundle missing. Build apps/server (with bundled client) first.",