| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import { randomUUID } from "node:crypto"
- import { mkdirSync, rmSync } from "node:fs"
- import * as http from "node:http"
- import { createServer } from "node:net"
- import { homedir, tmpdir } from "node:os"
- import { join } from "node:path"
- import { getCACertificates, setDefaultCACertificates } from "node:tls"
- import type { Event } from "electron"
- import { app } from "electron"
- import { Deferred, Effect, Fiber } from "effect"
- import contextMenu from "electron-context-menu"
- import type { ServerReadyData } from "../preload/types"
- import { checkAppExists, resolveAppPath } from "./apps"
- import { CHANNEL } from "./constants"
- import { registerIpcHandlers, sendDeepLinks, sendMenuCommand } from "./ipc"
- import { forwardInitializationFailure } from "./initialization"
- import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
- import { parseMarkdown } from "./markdown"
- import { createMenu } from "./menu"
- import { ensureDefaultModes, finishFirstLaunchOnboarding, isFirstLaunchOnboardingPending } from "./onboarding"
- import {
- getDefaultServerUrl,
- preferAppEnv,
- setDefaultServerUrl,
- spawnLocalServer,
- type SidecarListener,
- } from "./server"
- import { setupAutoUpdater, showUpdaterDialog } from "./updater"
- import {
- getLastFocusedWindow,
- registerRendererProtocol,
- setRelaunchHandler,
- setAppQuitting,
- setBackgroundColor,
- setDockIcon,
- restoreMainWindows,
- } from "./windows"
- import { createWslServersController } from "./wsl/servers"
- import { registerWslIpcHandlers } from "./wsl/ipc"
- import { spawnWslSidecar } from "./wsl/sidecar"
- import { migrate } from "./migrate"
- import { cleanupStoreFiles } from "./store-cleanup"
- const APP_NAMES: Record<string, string> = {
- dev: "KirinCode Dev",
- beta: "KirinCode Beta",
- prod: "KirinCode",
- }
- const APP_IDS: Record<string, string> = {
- dev: "ai.kirincode.desktop.dev",
- beta: "ai.kirincode.desktop.beta",
- prod: "ai.kirincode.desktop",
- }
- const TEST_ONBOARDING = process.env.KIRINCODE_TEST_ONBOARDING === "1"
- const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
- let logger: ReturnType<typeof initLogging>
- let server: SidecarListener | null = null
- const pendingDeepLinks: string[] = []
- function useEnvProxy() {
- try {
- // Electron 41.2 runs Node 24.14.1; latest @types/node@24 is 24.12.2.
- ;(http as any).setGlobalProxyFromEnv()
- } catch (error) {
- logger.warn("failed to load proxy environment", error)
- }
- }
- function emitDeepLinks(urls: string[]) {
- if (urls.length === 0) return
- pendingDeepLinks.push(...urls)
- const win = getLastFocusedWindow()
- if (win) sendDeepLinks(win, urls)
- }
- async function killSidecar() {
- if (!server) return
- const current = server
- server = null
- await current.stop()
- }
- function ensureLoopbackNoProxy() {
- const loopback = ["127.0.0.1", "localhost", "::1"]
- const upsert = (key: string) => {
- const items = (process.env[key] ?? "")
- .split(",")
- .map((value: string) => value.trim())
- .filter((value: string) => Boolean(value))
- for (const host of loopback) {
- if (items.some((value: string) => value.toLowerCase() === host)) continue
- items.push(host)
- }
- process.env[key] = items.join(",")
- }
- upsert("NO_PROXY")
- upsert("no_proxy")
- }
- const main = Effect.gen(function* () {
- contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
- // on macOS apps run in `/` which can cause issues with ripgrep
- try {
- process.chdir(homedir())
- } catch {}
- process.env.KIRINCODE_DISABLE_EMBEDDED_WEB_UI = "true"
- const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.kirincode.desktop.dev"
- const onboardingTestRoot = ((): string | undefined => {
- if (!TEST_ONBOARDING) return
- const root = join(tmpdir(), `opencode-onboarding-${randomUUID()}`)
- rmSync(root, { recursive: true, force: true })
- ;["data", "config", "cache", "state", "desktop", "session"].forEach((dir) =>
- mkdirSync(join(root, dir), { recursive: true }),
- )
- process.env.KIRINCODE_DB = ":memory:"
- process.env.XDG_DATA_HOME = join(root, "data")
- process.env.XDG_CONFIG_HOME = join(root, "config")
- process.env.XDG_CACHE_HOME = join(root, "cache")
- process.env.XDG_STATE_HOME = join(root, "state")
- return root
- })()
- app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "KirinCode Dev")
- app.setAppUserModelId(appId)
- app.setPath(
- "userData",
- onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId),
- )
- if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
- logger = initLogging()
- initCrashReporter()
- const wslServers = createWslServersController(
- app.getVersion(),
- async (distro) => {
- logger.log("spawning wsl sidecar", { distro })
- return spawnWslSidecar(distro, {
- onLine: (line) => logger.log("wsl sidecar", { distro, stream: line.stream, text: line.text }),
- })
- },
- {
- logger: {
- log: (message, meta) => logger.log(message, meta),
- error: (message, meta) => logger.error(message, meta),
- },
- },
- )
- const stopSidecars = async () => {
- await killSidecar()
- wslServers.stopAll()
- }
- const relaunch = () => {
- setAppQuitting()
- void stopSidecars().finally(() => {
- app.relaunch()
- app.exit(0)
- })
- }
- try {
- setDefaultCACertificates([...new Set([...getCACertificates("default"), ...getCACertificates("system")])])
- } catch (error) {
- logger.warn("failed to load system certificates", error)
- }
- logger.log("app starting", {
- version: app.getVersion(),
- packaged: app.isPackaged,
- onboardingTest: Boolean(onboardingTestRoot),
- })
- ensureLoopbackNoProxy()
- useEnvProxy()
- app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
- const features = app.commandLine.getSwitchValue("enable-features")
- app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature)
- if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222")
- if (!app.requestSingleInstanceLock()) {
- app.quit()
- return
- }
- preferAppEnv(app.getPath("userData"))
- app.on("second-instance", (_event: Event, argv: string[]) => {
- const urls = argv.filter((arg: string) => arg.startsWith("kirincode://"))
- if (urls.length) {
- logger.log("deep link received via second-instance", { urls })
- emitDeepLinks(urls)
- }
- const win = getLastFocusedWindow()
- if (win) {
- win.show()
- win.focus()
- }
- })
- app.on("open-url", (event: Event, url: string) => {
- event.preventDefault()
- logger.log("deep link received via open-url", { url })
- emitDeepLinks([url])
- })
- app.on("before-quit", () => {
- setAppQuitting()
- void stopSidecars()
- })
- app.on("will-quit", () => {
- setAppQuitting()
- void stopSidecars()
- })
- app.on("child-process-gone", (_event, details) => {
- writeLog("utility", "child process gone", { details }, "error")
- })
- app.on("render-process-gone", (_event, webContents, details) => {
- writeLog("window", "app render process gone", { url: webContents.getURL(), details }, "error")
- })
- setRelaunchHandler(() => {
- relaunch()
- })
- for (const signal of ["SIGINT", "SIGTERM"] as const) {
- process.on(signal, () => {
- setAppQuitting()
- void stopSidecars().finally(() => app.exit(0))
- })
- }
- const serverReady = Deferred.makeUnsafe<ServerReadyData, unknown>()
- yield* Effect.promise(() => app.whenReady())
- yield* Effect.promise(() => ensureDefaultModes())
- if (!TEST_ONBOARDING) migrate()
- yield* Effect.promise(() => cleanupStoreFiles(app.getPath("userData"))).pipe(
- Effect.tap((result) =>
- Effect.sync(() => {
- if (result.deleted.length === 0) return
- logger.log("cleaned scoped store files", { count: result.deleted.length, scanned: result.scanned })
- }),
- ),
- Effect.catch((error) =>
- Effect.sync(() => {
- logger.warn("failed to clean scoped store files", error)
- }),
- ),
- )
- app.setAsDefaultProtocolClient("kirincode")
- registerRendererProtocol()
- setDockIcon()
- const updater = setupAutoUpdater(stopSidecars)
- registerIpcHandlers({
- killSidecar: () => killSidecar(),
- relaunch,
- awaitInitialization: Effect.fnUntraced(
- function* () {
- logger.log("awaiting server ready")
- const res = yield* Deferred.await(serverReady)
- logger.log("server ready", { url: res.url })
- return res
- },
- (e) => Effect.runPromise(e),
- ),
- consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
- getDefaultServerUrl: () => getDefaultServerUrl(),
- setDefaultServerUrl: (url) => setDefaultServerUrl(url),
- isFirstLaunchOnboardingPending,
- finishFirstLaunchOnboarding,
- getDisplayBackend: async () => null,
- setDisplayBackend: async () => undefined,
- parseMarkdown: async (markdown) => parseMarkdown(markdown),
- checkAppExists: (appName) => checkAppExists(appName),
- resolveAppPath: async (appName) => resolveAppPath(appName),
- updater,
- showUpdater: () => showUpdaterDialog(updater, true),
- setBackgroundColor: (color) => setBackgroundColor(color),
- exportDebugLogs: () => exportDebugLogs(),
- recordFatalRendererError: (error) => writeLog("renderer", "fatal renderer error", { ...error }, "error"),
- })
- registerWslIpcHandlers(wslServers)
- void updater.start()
- const updateTimer = setInterval(() => void updater.check(), 10 * 60 * 1000)
- updateTimer.unref()
- app.once("will-quit", () => clearInterval(updateTimer))
- yield* Effect.promise(() => startNetLog()).pipe(
- Effect.catch((error) =>
- Effect.sync(() => {
- logger.warn("failed to start net log", error)
- }),
- ),
- )
- const port = yield* Effect.gen(function* () {
- const fromEnv = process.env.KIRINCODE_PORT
- if (fromEnv) {
- const parsed = Number.parseInt(fromEnv, 10)
- if (!Number.isNaN(parsed)) return parsed
- }
- const res = yield* Deferred.make<number, unknown>()
- const server = createServer()
- server.on("error", (e) => Deferred.failSync(res, () => e))
- server.listen(0, "127.0.0.1", () => {
- const address = server.address()
- if (typeof address !== "object" || !address) {
- server.close()
- Deferred.failSync(res, () => new Error("Failed to get port"))
- return
- }
- const port = address.port
- server.close(() => Effect.runSync(Deferred.succeed(res, port)))
- })
- return yield* Deferred.await(res)
- })
- const hostname = "127.0.0.1"
- const url = `http://${hostname}:${port}`
- const password = randomUUID()
- const loadingTask = yield* Effect.gen(function* () {
- logger.log("sidecar connection started", { url })
- ensureLoopbackNoProxy()
- useEnvProxy()
- logger.log("spawning sidecar", { url })
- const { listener, health } = yield* Effect.promise(() =>
- spawnLocalServer(hostname, port, password, {
- userDataPath: app.getPath("userData"),
- onStdout: (message) => writeLog("server", "stdout", { message }),
- onStderr: (message) => writeLog("server", "stderr", { message }, "warn"),
- onExit: (code) => writeLog("utility", "sidecar exited", { code }, "warn"),
- }),
- )
- server = listener
- yield* Deferred.succeed(serverReady, {
- url,
- username: "kirincode",
- password,
- })
- if (process.platform === "win32") {
- void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error))
- }
- yield* Effect.promise(() => health.wait).pipe(
- Effect.timeout("30 seconds"),
- Effect.catch((e) =>
- Effect.sync(() => {
- logger.error("sidecar health check failed", e.toString())
- }),
- ),
- )
- logger.log("loading task finished")
- }).pipe(forwardInitializationFailure(serverReady), Effect.forkChild)
- yield* Fiber.await(loadingTask)
- const windows = restoreMainWindows()
- if (windows.length) {
- createMenu({
- trigger: (id) => {
- const win = getLastFocusedWindow()
- if (win) sendMenuCommand(win, id)
- },
- checkForUpdates: () => {
- void showUpdaterDialog(updater, true)
- },
- relaunch: () => {
- relaunch()
- },
- })
- }
- })
- Effect.runFork(main)
|