@morlay/dsh-desktopify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/cli/index.mjs +1252 -0
- package/dist/index.mjs +860 -0
- package/dist/preload-app.cjs +4 -0
- package/dist/preload.cjs +4 -0
- package/dist/seed-Ca0AMFtp.mjs +152 -0
- package/package.json +51 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { i as loadAppConfig, n as ensureSeedProfile } from "./seed-Ca0AMFtp.mjs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { BrowserWindow, app, dialog, protocol } from "electron";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { once } from "node:events";
|
|
9
|
+
import { Readable, Writable } from "node:stream";
|
|
10
|
+
//#region src/dshhome.ts
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the runtime `DSH_HOME` from the shell configuration, following the
|
|
13
|
+
* reference desktop semantics: `xdg` maps to the XDG data home
|
|
14
|
+
* (`~/Library/Application Support` on macOS, `$XDG_DATA_HOME` or
|
|
15
|
+
* `~/.local/share` on Linux) joined with the application name, so the packaged
|
|
16
|
+
* app shares the same data root as the reference implementation and never
|
|
17
|
+
* touches the workspace. `env` inherits the environment; an absolute path pins
|
|
18
|
+
* the home. `DSH_APP_DSH_HOME` overrides everything (development and tests).
|
|
19
|
+
* @module @morlay/dsh-desktopify
|
|
20
|
+
*/
|
|
21
|
+
/** XDG data home for the current platform (macOS and Linux). */
|
|
22
|
+
function xdgDataHome() {
|
|
23
|
+
if (process.platform === "darwin") return join(homedir(), "Library", "Application Support");
|
|
24
|
+
const configured = process.env.XDG_DATA_HOME;
|
|
25
|
+
if (configured !== void 0 && configured.trim() !== "") return resolve(configured);
|
|
26
|
+
return join(homedir(), ".local", "share");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the runtime `DSH_HOME` for one shell configuration.
|
|
30
|
+
* @param config - shell configuration read beside the executable.
|
|
31
|
+
* @returns the absolute home, or `undefined` when the strategy is `env`
|
|
32
|
+
* (the caller then leaves `DSH_HOME` unset).
|
|
33
|
+
*/
|
|
34
|
+
function resolveDshHome(config) {
|
|
35
|
+
const override = process.env.DSH_APP_DSH_HOME;
|
|
36
|
+
if (override !== void 0 && override.trim() !== "") return resolve(override);
|
|
37
|
+
if (config.dshHome === "env") return void 0;
|
|
38
|
+
if (config.dshHome === "xdg") return join(xdgDataHome(), config.name);
|
|
39
|
+
if (!isAbsolute(config.dshHome)) throw new Error(`dsh desktop: dshHome must be xdg, env, or an absolute path, got ${JSON.stringify(config.dshHome)}`);
|
|
40
|
+
return config.dshHome;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/host-protocol.ts
|
|
44
|
+
/** Maximum raw body bytes carried by one data frame. */
|
|
45
|
+
const DESKTOP_PIPE_CHUNK_BYTES = 65536;
|
|
46
|
+
const FRAME_MAGIC = 1146308659;
|
|
47
|
+
const FRAME_HEADER_BYTES = 13;
|
|
48
|
+
const MAX_CONTROL_PAYLOAD_BYTES = 1048576;
|
|
49
|
+
const REQUEST_FRAME_START = 1;
|
|
50
|
+
const REQUEST_FRAME_DATA = 2;
|
|
51
|
+
const REQUEST_FRAME_END = 3;
|
|
52
|
+
const REQUEST_FRAME_CANCEL = 4;
|
|
53
|
+
const RESPONSE_FRAME_START = 1;
|
|
54
|
+
const RESPONSE_FRAME_DATA = 2;
|
|
55
|
+
const RESPONSE_FRAME_END = 3;
|
|
56
|
+
const RESPONSE_FRAME_ERROR = 4;
|
|
57
|
+
function isRecord(value) {
|
|
58
|
+
return typeof value === "object" && value !== null;
|
|
59
|
+
}
|
|
60
|
+
function isHeaders(value) {
|
|
61
|
+
return Array.isArray(value) && value.every((header) => Array.isArray(header) && header.length === 2 && typeof header[0] === "string" && typeof header[1] === "string");
|
|
62
|
+
}
|
|
63
|
+
function assertStreamId(streamId) {
|
|
64
|
+
if (!Number.isInteger(streamId) || streamId < 1 || streamId > 4294967295) throw new Error(`dsh desktop: invalid pipe stream id ${String(streamId)}`);
|
|
65
|
+
}
|
|
66
|
+
function encodeFrame(type, streamId, payload) {
|
|
67
|
+
assertStreamId(streamId);
|
|
68
|
+
const limit = type === REQUEST_FRAME_DATA ? DESKTOP_PIPE_CHUNK_BYTES : MAX_CONTROL_PAYLOAD_BYTES;
|
|
69
|
+
if (payload.byteLength > limit) throw new Error(`dsh desktop: request pipe frame exceeds the ${String(limit)}-byte limit`);
|
|
70
|
+
const frame = Buffer.allocUnsafe(FRAME_HEADER_BYTES + payload.byteLength);
|
|
71
|
+
frame.writeUInt32BE(FRAME_MAGIC, 0);
|
|
72
|
+
frame.writeUInt8(type, 4);
|
|
73
|
+
frame.writeUInt32BE(streamId, 5);
|
|
74
|
+
frame.writeUInt32BE(payload.byteLength, 9);
|
|
75
|
+
payload.copy(frame, FRAME_HEADER_BYTES);
|
|
76
|
+
return frame;
|
|
77
|
+
}
|
|
78
|
+
function encodeJsonFrame(type, streamId, value) {
|
|
79
|
+
return encodeFrame(type, streamId, Buffer.from(JSON.stringify(value), "utf8"));
|
|
80
|
+
}
|
|
81
|
+
/** Encode the metadata opening one request stream. */
|
|
82
|
+
function encodeDesktopRequestStart(streamId, request) {
|
|
83
|
+
return encodeJsonFrame(REQUEST_FRAME_START, streamId, request);
|
|
84
|
+
}
|
|
85
|
+
/** Encode one bounded raw request-body chunk. */
|
|
86
|
+
function encodeDesktopRequestData(streamId, data) {
|
|
87
|
+
return encodeFrame(REQUEST_FRAME_DATA, streamId, Buffer.from(data));
|
|
88
|
+
}
|
|
89
|
+
/** Encode normal request-body completion. */
|
|
90
|
+
function encodeDesktopRequestEnd(streamId) {
|
|
91
|
+
return encodeFrame(REQUEST_FRAME_END, streamId, Buffer.alloc(0));
|
|
92
|
+
}
|
|
93
|
+
/** Encode cancellation of one request and its response. */
|
|
94
|
+
function encodeDesktopRequestCancel(streamId) {
|
|
95
|
+
return encodeFrame(REQUEST_FRAME_CANCEL, streamId, Buffer.alloc(0));
|
|
96
|
+
}
|
|
97
|
+
/** Incrementally decode validated response frames from the Host byte pipe. */
|
|
98
|
+
var DesktopHostResponseDecoder = class {
|
|
99
|
+
buffer = Buffer.alloc(0);
|
|
100
|
+
/**
|
|
101
|
+
* Append bytes and return every complete response frame.
|
|
102
|
+
* @param chunk - next bytes read from the Host response pipe.
|
|
103
|
+
* @returns complete frames in pipe order.
|
|
104
|
+
*/
|
|
105
|
+
push(chunk) {
|
|
106
|
+
this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
107
|
+
const frames = [];
|
|
108
|
+
for (;;) {
|
|
109
|
+
const frame = this.next();
|
|
110
|
+
if (frame === void 0) return frames;
|
|
111
|
+
frames.push(frame);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Reject EOF that splits a frame. */
|
|
115
|
+
finish() {
|
|
116
|
+
if (this.buffer.byteLength !== 0) throw new Error("dsh desktop: Host response pipe ended inside a frame");
|
|
117
|
+
}
|
|
118
|
+
next() {
|
|
119
|
+
if (this.buffer.byteLength < FRAME_HEADER_BYTES) return void 0;
|
|
120
|
+
if (this.buffer.readUInt32BE(0) !== FRAME_MAGIC) throw new Error("dsh desktop: invalid Host response frame marker");
|
|
121
|
+
const rawType = this.buffer.readUInt8(4);
|
|
122
|
+
const streamId = this.buffer.readUInt32BE(5);
|
|
123
|
+
const payloadLength = this.buffer.readUInt32BE(9);
|
|
124
|
+
assertStreamId(streamId);
|
|
125
|
+
const limit = rawType === RESPONSE_FRAME_DATA ? DESKTOP_PIPE_CHUNK_BYTES : MAX_CONTROL_PAYLOAD_BYTES;
|
|
126
|
+
if (payloadLength > limit) throw new Error(`dsh desktop: Host response frame exceeds the ${String(limit)}-byte limit`);
|
|
127
|
+
const frameLength = FRAME_HEADER_BYTES + payloadLength;
|
|
128
|
+
if (this.buffer.byteLength < frameLength) return void 0;
|
|
129
|
+
const payload = this.buffer.subarray(FRAME_HEADER_BYTES, frameLength);
|
|
130
|
+
this.buffer = this.buffer.subarray(frameLength);
|
|
131
|
+
switch (rawType) {
|
|
132
|
+
case RESPONSE_FRAME_START: return this.parseStart(streamId, payload);
|
|
133
|
+
case RESPONSE_FRAME_DATA: return {
|
|
134
|
+
type: "data",
|
|
135
|
+
streamId,
|
|
136
|
+
data: payload
|
|
137
|
+
};
|
|
138
|
+
case RESPONSE_FRAME_END:
|
|
139
|
+
if (payloadLength !== 0) throw new Error("dsh desktop: Host response end frame carried a payload");
|
|
140
|
+
return {
|
|
141
|
+
type: "end",
|
|
142
|
+
streamId
|
|
143
|
+
};
|
|
144
|
+
case RESPONSE_FRAME_ERROR: return this.parseError(streamId, payload);
|
|
145
|
+
default: throw new Error(`dsh desktop: unknown Host response frame type ${String(rawType)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
parseStart(streamId, payload) {
|
|
149
|
+
const value = this.parseJson(payload, "start");
|
|
150
|
+
if (!isRecord(value) || !Number.isInteger(value.status) || value.status < 100 || value.status > 599 || !isHeaders(value.headers) || typeof value.hasBody !== "boolean") throw new Error("dsh desktop: invalid Host response start payload");
|
|
151
|
+
return {
|
|
152
|
+
type: "start",
|
|
153
|
+
streamId,
|
|
154
|
+
status: value.status,
|
|
155
|
+
headers: value.headers,
|
|
156
|
+
hasBody: value.hasBody
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
parseError(streamId, payload) {
|
|
160
|
+
const value = this.parseJson(payload, "error");
|
|
161
|
+
if (!isRecord(value) || typeof value.message !== "string") throw new Error("dsh desktop: invalid Host response error payload");
|
|
162
|
+
return {
|
|
163
|
+
type: "error",
|
|
164
|
+
streamId,
|
|
165
|
+
message: value.message
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
parseJson(payload, subject) {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(payload.toString("utf8"));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new Error(`dsh desktop: Host response ${subject} payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/host-process.ts
|
|
178
|
+
/** Upstream-Node child lifecycle and streaming custom-protocol carrier. */
|
|
179
|
+
function isDesktopHostEvent(message) {
|
|
180
|
+
if (typeof message !== "object" || message === null || !("type" in message)) return false;
|
|
181
|
+
const candidate = message;
|
|
182
|
+
switch (candidate.type) {
|
|
183
|
+
case "ready": return candidate.protocolVersion === 3 && typeof candidate.dshVersion === "string";
|
|
184
|
+
case "fatal": return typeof candidate.message === "string";
|
|
185
|
+
default: return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function errorOf(reason, fallback) {
|
|
189
|
+
return reason instanceof Error ? reason : new Error(fallback);
|
|
190
|
+
}
|
|
191
|
+
async function exitsWithin(exit, milliseconds) {
|
|
192
|
+
let timer;
|
|
193
|
+
const timeout = new Promise((resolve) => {
|
|
194
|
+
timer = setTimeout(() => {
|
|
195
|
+
resolve(false);
|
|
196
|
+
}, milliseconds);
|
|
197
|
+
timer.unref();
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
return await Promise.race([exit.then(() => true), timeout]);
|
|
201
|
+
} finally {
|
|
202
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** One dsh backend running under the bundled upstream Node.js executable. */
|
|
206
|
+
var DesktopHostProcess = class {
|
|
207
|
+
node;
|
|
208
|
+
projectDir;
|
|
209
|
+
inspectPort;
|
|
210
|
+
options;
|
|
211
|
+
child;
|
|
212
|
+
requestPipe;
|
|
213
|
+
responsePipe;
|
|
214
|
+
responseDecoder = new DesktopHostResponseDecoder();
|
|
215
|
+
requestWriteTail = Promise.resolve();
|
|
216
|
+
nextStreamId = 1;
|
|
217
|
+
pending = /* @__PURE__ */ new Map();
|
|
218
|
+
blockedResponses = /* @__PURE__ */ new Set();
|
|
219
|
+
readyResolve;
|
|
220
|
+
readyReject;
|
|
221
|
+
readyPromise = new Promise((resolve, reject) => {
|
|
222
|
+
this.readyResolve = resolve;
|
|
223
|
+
this.readyReject = reject;
|
|
224
|
+
});
|
|
225
|
+
exitPromise;
|
|
226
|
+
stderr = "";
|
|
227
|
+
/**
|
|
228
|
+
* @param node - absolute bundled upstream Node.js executable.
|
|
229
|
+
* @param projectDir - active or staged desktop npm project.
|
|
230
|
+
* @param inspectPort - optional loopback inspector port for workspace development.
|
|
231
|
+
* @param options - launch customization (node args, environment, shell spawn).
|
|
232
|
+
*/
|
|
233
|
+
constructor(node, projectDir, inspectPort, options = {}) {
|
|
234
|
+
this.node = node;
|
|
235
|
+
this.projectDir = projectDir;
|
|
236
|
+
this.inspectPort = inspectPort;
|
|
237
|
+
this.options = options;
|
|
238
|
+
}
|
|
239
|
+
/** Start the child once and resolve only after its complete composition is active. */
|
|
240
|
+
async start() {
|
|
241
|
+
if (this.child !== void 0) return this.readyPromise;
|
|
242
|
+
const entry = join(this.projectDir, "node_modules", "@deepseek-ai", "dsh-desktop-host", "lib", "index.js");
|
|
243
|
+
const allowLinked = this.options.allowLinkedProfile === true || this.inspectPort !== void 0;
|
|
244
|
+
const args = [
|
|
245
|
+
...this.inspectPort === void 0 ? [] : [`--inspect=127.0.0.1:${String(this.inspectPort)}`],
|
|
246
|
+
...this.options.nodeArgs ?? [],
|
|
247
|
+
entry,
|
|
248
|
+
this.projectDir,
|
|
249
|
+
...allowLinked ? ["--allow-linked-profile"] : []
|
|
250
|
+
];
|
|
251
|
+
const env = {
|
|
252
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => name !== "NODE_OPTIONS" && !name.startsWith("DSH_DESKTOP_") && !/^(?:npm|pnpm|corepack)_/iu.test(name))),
|
|
253
|
+
...this.options.extraEnv
|
|
254
|
+
};
|
|
255
|
+
const child = (this.options.spawn ?? spawn)(this.node, args, {
|
|
256
|
+
cwd: this.projectDir,
|
|
257
|
+
env,
|
|
258
|
+
stdio: [
|
|
259
|
+
"ignore",
|
|
260
|
+
"pipe",
|
|
261
|
+
"pipe",
|
|
262
|
+
"pipe",
|
|
263
|
+
"pipe",
|
|
264
|
+
"ipc"
|
|
265
|
+
]
|
|
266
|
+
});
|
|
267
|
+
const requestPipe = child.stdio[3];
|
|
268
|
+
const responsePipe = child.stdio[4];
|
|
269
|
+
if (!(requestPipe instanceof Writable) || !(responsePipe instanceof Readable)) {
|
|
270
|
+
this.killChild("SIGTERM");
|
|
271
|
+
throw new Error("dsh desktop host did not expose the required byte pipes and IPC channel");
|
|
272
|
+
}
|
|
273
|
+
this.child = child;
|
|
274
|
+
this.requestPipe = requestPipe;
|
|
275
|
+
this.responsePipe = responsePipe;
|
|
276
|
+
child.stderr?.setEncoding("utf8");
|
|
277
|
+
child.stderr?.on("data", (chunk) => {
|
|
278
|
+
this.stderr += chunk;
|
|
279
|
+
process.stderr.write(`[dsh-host] ${chunk}`);
|
|
280
|
+
});
|
|
281
|
+
child.stdout?.pipe(process.stdout);
|
|
282
|
+
responsePipe.on("data", (chunk) => {
|
|
283
|
+
this.acceptResponseBytes(chunk);
|
|
284
|
+
});
|
|
285
|
+
responsePipe.once("end", () => {
|
|
286
|
+
try {
|
|
287
|
+
this.responseDecoder.finish();
|
|
288
|
+
this.fail(/* @__PURE__ */ new Error("dsh desktop host response pipe ended"));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
this.fail(errorOf(error, "dsh desktop host response pipe failed"));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
requestPipe.once("error", (error) => {
|
|
294
|
+
this.fail(error);
|
|
295
|
+
});
|
|
296
|
+
responsePipe.once("error", (error) => {
|
|
297
|
+
this.fail(error);
|
|
298
|
+
});
|
|
299
|
+
child.on("message", (message) => {
|
|
300
|
+
if (!isDesktopHostEvent(message)) {
|
|
301
|
+
this.fail(/* @__PURE__ */ new Error("dsh desktop host sent an invalid IPC event"));
|
|
302
|
+
this.killChild("SIGTERM");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.handleMessage(message);
|
|
306
|
+
});
|
|
307
|
+
child.once("error", (error) => {
|
|
308
|
+
this.fail(error);
|
|
309
|
+
});
|
|
310
|
+
this.exitPromise = new Promise((resolve) => {
|
|
311
|
+
child.once("exit", (code) => {
|
|
312
|
+
const suffix = this.stderr.trim() === "" ? "" : `: ${this.stderr.trim()}`;
|
|
313
|
+
if (code !== 0 && code !== null) this.fail(/* @__PURE__ */ new Error(`dsh desktop host exited with ${String(code)}${suffix}`));
|
|
314
|
+
else this.fail(/* @__PURE__ */ new Error(`dsh desktop host stopped${suffix}`));
|
|
315
|
+
resolve();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
return this.readyPromise;
|
|
319
|
+
}
|
|
320
|
+
/** Forward one `dsh-app://app` request to the child without buffering its body. */
|
|
321
|
+
async fetch(request) {
|
|
322
|
+
await this.start();
|
|
323
|
+
const child = this.child;
|
|
324
|
+
if (child === void 0 || !child.connected || this.requestPipe === void 0) throw new Error("dsh desktop host is unavailable");
|
|
325
|
+
if (this.nextStreamId > 4294967295) throw new Error("dsh desktop host exhausted its request stream ids");
|
|
326
|
+
const streamId = this.nextStreamId++;
|
|
327
|
+
const method = request.method.toUpperCase();
|
|
328
|
+
const hasBody = method !== "GET" && method !== "HEAD" && request.body !== null;
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
const pending = {
|
|
331
|
+
resolve,
|
|
332
|
+
reject,
|
|
333
|
+
responseStarted: false,
|
|
334
|
+
uploadOpen: hasBody
|
|
335
|
+
};
|
|
336
|
+
const abort = () => {
|
|
337
|
+
if (!this.pending.has(streamId)) return;
|
|
338
|
+
const error = errorOf(request.signal.reason, "request aborted");
|
|
339
|
+
pending.uploadOpen = false;
|
|
340
|
+
pending.requestReader?.cancel(error).catch(() => void 0);
|
|
341
|
+
this.enqueueRequestFrame(encodeDesktopRequestCancel(streamId)).catch((pipeError) => {
|
|
342
|
+
this.fail(errorOf(pipeError, "dsh desktop request pipe failed"));
|
|
343
|
+
});
|
|
344
|
+
if (pending.controller === void 0) pending.reject(error);
|
|
345
|
+
else pending.controller.error(error);
|
|
346
|
+
this.finishPending(streamId, false);
|
|
347
|
+
};
|
|
348
|
+
if (request.signal.aborted) {
|
|
349
|
+
reject(errorOf(request.signal.reason, "request aborted"));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
353
|
+
pending.removeAbort = () => {
|
|
354
|
+
request.signal.removeEventListener("abort", abort);
|
|
355
|
+
};
|
|
356
|
+
this.pending.set(streamId, pending);
|
|
357
|
+
this.pumpRequest(streamId, request, hasBody).catch((error) => {
|
|
358
|
+
this.failPending(streamId, errorOf(error, "dsh desktop request upload failed"));
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** Request graceful teardown, then wait for child exit. */
|
|
363
|
+
async stop() {
|
|
364
|
+
const child = this.child;
|
|
365
|
+
if (child === void 0) return;
|
|
366
|
+
this.blockedResponses.clear();
|
|
367
|
+
this.responsePipe?.resume();
|
|
368
|
+
if (child.connected) this.send({ type: "shutdown" });
|
|
369
|
+
this.requestPipe?.destroy();
|
|
370
|
+
const exited = this.exitPromise ?? Promise.resolve();
|
|
371
|
+
if (!await exitsWithin(exited, 1e4)) this.killChild("SIGTERM");
|
|
372
|
+
if (!await exitsWithin(exited, 5e3)) {
|
|
373
|
+
this.killChild("SIGKILL");
|
|
374
|
+
if (!await exitsWithin(exited, 5e3)) throw new Error("dsh desktop host did not exit after SIGKILL");
|
|
375
|
+
}
|
|
376
|
+
this.child = void 0;
|
|
377
|
+
this.requestPipe = void 0;
|
|
378
|
+
this.responsePipe = void 0;
|
|
379
|
+
}
|
|
380
|
+
/** Signal the child, covering its whole process group when it was detached. */
|
|
381
|
+
killChild(signal) {
|
|
382
|
+
const child = this.child;
|
|
383
|
+
if (child === void 0 || child.pid === void 0) return;
|
|
384
|
+
if (process.platform !== "win32") try {
|
|
385
|
+
process.kill(-child.pid, signal);
|
|
386
|
+
return;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (error.code !== "ESRCH") throw error;
|
|
389
|
+
}
|
|
390
|
+
child.kill(signal);
|
|
391
|
+
}
|
|
392
|
+
async pumpRequest(streamId, request, hasBody) {
|
|
393
|
+
await this.enqueueRequestFrame(encodeDesktopRequestStart(streamId, {
|
|
394
|
+
url: request.url,
|
|
395
|
+
method: request.method.toUpperCase(),
|
|
396
|
+
headers: [...request.headers.entries()],
|
|
397
|
+
hasBody
|
|
398
|
+
}));
|
|
399
|
+
if (!hasBody) return;
|
|
400
|
+
const body = request.body;
|
|
401
|
+
if (body === null) throw new Error("dsh desktop request body disappeared before upload");
|
|
402
|
+
const reader = body.getReader();
|
|
403
|
+
const pending = this.pending.get(streamId);
|
|
404
|
+
if (pending === void 0) {
|
|
405
|
+
await reader.cancel();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
pending.requestReader = reader;
|
|
409
|
+
try {
|
|
410
|
+
for (;;) {
|
|
411
|
+
const next = await reader.read();
|
|
412
|
+
if (next.done) break;
|
|
413
|
+
for (let offset = 0; offset < next.value.byteLength; offset += DESKTOP_PIPE_CHUNK_BYTES) {
|
|
414
|
+
if (!this.pending.has(streamId)) return;
|
|
415
|
+
await this.enqueueRequestFrame(encodeDesktopRequestData(streamId, next.value.subarray(offset, offset + DESKTOP_PIPE_CHUNK_BYTES)));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const live = this.pending.get(streamId);
|
|
419
|
+
if (live !== void 0) {
|
|
420
|
+
await this.enqueueRequestFrame(encodeDesktopRequestEnd(streamId));
|
|
421
|
+
live.uploadOpen = false;
|
|
422
|
+
}
|
|
423
|
+
} finally {
|
|
424
|
+
reader.releaseLock();
|
|
425
|
+
const live = this.pending.get(streamId);
|
|
426
|
+
if (live?.requestReader === reader) delete live.requestReader;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
enqueueRequestFrame(frame) {
|
|
430
|
+
const write = this.requestWriteTail.then(async () => {
|
|
431
|
+
const pipe = this.requestPipe;
|
|
432
|
+
if (pipe === void 0 || pipe.destroyed) throw new Error("dsh desktop host request pipe is unavailable");
|
|
433
|
+
if (!pipe.write(frame)) await once(pipe, "drain");
|
|
434
|
+
});
|
|
435
|
+
this.requestWriteTail = write.catch(() => void 0);
|
|
436
|
+
return write;
|
|
437
|
+
}
|
|
438
|
+
send(message) {
|
|
439
|
+
const child = this.child;
|
|
440
|
+
if (child === void 0 || !child.connected) throw new Error("dsh desktop host IPC is unavailable");
|
|
441
|
+
child.send(message);
|
|
442
|
+
}
|
|
443
|
+
acceptResponseBytes(chunk) {
|
|
444
|
+
try {
|
|
445
|
+
for (const frame of this.responseDecoder.push(chunk)) this.handleResponseFrame(frame);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.fail(errorOf(error, "dsh desktop host response pipe failed"));
|
|
448
|
+
this.child?.kill("SIGTERM");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
handleResponseFrame(frame) {
|
|
452
|
+
const pending = this.pending.get(frame.streamId);
|
|
453
|
+
if (pending === void 0) {
|
|
454
|
+
if (frame.streamId >= this.nextStreamId) throw new Error(`dsh desktop host responded for unknown stream ${String(frame.streamId)}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
switch (frame.type) {
|
|
458
|
+
case "start": {
|
|
459
|
+
if (pending.responseStarted) throw new Error(`dsh desktop host started stream ${String(frame.streamId)} twice`);
|
|
460
|
+
pending.responseStarted = true;
|
|
461
|
+
let body = null;
|
|
462
|
+
if (frame.hasBody) body = new ReadableStream({
|
|
463
|
+
start: (controller) => {
|
|
464
|
+
pending.controller = controller;
|
|
465
|
+
},
|
|
466
|
+
pull: () => {
|
|
467
|
+
this.blockedResponses.delete(frame.streamId);
|
|
468
|
+
this.resumeResponsePipe();
|
|
469
|
+
},
|
|
470
|
+
cancel: (reason) => {
|
|
471
|
+
this.cancelResponse(frame.streamId, reason);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
pending.resolve(new Response(body, {
|
|
475
|
+
status: frame.status,
|
|
476
|
+
headers: new Headers(frame.headers.map(([name, value]) => [name, value]))
|
|
477
|
+
}));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
case "data": {
|
|
481
|
+
const controller = pending.controller;
|
|
482
|
+
if (!pending.responseStarted || controller === void 0) throw new Error(`dsh desktop host sent body data before a body start for stream ${String(frame.streamId)}`);
|
|
483
|
+
controller.enqueue(frame.data);
|
|
484
|
+
if ((controller.desiredSize ?? 0) <= 0) {
|
|
485
|
+
this.blockedResponses.add(frame.streamId);
|
|
486
|
+
this.responsePipe?.pause();
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
case "end":
|
|
491
|
+
if (!pending.responseStarted) throw new Error(`dsh desktop host ended stream ${String(frame.streamId)} before its response start`);
|
|
492
|
+
pending.controller?.close();
|
|
493
|
+
this.finishPending(frame.streamId, true);
|
|
494
|
+
return;
|
|
495
|
+
case "error":
|
|
496
|
+
this.failPending(frame.streamId, new Error(frame.message));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
cancelResponse(streamId, reason) {
|
|
501
|
+
const pending = this.pending.get(streamId);
|
|
502
|
+
if (pending === void 0) return;
|
|
503
|
+
pending.uploadOpen = false;
|
|
504
|
+
pending.requestReader?.cancel(reason).catch(() => void 0);
|
|
505
|
+
this.enqueueRequestFrame(encodeDesktopRequestCancel(streamId)).catch((error) => {
|
|
506
|
+
this.fail(errorOf(error, "dsh desktop request pipe failed"));
|
|
507
|
+
});
|
|
508
|
+
this.finishPending(streamId, false);
|
|
509
|
+
}
|
|
510
|
+
failPending(streamId, error) {
|
|
511
|
+
const pending = this.pending.get(streamId);
|
|
512
|
+
if (pending === void 0) return;
|
|
513
|
+
pending.uploadOpen = false;
|
|
514
|
+
pending.requestReader?.cancel(error).catch(() => void 0);
|
|
515
|
+
if (pending.controller === void 0) pending.reject(error);
|
|
516
|
+
else pending.controller.error(error);
|
|
517
|
+
this.enqueueRequestFrame(encodeDesktopRequestCancel(streamId)).catch((pipeError) => {
|
|
518
|
+
this.fail(errorOf(pipeError, "dsh desktop request pipe failed"));
|
|
519
|
+
});
|
|
520
|
+
this.finishPending(streamId, false);
|
|
521
|
+
}
|
|
522
|
+
finishPending(streamId, cancelOpenUpload) {
|
|
523
|
+
const pending = this.pending.get(streamId);
|
|
524
|
+
if (pending === void 0) return;
|
|
525
|
+
if (cancelOpenUpload && pending.uploadOpen) {
|
|
526
|
+
pending.uploadOpen = false;
|
|
527
|
+
pending.requestReader?.cancel().catch(() => void 0);
|
|
528
|
+
this.enqueueRequestFrame(encodeDesktopRequestCancel(streamId)).catch((error) => {
|
|
529
|
+
this.fail(errorOf(error, "dsh desktop request pipe failed"));
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
pending.removeAbort?.();
|
|
533
|
+
this.pending.delete(streamId);
|
|
534
|
+
this.blockedResponses.delete(streamId);
|
|
535
|
+
this.resumeResponsePipe();
|
|
536
|
+
}
|
|
537
|
+
resumeResponsePipe() {
|
|
538
|
+
if (this.blockedResponses.size === 0) this.responsePipe?.resume();
|
|
539
|
+
}
|
|
540
|
+
handleMessage(message) {
|
|
541
|
+
switch (message.type) {
|
|
542
|
+
case "ready":
|
|
543
|
+
this.readyResolve(message);
|
|
544
|
+
return;
|
|
545
|
+
case "fatal":
|
|
546
|
+
this.fail(new Error(message.message));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
fail(error) {
|
|
551
|
+
this.readyReject(error);
|
|
552
|
+
for (const pending of this.pending.values()) {
|
|
553
|
+
pending.requestReader?.cancel(error).catch(() => void 0);
|
|
554
|
+
if (pending.controller === void 0) pending.reject(error);
|
|
555
|
+
else pending.controller.error(error);
|
|
556
|
+
pending.removeAbort?.();
|
|
557
|
+
}
|
|
558
|
+
this.pending.clear();
|
|
559
|
+
this.blockedResponses.clear();
|
|
560
|
+
this.responsePipe?.resume();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/shell-env.ts
|
|
565
|
+
/**
|
|
566
|
+
* Unix backend launch with user-shell environment injection: when the user's
|
|
567
|
+
* `$SHELL` has a matching rc file (`~/.bashrc` for bash, `~/.zshrc` for zsh),
|
|
568
|
+
* the backend command is spawned through `$SHELL -c 'source <rc> >/dev/null
|
|
569
|
+
* 2>&1; exec <command> <args...>'` so the backend inherits the user's terminal
|
|
570
|
+
* environment (API keys and other exports). `exec` keeps the same process, so
|
|
571
|
+
* the supervisor's wait and process-group semantics are unaffected; the source
|
|
572
|
+
* output is discarded so it never pollutes the backend's stdout. The child is
|
|
573
|
+
* detached into its own process group so the shell can terminate the whole
|
|
574
|
+
* backend tree on exit. Windows spawns directly.
|
|
575
|
+
* @module @morlay/dsh-desktopify
|
|
576
|
+
*/
|
|
577
|
+
/** rc file to source for one shell basename; empty means no injection. */
|
|
578
|
+
function rcFileFor(shell) {
|
|
579
|
+
switch (basename(shell)) {
|
|
580
|
+
case "bash": return "~/.bashrc";
|
|
581
|
+
case "zsh": return "~/.zshrc";
|
|
582
|
+
default: return "";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Single-quote a string for shell command lines (paths may contain spaces). */
|
|
586
|
+
function shellQuote(value) {
|
|
587
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Spawn a backend command through the user shell with rc sourcing on Unix,
|
|
591
|
+
* directly otherwise. The child joins a new process group (detached) so the
|
|
592
|
+
* shell can terminate the whole backend tree on exit.
|
|
593
|
+
* @param command - backend executable (bundled node or the Electron binary in node mode).
|
|
594
|
+
* @param args - backend arguments.
|
|
595
|
+
* @param options - spawn options (env, stdio pipes, cwd).
|
|
596
|
+
* @returns the spawned child.
|
|
597
|
+
*/
|
|
598
|
+
function shellWrappedSpawn(command, args, options) {
|
|
599
|
+
const shell = process.env.SHELL;
|
|
600
|
+
const rc = shell === void 0 || shell === "" ? "" : rcFileFor(shell);
|
|
601
|
+
if (rc !== "" && process.platform !== "win32" && shell !== void 0) {
|
|
602
|
+
const commandLine = `source ${rc} >/dev/null 2>&1; exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")}`;
|
|
603
|
+
return spawn(shell, ["-c", commandLine], {
|
|
604
|
+
...options,
|
|
605
|
+
detached: true
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return spawn(command, [...args], {
|
|
609
|
+
...options,
|
|
610
|
+
detached: process.platform !== "win32"
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/index.ts
|
|
615
|
+
/**
|
|
616
|
+
* Electron shell: window, `dsh-app://` custom protocol, and the dsh backend
|
|
617
|
+
* child lifecycle. The backend is the upstream `@deepseek-ai/dsh-desktop-host`
|
|
618
|
+
* entry run under a bundled Node.js executable (packaged) or the current
|
|
619
|
+
* Electron executable in node mode (development); requests from the renderer
|
|
620
|
+
* are forwarded over framed byte pipes. On Unix the backend inherits the
|
|
621
|
+
* user's shell environment through rc sourcing (see shell-env.ts).
|
|
622
|
+
* @module @morlay/dsh-desktopify
|
|
623
|
+
*/
|
|
624
|
+
const SCHEME = "dsh-app";
|
|
625
|
+
let focusPrimaryWindow = () => {};
|
|
626
|
+
protocol.registerSchemesAsPrivileged([{
|
|
627
|
+
scheme: SCHEME,
|
|
628
|
+
privileges: {
|
|
629
|
+
standard: true,
|
|
630
|
+
secure: true,
|
|
631
|
+
supportFetchAPI: true,
|
|
632
|
+
corsEnabled: false,
|
|
633
|
+
stream: true,
|
|
634
|
+
codeCache: true
|
|
635
|
+
}
|
|
636
|
+
}]);
|
|
637
|
+
const MIME = {
|
|
638
|
+
".css": "text/css; charset=utf-8",
|
|
639
|
+
".html": "text/html; charset=utf-8",
|
|
640
|
+
".js": "text/javascript; charset=utf-8",
|
|
641
|
+
".svg": "image/svg+xml"
|
|
642
|
+
};
|
|
643
|
+
function runtimeResources() {
|
|
644
|
+
const development = !app.isPackaged;
|
|
645
|
+
return {
|
|
646
|
+
node: (development ? process.env.DSH_DESKTOP_NODE_BINARY : void 0) ?? join(process.resourcesPath, "runtime", "node", process.platform === "win32" ? "node.exe" : "node"),
|
|
647
|
+
seed: (development ? process.env.DSH_DESKTOP_SEED_DIR : void 0) ?? join(process.resourcesPath, "seed")
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function developmentProject() {
|
|
651
|
+
const configured = process.env.DSH_DESKTOP_DEV_PROJECT_DIR;
|
|
652
|
+
if (configured === void 0 || configured === "") return void 0;
|
|
653
|
+
if (app.isPackaged) throw new Error("dsh desktop: development project override is unavailable in packaged applications");
|
|
654
|
+
return resolve(configured);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* 无标题栏窗口框架:macOS 隐藏标题栏(保留交通灯,顶部仍可拖动);Windows
|
|
658
|
+
* 隐藏标题栏并用 `titleBarOverlay` 保留系统窗口控制按钮(透明底、中性符号
|
|
659
|
+
* 色,避免与页面内容冲突);其余平台去掉系统边框。
|
|
660
|
+
*/
|
|
661
|
+
function windowFrame() {
|
|
662
|
+
if (process.platform === "darwin") return { titleBarStyle: "hidden" };
|
|
663
|
+
if (process.platform === "win32") return {
|
|
664
|
+
titleBarStyle: "hidden",
|
|
665
|
+
titleBarOverlay: {
|
|
666
|
+
color: "#00000000",
|
|
667
|
+
symbolColor: "#888888"
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
return { frame: false };
|
|
671
|
+
}
|
|
672
|
+
function createWindow(preload, config) {
|
|
673
|
+
const window = new BrowserWindow({
|
|
674
|
+
width: config.width,
|
|
675
|
+
height: config.height,
|
|
676
|
+
minWidth: config.minWidth,
|
|
677
|
+
minHeight: config.minHeight,
|
|
678
|
+
show: false,
|
|
679
|
+
...windowFrame(),
|
|
680
|
+
webPreferences: {
|
|
681
|
+
preload,
|
|
682
|
+
nodeIntegration: false,
|
|
683
|
+
contextIsolation: true,
|
|
684
|
+
sandbox: true,
|
|
685
|
+
webSecurity: true
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
689
|
+
window.webContents.on("will-navigate", (event, url) => {
|
|
690
|
+
if (new URL(url).protocol !== `${SCHEME}:`) event.preventDefault();
|
|
691
|
+
});
|
|
692
|
+
return window;
|
|
693
|
+
}
|
|
694
|
+
async function serveShellAsset(request) {
|
|
695
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response(null, { status: 405 });
|
|
696
|
+
const root = resolve(app.getAppPath(), "renderer");
|
|
697
|
+
const url = new URL(request.url);
|
|
698
|
+
let pathname;
|
|
699
|
+
try {
|
|
700
|
+
pathname = decodeURIComponent(url.pathname);
|
|
701
|
+
} catch {
|
|
702
|
+
return new Response(null, { status: 400 });
|
|
703
|
+
}
|
|
704
|
+
const target = resolve(normalize(join(root, pathname)));
|
|
705
|
+
if (target !== root && !target.startsWith(root + sep)) return new Response(null, { status: 403 });
|
|
706
|
+
try {
|
|
707
|
+
const body = request.method === "HEAD" ? null : await readFile(target);
|
|
708
|
+
return new Response(body, { headers: { "content-type": MIME[extname(target)] ?? "application/octet-stream" } });
|
|
709
|
+
} catch {
|
|
710
|
+
return new Response(null, { status: 404 });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function developmentHostInspectPort(enabled) {
|
|
714
|
+
const configured = process.env.DSH_DESKTOP_HOST_INSPECT_PORT;
|
|
715
|
+
if (!enabled || configured === void 0 || configured === "") return void 0;
|
|
716
|
+
const port = Number(configured);
|
|
717
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error("dsh desktop: DSH_DESKTOP_HOST_INSPECT_PORT must be an integer from 1 through 65535");
|
|
718
|
+
return port;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Development host Node arguments. The launcher sets `DSH_DESKTOP_TSX_IMPORT`
|
|
722
|
+
* only when the workspace actually has tsx installed, so a workspace without
|
|
723
|
+
* it never receives a loader it cannot resolve.
|
|
724
|
+
*/
|
|
725
|
+
function developmentNodeArgs() {
|
|
726
|
+
const specifier = process.env.DSH_DESKTOP_TSX_IMPORT;
|
|
727
|
+
return specifier === void 0 || specifier === "" ? [] : [`--import=${specifier}`];
|
|
728
|
+
}
|
|
729
|
+
async function main() {
|
|
730
|
+
const development = developmentProject();
|
|
731
|
+
const config = development === void 0 ? loadAppConfig(process.resourcesPath) : loadAppConfig(process.env.DSH_DESKTOP_APPCONFIG_DIR ?? process.resourcesPath);
|
|
732
|
+
const resources = runtimeResources();
|
|
733
|
+
const hostInspectPort = developmentHostInspectPort(development !== void 0);
|
|
734
|
+
const activeProject = development ?? join(resolveDshHome(config) ?? "", "profiles", "desktop");
|
|
735
|
+
const dshHome = resolveDshHome(config);
|
|
736
|
+
if (development === void 0) {
|
|
737
|
+
if (dshHome === void 0) throw new Error("dsh desktop: packaged applications require a concrete dshHome");
|
|
738
|
+
ensureSeedProfile(resources.seed, dshHome);
|
|
739
|
+
}
|
|
740
|
+
let host;
|
|
741
|
+
let mainWindow;
|
|
742
|
+
let quitConfirmed = false;
|
|
743
|
+
let quitPrompting = false;
|
|
744
|
+
const appPreload = fileURLToPath(new URL("./preload-app.cjs", import.meta.url));
|
|
745
|
+
/**
|
|
746
|
+
* 退出确认:红叉 / Cmd+Q / 菜单退出都先问一次,避免误关把后台 host 与
|
|
747
|
+
* 运行中的会话一起强杀。确认后置 quitConfirmed 并重新走退出流程。
|
|
748
|
+
*/
|
|
749
|
+
const confirmQuit = async (window) => {
|
|
750
|
+
if (quitPrompting) return;
|
|
751
|
+
quitPrompting = true;
|
|
752
|
+
try {
|
|
753
|
+
const options = {
|
|
754
|
+
type: "question",
|
|
755
|
+
buttons: ["Cancel", "Quit"],
|
|
756
|
+
defaultId: 0,
|
|
757
|
+
cancelId: 0,
|
|
758
|
+
noLink: true,
|
|
759
|
+
message: `Quit ${config.name}?`,
|
|
760
|
+
detail: "The desktop backend and its running sessions will stop."
|
|
761
|
+
};
|
|
762
|
+
const parent = window ?? mainWindow;
|
|
763
|
+
const { response } = parent === void 0 || parent.isDestroyed() ? await dialog.showMessageBox(options) : await dialog.showMessageBox(parent, options);
|
|
764
|
+
if (response !== 1) return;
|
|
765
|
+
quitConfirmed = true;
|
|
766
|
+
app.quit();
|
|
767
|
+
} finally {
|
|
768
|
+
quitPrompting = false;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
const startHost = async (projectDir = activeProject) => {
|
|
772
|
+
const next = new DesktopHostProcess(resources.node, projectDir, hostInspectPort, {
|
|
773
|
+
nodeArgs: development === void 0 ? [] : developmentNodeArgs(),
|
|
774
|
+
extraEnv: {
|
|
775
|
+
...dshHome === void 0 ? {} : { DSH_HOME: dshHome },
|
|
776
|
+
...development === void 0 ? {} : { ELECTRON_RUN_AS_NODE: "1" }
|
|
777
|
+
},
|
|
778
|
+
spawn: shellWrappedSpawn
|
|
779
|
+
});
|
|
780
|
+
await next.start();
|
|
781
|
+
return next;
|
|
782
|
+
};
|
|
783
|
+
protocol.handle(SCHEME, (request) => {
|
|
784
|
+
const url = new URL(request.url);
|
|
785
|
+
if (url.hostname === "shell") return serveShellAsset(request);
|
|
786
|
+
if (url.hostname !== "app") return Promise.resolve(new Response(null, { status: 404 }));
|
|
787
|
+
const active = host;
|
|
788
|
+
if (active === void 0) return Promise.resolve(new Response("backend unavailable", { status: 503 }));
|
|
789
|
+
return active.fetch(request);
|
|
790
|
+
});
|
|
791
|
+
const createMainWindow = () => {
|
|
792
|
+
const window = createWindow(appPreload, config.window);
|
|
793
|
+
mainWindow = window;
|
|
794
|
+
window.once("ready-to-show", () => {
|
|
795
|
+
if (!window.isDestroyed()) window.show();
|
|
796
|
+
});
|
|
797
|
+
window.on("close", (event) => {
|
|
798
|
+
if (quitConfirmed) return;
|
|
799
|
+
event.preventDefault();
|
|
800
|
+
confirmQuit(window);
|
|
801
|
+
});
|
|
802
|
+
window.on("closed", () => {
|
|
803
|
+
if (mainWindow === window) mainWindow = void 0;
|
|
804
|
+
});
|
|
805
|
+
return window;
|
|
806
|
+
};
|
|
807
|
+
focusPrimaryWindow = () => {
|
|
808
|
+
const window = mainWindow;
|
|
809
|
+
if (window === void 0 || window.isDestroyed()) {
|
|
810
|
+
createMainWindow().loadURL(`${SCHEME}://app/index.html`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (window.isMinimized()) window.restore();
|
|
814
|
+
window.show();
|
|
815
|
+
window.focus();
|
|
816
|
+
};
|
|
817
|
+
host = await startHost();
|
|
818
|
+
mainWindow = createMainWindow();
|
|
819
|
+
await mainWindow.loadURL(`${SCHEME}://app/index.html`);
|
|
820
|
+
if (development !== void 0 && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== "0") mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
821
|
+
app.on("activate", () => {
|
|
822
|
+
if (BrowserWindow.getAllWindows().length === 0) focusPrimaryWindow();
|
|
823
|
+
});
|
|
824
|
+
app.on("window-all-closed", () => {
|
|
825
|
+
app.quit();
|
|
826
|
+
});
|
|
827
|
+
app.on("before-quit", (event) => {
|
|
828
|
+
if (!quitConfirmed) {
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
confirmQuit();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (host === void 0) return;
|
|
834
|
+
event.preventDefault();
|
|
835
|
+
const active = host;
|
|
836
|
+
host = void 0;
|
|
837
|
+
active.stop().finally(() => {
|
|
838
|
+
app.quit();
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if ((() => {
|
|
843
|
+
if (!app.requestSingleInstanceLock()) {
|
|
844
|
+
app.quit();
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
app.on("second-instance", () => {
|
|
848
|
+
focusPrimaryWindow();
|
|
849
|
+
});
|
|
850
|
+
return true;
|
|
851
|
+
})()) app.whenReady().then(main).catch(async (error) => {
|
|
852
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
853
|
+
console.error(error);
|
|
854
|
+
const diagnosticFile = process.env.DSH_DESKTOP_DIAGNOSTIC_FILE;
|
|
855
|
+
if (diagnosticFile !== void 0) await import("node:fs/promises").then((fs) => fs.writeFile(diagnosticFile, `${error instanceof Error ? error.stack ?? message : message}\n`)).catch(() => void 0);
|
|
856
|
+
dialog.showErrorBox("dsh desktop startup failed", message);
|
|
857
|
+
app.exit(1);
|
|
858
|
+
});
|
|
859
|
+
//#endregion
|
|
860
|
+
export {};
|