1import * as MacPermissions from "./permissions/MacPermissions.ts";
2for (const stream of [process.stdout, process.stderr]) {
3 stream.on("error", (err: NodeJS.ErrnoException) => {
4 if (err.code !== "EPIPE") throw err;
5 });
6}
7
8import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
9import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
10import * as NodeServices from "@effect/platform-node/NodeServices";
11// @effect-diagnostics-next-line nodeBuiltinImport:off - Version output must flush before Electron exits, without acquiring the runtime.
12import * as NodeFS from "node:fs";
13import * as NodeOS from "node:os";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17
18import * as Electron from "electron";
19
20import * as NetService from "@t3tools/shared/Net";
21import * as HostProcess from "@t3tools/shared/HostProcess";
22import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";
23import serverPackageJson from "../../server/package.json" with { type: "json" };
24
25import * as DesktopIpc from "./ipc/DesktopIpc.ts";
26import * as DesktopVideo from "./media/DesktopVideo.ts";
27import * as ElectronApp from "./electron/ElectronApp.ts";
28import * as ElectronDialog from "./electron/ElectronDialog.ts";
29import * as ElectronMenu from "./electron/ElectronMenu.ts";
30import * as ElectronPowerMonitor from "./electron/ElectronPowerMonitor.ts";
31import * as ElectronProtocol from "./electron/ElectronProtocol.ts";
32import * as ElectronSafeStorage from "./electron/ElectronSafeStorage.ts";
33import * as ElectronShell from "./electron/ElectronShell.ts";
34import * as ElectronTheme from "./electron/ElectronTheme.ts";
35import * as ElectronUpdater from "./electron/ElectronUpdater.ts";
36import * as ElectronWindow from "./electron/ElectronWindow.ts";
37import * as DesktopApp from "./app/DesktopApp.ts";
38import * as DesktopAppActivation from "./app/DesktopAppActivation.ts";
39import * as DesktopWebLinks from "./app/DesktopWebLinks.ts";
40import * as DesktopAppIdentity from "./app/DesktopAppIdentity.ts";
41import * as DesktopConnectionCatalogStore from "./app/DesktopConnectionCatalogStore.ts";
42import * as DesktopHostedAuth from "./app/DesktopHostedAuth.ts";
43import * as DesktopClerk from "./app/DesktopClerk.ts";
44import * as DesktopCliCommand from "./app/DesktopCliCommand.ts";
45import * as DesktopApplicationMenu from "./window/DesktopApplicationMenu.ts";
46import * as DesktopAssets from "./app/DesktopAssets.ts";
47import * as DesktopBackendConfiguration from "./backend/DesktopBackendConfiguration.ts";
48import * as DesktopTerminalHost from "./backend/DesktopTerminalHost.ts";
49import * as DesktopBackendPool from "./backend/DesktopBackendPool.ts";
50import * as DesktopLocalEnvironmentAuth from "./backend/DesktopLocalEnvironmentAuth.ts";
51import * as DesktopNetworkInterfaces from "./backend/DesktopNetworkInterfaces.ts";
52import * as DesktopEnvironment from "./app/DesktopEnvironment.ts";
53import * as DesktopLifecycle from "./app/DesktopLifecycle.ts";
54import * as DesktopLinuxUrlHandler from "./app/DesktopLinuxUrlHandler.ts";
55import * as DesktopShutdown from "./app/DesktopShutdown.ts";
56import * as DesktopObservability from "./app/DesktopObservability.ts";
57import * as DesktopServerExposure from "./backend/DesktopServerExposure.ts";
58import * as DesktopClientSettings from "./settings/DesktopClientSettings.ts";
59import * as DesktopSavedEnvironments from "./settings/DesktopSavedEnvironments.ts";
60import * as DesktopSnapShot from "./snapShot/DesktopSnapShot.ts";
61import * as DesktopAppSettings from "./settings/DesktopAppSettings.ts";
62import * as DesktopPreReadyFileSystem from "./app/DesktopPreReadyFileSystem.ts";
63import * as DesktopPreReadyPlatform from "./app/DesktopPreReadyPlatform.ts";
64import * as DesktopShellEnvironment from "./shell/DesktopShellEnvironment.ts";
65import * as DesktopSshEnvironment from "./ssh/DesktopSshEnvironment.ts";
66import * as DesktopSshPasswordPrompts from "./ssh/DesktopSshPasswordPrompts.ts";
67import * as DesktopState from "./app/DesktopState.ts";
68import * as DesktopLegacyLocalStorage from "./app/DesktopLegacyLocalStorage.ts";
69import * as DesktopTelemetryPublisher from "./telemetry/DesktopTelemetryPublisher.ts";
70import * as DesktopRendererHistory from "./telemetry/DesktopRendererHistory.ts";
71import * as DesktopUpdates from "./updates/DesktopUpdates.ts";
72import * as DesktopLocalUi from "./updates/DesktopLocalUi.ts";
73import * as DesktopForkUpdates from "./updates/DesktopForkUpdates.ts";
74import { activeForkRuntime } from "./updates/forkRuntime.ts";
75import * as BrowserImport from "./preview/BrowserImport/BrowserImport.ts";
76import * as LinuxBrowserSecret from "./preview/BrowserImport/LinuxBrowserSecret.ts";
77import * as BrowserSession from "./preview/BrowserSession.ts";
78import * as DesktopBrowserHost from "./preview/DesktopBrowserHost.ts";
79import * as PreviewManager from "./preview/Manager.ts";
80import * as PreviewPasskeys from "./preview/Passkeys.ts";
81import * as DesktopWindow from "./window/DesktopWindow.ts";
82import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts";
83import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts";
84import * as DesktopWslServerTree from "./wsl/DesktopWslServerTree.ts";
85
86if (process.argv.includes("--version")) {
87 try {
88 NodeFS.writeSync(
89 process.stdout.fd,
90 `${activeForkRuntime()?.version ?? Electron.app.getVersion()}\n`,
91 );
92 } catch (error) {
93 if (!(error instanceof Error) || !("code" in error) || error.code !== "EPIPE") throw error;
94 }
95 Electron.app.exit(0);
96}
97
98const layerDesktopEnvironment = Layer.unwrap(
99 Effect.gen(function* () {
100 const metadata = yield* Effect.service(ElectronApp.ElectronApp).pipe(
101 Effect.flatMap((app) => app.metadata),
102 );
103 const platform = yield* HostProcess.Platform;
104 const processArch = yield* HostProcess.Architecture;
105 return DesktopEnvironment.layer({
106 dirname: __dirname,
107 homeDirectory: NodeOS.homedir(),
108 platform,
109 processArch,
110 ...metadata,
111 });
112 }),
113);
114
115// The remote runs the exact release this app is on, from its self-contained
116// archive, so it needs neither Node nor npm. Development points the remote at
117// a source checkout instead so the two sides can be iterated together.
118const resolveDesktopSshCliRunner = (
119 environment: DesktopEnvironment.DesktopEnvironment["Service"],
120): RemoteT3RunnerOptions => {
121 const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath);
122 if (environment.isDevelopment && devRemoteEntryPath !== undefined) {
123 return {
124 nodeScriptPath: devRemoteEntryPath,
125 nodeEngineRange: serverPackageJson.engines.node,
126 };
127 }
128 return { archiveVersion: environment.appVersion };
129};
130
131const layerDesktopSshEnvironment = Layer.unwrap(
132 Effect.gen(function* () {
133 const environment = yield* DesktopEnvironment.DesktopEnvironment;
134 return DesktopSshEnvironment.layer({
135 resolveCliRunner: Effect.succeed(resolveDesktopSshCliRunner(environment)),
136 });
137 }),
138);
139
140const layerElectron = Layer.mergeAll(
141 ElectronApp.layer,
142 ElectronDialog.layer,
143 ElectronMenu.layer,
144 ElectronPowerMonitor.layer,
145 ElectronProtocol.layer,
146 ElectronSafeStorage.layer,
147 ElectronShell.layer,
148 ElectronTheme.layer,
149 ElectronUpdater.layer,
150 ElectronWindow.layer,
151 DesktopIpc.layer(Electron.ipcMain),
152);
153
154const layerDesktopFoundation = Layer.mergeAll(
155 MacPermissions.layer,
156 DesktopState.layer,
157 DesktopShutdown.layer,
158 DesktopLegacyLocalStorage.layer,
159 DesktopAppSettings.layer,
160 DesktopClientSettings.layer,
161 DesktopConnectionCatalogStore.layer.pipe(Layer.provideMerge(DesktopSavedEnvironments.layer)),
162 DesktopAssets.layer,
163 DesktopObservability.layer,
164 DesktopRendererHistory.layer,
165).pipe(Layer.provideMerge(layerDesktopEnvironment));
166
167const layerDesktopSsh = layerDesktopSshEnvironment.pipe(
168 Layer.provideMerge(DesktopSshPasswordPrompts.layer()),
169);
170
171const layerDesktopServerExposure = DesktopServerExposure.layer.pipe(
172 Layer.provideMerge(DesktopNetworkInterfaces.layer),
173 Layer.provideMerge(layerDesktopFoundation),
174);
175
176const layerDesktopPreview = PreviewManager.layer.pipe(
177 Layer.provideMerge(DesktopBrowserHost.layer),
178 // Merged rather than provided so the IPC handlers can reach the import
179 // service alongside the manager; both sit on the same BrowserSession.
180 Layer.provideMerge(BrowserImport.layer.pipe(Layer.provide(LinuxBrowserSecret.layer))),
181 Layer.provideMerge(BrowserSession.layer),
182 Layer.provideMerge(PreviewPasskeys.layer),
183 Layer.provideMerge(layerDesktopFoundation),
184);
185
186const layerDesktopWindow = DesktopWindow.layer.pipe(
187 Layer.provideMerge(layerDesktopServerExposure),
188 Layer.provideMerge(layerDesktopPreview),
189);
190
191const layerDesktopSnapShot = DesktopSnapShot.layer.pipe(
192 Layer.provideMerge(layerDesktopWindow),
193 Layer.provideMerge(layerDesktopFoundation),
194);
195const layerDesktopAppActivation = DesktopAppActivation.layer.pipe(
196 Layer.provide(layerDesktopWindow),
197);
198
199// Pool layer instantiates the backend factory once for the Windows
200// primary instance and exposes it via pool.primary. Consumers go through
201// the pool now; the legacy DesktopBackendManager service is gone. The
202// WSL second instance gets registered later in the migration. See
203// DesktopBackendPool.ts header for the full rollout plan.
204const layerDesktopBackend = DesktopBackendPool.layer.pipe(
205 Layer.provideMerge(DesktopTerminalHost.layer),
206 Layer.provideMerge(DesktopAppIdentity.layer),
207 Layer.provideMerge(DesktopBackendConfiguration.layer),
208 Layer.provideMerge(DesktopWslEnvironment.layer),
209 Layer.provideMerge(DesktopWslServerTree.layer),
210 Layer.provideMerge(DesktopTelemetryPublisher.layer),
211 Layer.provideMerge(layerDesktopWindow),
212);
213
214// WSL orchestrator hangs off the backend layer because it needs the
215// pool + configuration + serverExposure; it pulls NetService and the
216// foundation services through the same provideMerge chain.
217const layerDesktopWslBackend = DesktopWslBackend.layer.pipe(
218 Layer.provideMerge(layerDesktopBackend),
219);
220
221const layerDesktopLocalEnvironmentAuth = DesktopLocalEnvironmentAuth.layer.pipe(
222 Layer.provideMerge(layerDesktopBackend),
223);
224
225const layerDesktopApplication = Layer.mergeAll(
226 DesktopHostedAuth.layer,
227 DesktopLifecycle.layer,
228 DesktopVideo.layer,
229 layerDesktopAppActivation,
230 DesktopWebLinks.layer,
231 DesktopApplicationMenu.layer,
232 DesktopLinuxUrlHandler.layer,
233 DesktopCliCommand.layer,
234 DesktopShellEnvironment.layer,
235 layerDesktopSsh,
236).pipe(
237 Layer.provideMerge(layerDesktopSnapShot),
238 Layer.provideMerge(DesktopUpdates.layer),
239 Layer.provideMerge(DesktopForkUpdates.layer),
240 Layer.provideMerge(DesktopLocalUi.layer),
241 Layer.provideMerge(layerDesktopWslBackend),
242 Layer.provideMerge(layerDesktopLocalEnvironmentAuth),
243);
244
245// Clerk resolves userData before Electron is ready, so it gets the synchronous FileSystem.
246const layerDesktopClerk = DesktopClerk.layer.pipe(
247 Layer.provide(DesktopPreReadyFileSystem.layer),
248 Layer.provideMerge(ElectronShell.layer),
249 Layer.provideMerge(layerDesktopEnvironment),
250 Layer.provideMerge(NodeServices.layer),
251 Layer.provideMerge(ElectronApp.layer),
252);
253
254const layerDesktopApplicationRuntime = layerDesktopApplication.pipe(
255 Layer.provideMerge(NodeServices.layer),
256 Layer.provideMerge(NodeHttpClient.layerUndici),
257 Layer.provideMerge(NetService.layer),
258 Layer.provideMerge(layerElectron),
259);
260
261// Acquire strict pre-ready setup before Clerk. Nothing before the Clerk bridge
262// may yield, or Electron can emit ready before Clerk registers its scheme.
263const layerDesktopRuntime = layerDesktopClerk.pipe(
264 Layer.flatMap((clerkContext) =>
265 layerDesktopApplicationRuntime.pipe(Layer.provideMerge(Layer.succeedContext(clerkContext))),
266 ),
267 Layer.provideMerge(DesktopPreReadyPlatform.layer),
268);
269
270DesktopApp.program.pipe(Effect.provide(layerDesktopRuntime), NodeRuntime.runMain);