@morlay/dsh-desktopify 0.1.0 → 0.1.1
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/README.md
CHANGED
|
@@ -37,15 +37,18 @@
|
|
|
37
37
|
`src/cli/electron-builder.ts` 内配置(无独立配置文件)。壳以最小 app
|
|
38
38
|
目录(`dist/` + 入口 manifest,版本取工作区版本)打包,工具自身的构建
|
|
39
39
|
依赖不进 app;`--dir` 产出未打包的应用目录,不签名、不 notarize。
|
|
40
|
-
-
|
|
41
|
-
|
|
40
|
+
- **后端**:复用上游已构建的 desktop-host 产物(字节管道协议),不重复实现
|
|
41
|
+
boot 逻辑。上游 `@deepseek-ai/dsh-desktop-host` 是 `private` 包、不发布,
|
|
42
|
+
所以 `tsdown.config.ts` 构建时把它的 `lib/index.js` +
|
|
43
|
+
`config/desktop.cordis.patch.yml` + manifest `copy` 进 `dist/desktop-host`,
|
|
44
|
+
运行时从这里装配;`package.json` 不再依赖该私有包,可正常发布。
|
|
42
45
|
- **官方依赖内部维护**:`@deepseek-ai/*` 依赖清单(dsh、dsh-desktop-host、
|
|
43
46
|
cordis-plugin-group 及约 20 个 peer 包)由工具内部维护
|
|
44
47
|
(`src/official.ts`),app 只声明自己的依赖、`dsh.version` 与 bundles。
|
|
45
48
|
官方依赖 spec 由工具按 `dsh.version` + 已安装包解析
|
|
46
49
|
(`src/cli/official-deps.ts`):app 工作区优先,工具自身安装兜底,不写死
|
|
47
|
-
`workspace
|
|
48
|
-
|
|
50
|
+
`workspace:`,仓库外的独立项目同样可装配;desktop-host 用工具自带的产物
|
|
51
|
+
(同一 workspace 用 `link:`,独立项目暂存后 `file:`)。
|
|
49
52
|
- **bundles 自动合并**:官方 bundles(`@deepseek-ai/dsh-base`、
|
|
50
53
|
`@deepseek-ai/dsh-web-app`)+ app 的 `dsh.profile.bundles` 自动合并进 dev
|
|
51
54
|
项目与种子 profile。
|
|
@@ -118,5 +121,7 @@
|
|
|
118
121
|
|
|
119
122
|
- pnpm workspace(dev 依赖 `findWorkspaceRoot` 装配临时项目;bundle 依赖
|
|
120
123
|
`pnpm deploy`)。
|
|
121
|
-
- `vendor/deepseek-harness`
|
|
122
|
-
`lib/`
|
|
124
|
+
- `vendor/deepseek-harness` 已构建(`just vendor prepare`:dev 需要 dsh CLI,
|
|
125
|
+
工具构建需要 desktop-host 的 `lib/` 产物来 stage 进 `dist/desktop-host`)。
|
|
126
|
+
- 工具自身已构建(`pnpm build`):dev / bundle 用 `dist/desktop-host` 里的
|
|
127
|
+
后端产物。
|
package/dist/cli/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { a as writeAppConfig, t as SEED_HASH_NAME } from "../seed-Ca0AMFtp.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { chmod, readFile } from "node:fs/promises";
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { cpSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -400,9 +400,8 @@ async function runPrepareRuntime(options) {
|
|
|
400
400
|
* repository resolves the same way:
|
|
401
401
|
* - `@deepseek-ai/dsh` → `dsh.version` when declared (concrete version,
|
|
402
402
|
* range, or `workspace:` in-tree), else `^<resolved>`;
|
|
403
|
-
* - `@deepseek-ai/dsh-desktop-host` →
|
|
404
|
-
*
|
|
405
|
-
* published, so it always comes from the tool);
|
|
403
|
+
* - `@deepseek-ai/dsh-desktop-host` → the tool's own bundled host artifact
|
|
404
|
+
* (upstream never publishes it), `link:<dir>` or a staged `file:` copy;
|
|
406
405
|
* - every other official package → `^<resolved>`.
|
|
407
406
|
*
|
|
408
407
|
* `hasTsx` reports whether the workspace can load TypeScript sources directly;
|
|
@@ -444,6 +443,29 @@ function resolveOfficialPackage(packageName, input) {
|
|
|
444
443
|
}
|
|
445
444
|
}
|
|
446
445
|
/**
|
|
446
|
+
* The tool's bundled desktop-host artifact. Upstream keeps
|
|
447
|
+
* `@deepseek-ai/dsh-desktop-host` private (never published), so the tool ships
|
|
448
|
+
* its built output itself: `dist/desktop-host`, written by the `tsdown` build.
|
|
449
|
+
*/
|
|
450
|
+
function desktopHostDir(toolRoot) {
|
|
451
|
+
const dir = join(toolRoot, "dist", "desktop-host");
|
|
452
|
+
return existsSync(join(dir, "lib", "index.js")) ? realpathSync(dir) : void 0;
|
|
453
|
+
}
|
|
454
|
+
/** The bundled desktop host, or `undefined` when it has not been built. */
|
|
455
|
+
function desktopHost(input) {
|
|
456
|
+
const dir = desktopHostDir(input.toolRoot);
|
|
457
|
+
if (dir === void 0) return void 0;
|
|
458
|
+
try {
|
|
459
|
+
const value = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
460
|
+
return typeof value.version === "string" && value.version !== "" ? {
|
|
461
|
+
dir,
|
|
462
|
+
version: value.version
|
|
463
|
+
} : void 0;
|
|
464
|
+
} catch {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
447
469
|
* The tool's own `node_modules` root holding the official surface, used as a
|
|
448
470
|
* dev fallback for an app that declares only `dsh.version` and has not
|
|
449
471
|
* installed the official packages itself. Prefers the surrounding store (the
|
|
@@ -465,17 +487,22 @@ function officialDependencySpecs(input) {
|
|
|
465
487
|
const specs = {};
|
|
466
488
|
const missing = [];
|
|
467
489
|
for (const packageName of OFFICIAL_RUNTIME_PACKAGES) {
|
|
468
|
-
const resolved = resolveOfficialPackage(packageName, input);
|
|
469
490
|
if (packageName === "@deepseek-ai/dsh" && input.dshVersion !== void 0) {
|
|
470
491
|
specs[packageName] = input.dshVersion;
|
|
471
492
|
continue;
|
|
472
493
|
}
|
|
473
|
-
if (
|
|
474
|
-
|
|
494
|
+
if (packageName === "@deepseek-ai/dsh-desktop-host") {
|
|
495
|
+
const host = desktopHost(input);
|
|
496
|
+
if (host === void 0) {
|
|
497
|
+
missing.push(packageName);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
specs[packageName] = `link:${host.dir}`;
|
|
475
501
|
continue;
|
|
476
502
|
}
|
|
477
|
-
|
|
478
|
-
|
|
503
|
+
const resolved = resolveOfficialPackage(packageName, input);
|
|
504
|
+
if (resolved === void 0) {
|
|
505
|
+
missing.push(packageName);
|
|
479
506
|
continue;
|
|
480
507
|
}
|
|
481
508
|
specs[packageName] = `^${resolved.version}`;
|
|
@@ -483,24 +510,18 @@ function officialDependencySpecs(input) {
|
|
|
483
510
|
if (missing.length > 0) throw new Error(`dsh-desktopify: cannot resolve official packages ${missing.join(", ")}; install them in the workspace or run from the tool's own install`);
|
|
484
511
|
return specs;
|
|
485
512
|
}
|
|
486
|
-
/** Whether `candidate` is `root` itself or nested under it. */
|
|
487
|
-
function isInside(root, candidate) {
|
|
488
|
-
const rel = relative(resolve(root), resolve(candidate));
|
|
489
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
490
|
-
}
|
|
491
513
|
/**
|
|
492
|
-
* Dependency spec for the
|
|
514
|
+
* Dependency spec for the tool's desktop host in a `pnpm deploy` closure.
|
|
493
515
|
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
516
|
+
* `pnpm deploy` only copies linked packages that are workspace members, so a
|
|
517
|
+
* `link:` to the tool's own `dist/desktop-host` would be dropped. Stage a copy
|
|
518
|
+
* inside the app workspace instead (with the host's `workspace:` dependencies
|
|
519
|
+
* rewritten to the specs already computed) and inject it as a `file:` dep. The
|
|
520
|
+
* staged directory is temporary and removed by the caller.
|
|
499
521
|
*/
|
|
500
522
|
function stageDesktopHost(input, specs) {
|
|
501
|
-
const host =
|
|
502
|
-
if (host === void 0) throw new Error(`dsh-desktopify:
|
|
503
|
-
if (isInside(input.workspaceRoot, host.dir)) return `link:${host.dir}`;
|
|
523
|
+
const host = desktopHost(input);
|
|
524
|
+
if (host === void 0) throw new Error(`dsh-desktopify: bundled ${DESKTOP_HOST_PACKAGE} is missing; run the tool build (pnpm build)`);
|
|
504
525
|
const staged = join(input.workspace, ".dsh-desktopify-host");
|
|
505
526
|
rmSync(staged, {
|
|
506
527
|
recursive: true,
|
|
@@ -1052,9 +1073,9 @@ function mirrorDependencyLinks(sourceRoot, destinationRoot, skipExisting = false
|
|
|
1052
1073
|
function prepareDevelopmentProject(projectDir, workspace, input) {
|
|
1053
1074
|
const manifest = workspaceManifest(workspace);
|
|
1054
1075
|
const dsh = resolveOfficialPackage(DSH_PACKAGE, input);
|
|
1055
|
-
const host =
|
|
1076
|
+
const host = desktopHost(input);
|
|
1056
1077
|
if (dsh === void 0) throw new Error(`desktop development: cannot resolve ${DSH_PACKAGE}`);
|
|
1057
|
-
if (host === void 0) throw new Error(`desktop development:
|
|
1078
|
+
if (host === void 0) throw new Error(`desktop development: bundled ${DESKTOP_HOST_PACKAGE} is missing; run the tool build (pnpm build)`);
|
|
1058
1079
|
const virtualStore = join(input.workspaceRoot, "node_modules", ".pnpm", "node_modules");
|
|
1059
1080
|
const workspaceDependencyDir = existsSync(virtualStore) ? virtualStore : join(workspace, "node_modules");
|
|
1060
1081
|
if (!existsSync(workspaceDependencyDir)) throw new Error(`desktop development: workspace dependency links are missing under ${workspaceDependencyDir}; run pnpm install`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Electron reuses the browser composition without its network and browser-launch rows.
|
|
2
|
+
|
|
3
|
+
- id: web-startup
|
|
4
|
+
disabled: true
|
|
5
|
+
|
|
6
|
+
- id: webserver
|
|
7
|
+
disabled: true
|
|
8
|
+
|
|
9
|
+
- id: web-runtime
|
|
10
|
+
disabled: true
|
|
11
|
+
|
|
12
|
+
- id: client-hmr
|
|
13
|
+
disabled: true
|
|
14
|
+
|
|
15
|
+
- id: open-in-app
|
|
16
|
+
disabled: true
|
|
17
|
+
|
|
18
|
+
- id: ui-open-in-app
|
|
19
|
+
disabled: true
|
|
20
|
+
|
|
21
|
+
- id: directory-picker
|
|
22
|
+
disabled: true
|
|
23
|
+
|
|
24
|
+
- id: connection
|
|
25
|
+
inject:
|
|
26
|
+
- credentials
|
|
27
|
+
config: {}
|
|
28
|
+
|
|
29
|
+
- insert:
|
|
30
|
+
- id: directory-picker-native
|
|
31
|
+
name: '@deepseek-ai/dsh-host-directory-picker-native'
|
|
32
|
+
|
|
33
|
+
- id: ui-directory-picker-native
|
|
34
|
+
name: '@deepseek-ai/dsh-client-ui-directory-picker-native'
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { boot, composeEntries, loadLayeredEnv, loadOverlayPatches, loadProfileDirectory } from "@deepseek-ai/dsh-app-boot";
|
|
8
|
+
import { provideCmdline } from "@deepseek-ai/dsh-cmdline";
|
|
9
|
+
import { DSH_LAUNCH_ENVIRONMENT_KEY } from "@deepseek-ai/dsh-launch-environment";
|
|
10
|
+
import { renderIndexInjections } from "@deepseek-ai/dsh-host-webserver";
|
|
11
|
+
//#region lib/types/wire.js
|
|
12
|
+
/** Framed request and response bytes for the Electron Desktop Host transport. */
|
|
13
|
+
/** Protocol version shared with the Electron shell. */
|
|
14
|
+
const DESKTOP_HOST_PROTOCOL_VERSION = 3;
|
|
15
|
+
/** Maximum raw body bytes carried by one data frame. */
|
|
16
|
+
const DESKTOP_PIPE_CHUNK_BYTES = 64 * 1024;
|
|
17
|
+
const FRAME_MAGIC = 1146308659;
|
|
18
|
+
const FRAME_HEADER_BYTES = 13;
|
|
19
|
+
const MAX_CONTROL_PAYLOAD_BYTES = 1024 * 1024;
|
|
20
|
+
const REQUEST_FRAME_START = 1;
|
|
21
|
+
const REQUEST_FRAME_DATA = 2;
|
|
22
|
+
const REQUEST_FRAME_END = 3;
|
|
23
|
+
const REQUEST_FRAME_CANCEL = 4;
|
|
24
|
+
const RESPONSE_FRAME_START = 1;
|
|
25
|
+
const RESPONSE_FRAME_DATA = 2;
|
|
26
|
+
const RESPONSE_FRAME_END = 3;
|
|
27
|
+
const RESPONSE_FRAME_ERROR = 4;
|
|
28
|
+
function isRecord$1(value) {
|
|
29
|
+
return typeof value === "object" && value !== null;
|
|
30
|
+
}
|
|
31
|
+
function isHeaders(value) {
|
|
32
|
+
return Array.isArray(value) && value.every((header) => Array.isArray(header) && header.length === 2 && typeof header[0] === "string" && typeof header[1] === "string");
|
|
33
|
+
}
|
|
34
|
+
function assertStreamId(streamId) {
|
|
35
|
+
if (!Number.isInteger(streamId) || streamId < 1 || streamId > 4294967295) throw new Error(`dsh desktop: invalid pipe stream id ${String(streamId)}`);
|
|
36
|
+
}
|
|
37
|
+
function encodeFrame(type, streamId, payload) {
|
|
38
|
+
assertStreamId(streamId);
|
|
39
|
+
const limit = type === RESPONSE_FRAME_DATA ? DESKTOP_PIPE_CHUNK_BYTES : MAX_CONTROL_PAYLOAD_BYTES;
|
|
40
|
+
if (payload.byteLength > limit) throw new Error(`dsh desktop: response pipe frame exceeds the ${String(limit)}-byte limit`);
|
|
41
|
+
const frame = Buffer.allocUnsafe(FRAME_HEADER_BYTES + payload.byteLength);
|
|
42
|
+
frame.writeUInt32BE(FRAME_MAGIC, 0);
|
|
43
|
+
frame.writeUInt8(type, 4);
|
|
44
|
+
frame.writeUInt32BE(streamId, 5);
|
|
45
|
+
frame.writeUInt32BE(payload.byteLength, 9);
|
|
46
|
+
payload.copy(frame, FRAME_HEADER_BYTES);
|
|
47
|
+
return frame;
|
|
48
|
+
}
|
|
49
|
+
function encodeJsonFrame(type, streamId, value) {
|
|
50
|
+
return encodeFrame(type, streamId, Buffer.from(JSON.stringify(value), "utf8"));
|
|
51
|
+
}
|
|
52
|
+
/** Encode response metadata before any body frames. */
|
|
53
|
+
function encodeDesktopResponseStart(streamId, response) {
|
|
54
|
+
return encodeJsonFrame(RESPONSE_FRAME_START, streamId, response);
|
|
55
|
+
}
|
|
56
|
+
/** Encode one bounded raw response-body chunk. */
|
|
57
|
+
function encodeDesktopResponseData(streamId, data) {
|
|
58
|
+
return encodeFrame(RESPONSE_FRAME_DATA, streamId, Buffer.from(data));
|
|
59
|
+
}
|
|
60
|
+
/** Encode normal response completion. */
|
|
61
|
+
function encodeDesktopResponseEnd(streamId) {
|
|
62
|
+
return encodeFrame(RESPONSE_FRAME_END, streamId, Buffer.alloc(0));
|
|
63
|
+
}
|
|
64
|
+
/** Encode one response failure without exposing an Error object across processes. */
|
|
65
|
+
function encodeDesktopResponseError(streamId, message) {
|
|
66
|
+
return encodeJsonFrame(RESPONSE_FRAME_ERROR, streamId, { message });
|
|
67
|
+
}
|
|
68
|
+
/** Incrementally decode validated request frames from the Electron byte pipe. */
|
|
69
|
+
var DesktopHostRequestDecoder = class {
|
|
70
|
+
buffer = Buffer.alloc(0);
|
|
71
|
+
/**
|
|
72
|
+
* Append bytes and return every complete request frame.
|
|
73
|
+
* @param chunk - next bytes read from the Electron request pipe.
|
|
74
|
+
* @returns complete frames in pipe order.
|
|
75
|
+
*/
|
|
76
|
+
push(chunk) {
|
|
77
|
+
this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
78
|
+
const frames = [];
|
|
79
|
+
for (;;) {
|
|
80
|
+
const frame = this.next();
|
|
81
|
+
if (frame === void 0) return frames;
|
|
82
|
+
frames.push(frame);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Reject EOF that splits a frame. */
|
|
86
|
+
finish() {
|
|
87
|
+
if (this.buffer.byteLength !== 0) throw new Error("dsh desktop: Electron request pipe ended inside a frame");
|
|
88
|
+
}
|
|
89
|
+
next() {
|
|
90
|
+
if (this.buffer.byteLength < FRAME_HEADER_BYTES) return void 0;
|
|
91
|
+
if (this.buffer.readUInt32BE(0) !== FRAME_MAGIC) throw new Error("dsh desktop: invalid Electron request frame marker");
|
|
92
|
+
const rawType = this.buffer.readUInt8(4);
|
|
93
|
+
const streamId = this.buffer.readUInt32BE(5);
|
|
94
|
+
const payloadLength = this.buffer.readUInt32BE(9);
|
|
95
|
+
assertStreamId(streamId);
|
|
96
|
+
const limit = rawType === REQUEST_FRAME_DATA ? DESKTOP_PIPE_CHUNK_BYTES : MAX_CONTROL_PAYLOAD_BYTES;
|
|
97
|
+
if (payloadLength > limit) throw new Error(`dsh desktop: Electron request frame exceeds the ${String(limit)}-byte limit`);
|
|
98
|
+
const frameLength = FRAME_HEADER_BYTES + payloadLength;
|
|
99
|
+
if (this.buffer.byteLength < frameLength) return void 0;
|
|
100
|
+
const payload = this.buffer.subarray(FRAME_HEADER_BYTES, frameLength);
|
|
101
|
+
this.buffer = this.buffer.subarray(frameLength);
|
|
102
|
+
switch (rawType) {
|
|
103
|
+
case REQUEST_FRAME_START: return this.parseStart(streamId, payload);
|
|
104
|
+
case REQUEST_FRAME_DATA: return {
|
|
105
|
+
type: "data",
|
|
106
|
+
streamId,
|
|
107
|
+
data: payload
|
|
108
|
+
};
|
|
109
|
+
case REQUEST_FRAME_END:
|
|
110
|
+
if (payloadLength !== 0) throw new Error("dsh desktop: Electron request end frame carried a payload");
|
|
111
|
+
return {
|
|
112
|
+
type: "end",
|
|
113
|
+
streamId
|
|
114
|
+
};
|
|
115
|
+
case REQUEST_FRAME_CANCEL:
|
|
116
|
+
if (payloadLength !== 0) throw new Error("dsh desktop: Electron request cancel frame carried a payload");
|
|
117
|
+
return {
|
|
118
|
+
type: "cancel",
|
|
119
|
+
streamId
|
|
120
|
+
};
|
|
121
|
+
default: throw new Error(`dsh desktop: unknown Electron request frame type ${String(rawType)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
parseStart(streamId, payload) {
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = JSON.parse(payload.toString("utf8"));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`dsh desktop: Electron request start payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
if (!isRecord$1(value) || typeof value.url !== "string" || typeof value.method !== "string" || !isHeaders(value.headers) || typeof value.hasBody !== "boolean") throw new Error("dsh desktop: invalid Electron request start payload");
|
|
132
|
+
return {
|
|
133
|
+
type: "start",
|
|
134
|
+
streamId,
|
|
135
|
+
url: value.url,
|
|
136
|
+
method: value.method,
|
|
137
|
+
headers: value.headers,
|
|
138
|
+
hasBody: value.hasBody
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region lib/types/index.js
|
|
144
|
+
/**
|
|
145
|
+
* Electron child-process entry: boots the desktop project without a listening
|
|
146
|
+
* socket and carries API plus validated Web assets over framed byte pipes.
|
|
147
|
+
* @module @deepseek-ai/dsh-desktop-host
|
|
148
|
+
*/
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
function isDesktopHostCommand(message) {
|
|
153
|
+
return typeof message === "object" && message !== null && "type" in message && message.type === "shutdown";
|
|
154
|
+
}
|
|
155
|
+
const DESKTOP_PATCH = fileURLToPath(new URL("../config/desktop.cordis.patch.yml", import.meta.url));
|
|
156
|
+
const ROOT_CONFIG = "# Electron desktop composition root; package transactions own this file.\n[]\n";
|
|
157
|
+
const ROOT_CONFIG_FILENAME = "desktop.cordis.yml";
|
|
158
|
+
const DESKTOP_STREAM_PATH = "/.dsh/remote-stream";
|
|
159
|
+
const DESKTOP_TRANSPORT_SCRIPT = `globalThis.__DSH_TRANSPORT__={
|
|
160
|
+
ownsHost:true,
|
|
161
|
+
async *openStream(endpoint,payload,signal){
|
|
162
|
+
const response=await fetch(${JSON.stringify(DESKTOP_STREAM_PATH)},{
|
|
163
|
+
method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({endpoint,payload}),signal
|
|
164
|
+
})
|
|
165
|
+
if(!response.ok||response.body===null)throw new Error('desktop stream transport failed: HTTP '+response.status)
|
|
166
|
+
const reader=response.body.getReader(),decoder=new TextDecoder()
|
|
167
|
+
let pending=''
|
|
168
|
+
for(;;){
|
|
169
|
+
const {done,value}=await reader.read()
|
|
170
|
+
pending+=decoder.decode(value,{stream:!done})
|
|
171
|
+
let newline
|
|
172
|
+
while((newline=pending.indexOf('\\n'))!==-1){
|
|
173
|
+
const line=pending.slice(0,newline);pending=pending.slice(newline+1)
|
|
174
|
+
if(line!=='')yield JSON.parse(line)
|
|
175
|
+
}
|
|
176
|
+
if(done)break
|
|
177
|
+
}
|
|
178
|
+
if(pending!=='')yield JSON.parse(pending)
|
|
179
|
+
}
|
|
180
|
+
}`;
|
|
181
|
+
const MIME = {
|
|
182
|
+
".css": "text/css; charset=utf-8",
|
|
183
|
+
".html": "text/html; charset=utf-8",
|
|
184
|
+
".js": "text/javascript; charset=utf-8",
|
|
185
|
+
".json": "application/json",
|
|
186
|
+
".svg": "image/svg+xml",
|
|
187
|
+
".webmanifest": "application/manifest+json"
|
|
188
|
+
};
|
|
189
|
+
function readManifest(path) {
|
|
190
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
191
|
+
if (!isRecord(value)) throw new Error(`dsh desktop: ${path} must contain a package manifest`);
|
|
192
|
+
return {
|
|
193
|
+
...typeof value.name === "string" ? { name: value.name } : {},
|
|
194
|
+
...typeof value.version === "string" ? { version: value.version } : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function packageManifestPath(projectDir, packageName) {
|
|
198
|
+
const path = join(projectDir, "node_modules", ...packageName.split("/"), "package.json");
|
|
199
|
+
if (!existsSync(path)) throw new Error(`dsh desktop: installed package ${JSON.stringify(packageName)} has no manifest`);
|
|
200
|
+
return path;
|
|
201
|
+
}
|
|
202
|
+
function isProjectPath(projectDir, target) {
|
|
203
|
+
const root = realpathSync(projectDir);
|
|
204
|
+
const path = realpathSync(target);
|
|
205
|
+
return path === root || path.startsWith(root + sep);
|
|
206
|
+
}
|
|
207
|
+
function desktopPatches(projectDir, allowLinkedPackages) {
|
|
208
|
+
const dshRoot = dirname(packageManifestPath(projectDir, "@deepseek-ai/dsh"));
|
|
209
|
+
const profile = loadProfileDirectory("dsh desktop", projectDir, join(dshRoot, "package.json"));
|
|
210
|
+
for (const layer of profile.layers) if (!allowLinkedPackages && !isProjectPath(projectDir, layer.packageDir)) throw new Error(`dsh desktop: profile bundle ${JSON.stringify(layer.packageName)} resolved outside the desktop profile`);
|
|
211
|
+
const layers = [
|
|
212
|
+
...profile.layers.map((layer) => layer.patches),
|
|
213
|
+
profile.patches,
|
|
214
|
+
loadOverlayPatches("dsh desktop", DESKTOP_PATCH)
|
|
215
|
+
];
|
|
216
|
+
const agentPresets = new Map(composeEntries(layers).flatMap((row) => typeof row.id === "string" ? [[row.id, row]] : [])).get("agent-presets");
|
|
217
|
+
if (agentPresets !== void 0) layers.push([{
|
|
218
|
+
id: "agent-presets",
|
|
219
|
+
config: {
|
|
220
|
+
...agentPresets.config ?? {},
|
|
221
|
+
roots: [{
|
|
222
|
+
path: join(dshRoot, "config", "agent-presets"),
|
|
223
|
+
trust: "system"
|
|
224
|
+
}]
|
|
225
|
+
}
|
|
226
|
+
}]);
|
|
227
|
+
return layers.flat();
|
|
228
|
+
}
|
|
229
|
+
function dshVersion(projectDir) {
|
|
230
|
+
const manifest = readManifest(packageManifestPath(projectDir, "@deepseek-ai/dsh"));
|
|
231
|
+
if (typeof manifest.version !== "string") throw new Error("dsh desktop: installed dsh manifest has no version");
|
|
232
|
+
return manifest.version;
|
|
233
|
+
}
|
|
234
|
+
function assetHandler(ctx, projectDir) {
|
|
235
|
+
const distIndex = createRequire(join(projectDir, "package.json")).resolve("@deepseek-ai/dsh-web-frontend/dist/index.html");
|
|
236
|
+
const distRoot = realpathSync(dirname(distIndex));
|
|
237
|
+
const renderIndex = async () => {
|
|
238
|
+
const rows = [{
|
|
239
|
+
kind: "script",
|
|
240
|
+
placement: "head",
|
|
241
|
+
text: DESKTOP_TRANSPORT_SCRIPT
|
|
242
|
+
}];
|
|
243
|
+
ctx.emit("webserver/index-inject", rows);
|
|
244
|
+
const body = renderIndexInjections(await readFile(distIndex, "utf8"), rows);
|
|
245
|
+
return new Response(body, { headers: { "content-type": MIME[".html"] ?? "text/html; charset=utf-8" } });
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
requestBodyMode: () => "buffered",
|
|
249
|
+
async fetch(request) {
|
|
250
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response(null, { status: 405 });
|
|
251
|
+
const url = new URL(request.url);
|
|
252
|
+
if (url.pathname.startsWith("/plugins/")) return ctx.clientModules.fetchBundle(request);
|
|
253
|
+
let pathname;
|
|
254
|
+
try {
|
|
255
|
+
pathname = decodeURIComponent(url.pathname);
|
|
256
|
+
} catch {
|
|
257
|
+
return new Response(null, { status: 400 });
|
|
258
|
+
}
|
|
259
|
+
if (pathname === "/" || pathname === "/index.html") return renderIndex();
|
|
260
|
+
const target = resolve(normalize(join(distRoot, pathname)));
|
|
261
|
+
if (target !== distRoot && !target.startsWith(distRoot + sep)) return new Response(null, { status: 403 });
|
|
262
|
+
try {
|
|
263
|
+
const realTarget = realpathSync(target);
|
|
264
|
+
if (realTarget !== distRoot && !realTarget.startsWith(distRoot + sep)) return new Response(null, { status: 403 });
|
|
265
|
+
return new Response(request.method === "HEAD" ? null : await readFile(realTarget), { headers: { "content-type": MIME[extname(realTarget)] ?? "application/octet-stream" } });
|
|
266
|
+
} catch {
|
|
267
|
+
return renderIndex();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function remoteStreamHandler(ctx) {
|
|
273
|
+
return {
|
|
274
|
+
requestBodyMode: () => "buffered",
|
|
275
|
+
async fetch(request) {
|
|
276
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
277
|
+
const gateway = ctx.get("typertGateway");
|
|
278
|
+
if (gateway === void 0) return new Response("gateway unavailable", { status: 503 });
|
|
279
|
+
let body;
|
|
280
|
+
try {
|
|
281
|
+
body = await request.json();
|
|
282
|
+
} catch {
|
|
283
|
+
return new Response("body is not JSON", { status: 400 });
|
|
284
|
+
}
|
|
285
|
+
if (!isRecord(body) || typeof body.endpoint !== "string") return new Response("invalid stream request", { status: 400 });
|
|
286
|
+
const abort = new AbortController();
|
|
287
|
+
const cancel = () => {
|
|
288
|
+
abort.abort(request.signal.reason);
|
|
289
|
+
};
|
|
290
|
+
request.signal.addEventListener("abort", cancel, { once: true });
|
|
291
|
+
const encoder = new TextEncoder();
|
|
292
|
+
const stream = new ReadableStream({
|
|
293
|
+
async start(controller) {
|
|
294
|
+
try {
|
|
295
|
+
const values = await gateway.wireStream.open(body.endpoint, body.payload, abort.signal);
|
|
296
|
+
for await (const value of values) controller.enqueue(encoder.encode(`${JSON.stringify(value)}\n`));
|
|
297
|
+
controller.close();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
controller.error(error);
|
|
300
|
+
} finally {
|
|
301
|
+
request.signal.removeEventListener("abort", cancel);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
cancel(reason) {
|
|
305
|
+
abort.abort(reason);
|
|
306
|
+
request.signal.removeEventListener("abort", cancel);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return new Response(stream, { headers: { "content-type": "application/x-ndjson" } });
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Boot one installed desktop npm project.
|
|
315
|
+
* @param projectDir - active or staged Electron-owned desktop profile.
|
|
316
|
+
* @param writeResponse - serialized response-pipe writer that applies byte backpressure.
|
|
317
|
+
* @param options - development-only allowance for workspace-linked bundle packages.
|
|
318
|
+
* @returns controller after every Host and client-manifest row is active.
|
|
319
|
+
*/
|
|
320
|
+
async function runDesktopHost(projectDir, writeResponse, options = {}) {
|
|
321
|
+
const absoluteProject = resolve(projectDir);
|
|
322
|
+
mkdirSync(absoluteProject, { recursive: true });
|
|
323
|
+
const rootConfig = join(absoluteProject, ROOT_CONFIG_FILENAME);
|
|
324
|
+
writeFileSync(rootConfig, ROOT_CONFIG);
|
|
325
|
+
const environment = loadLayeredEnv("dsh desktop");
|
|
326
|
+
let current;
|
|
327
|
+
const ctx = await boot("dsh desktop", rootConfig, structuredClone(desktopPatches(absoluteProject, options.allowLinkedPackages === true)), (hostCtx) => {
|
|
328
|
+
current = hostCtx;
|
|
329
|
+
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, environment);
|
|
330
|
+
provideCmdline(hostCtx, {
|
|
331
|
+
args: [],
|
|
332
|
+
exit: () => {}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
current = ctx;
|
|
336
|
+
const connection = ctx.get("connection");
|
|
337
|
+
const clientModules = ctx.get("clientModules");
|
|
338
|
+
const gateway = ctx.get("typertGateway");
|
|
339
|
+
if (connection === void 0 || clientModules === void 0 || gateway === void 0) {
|
|
340
|
+
await ctx.fiber.dispose();
|
|
341
|
+
throw new Error("dsh desktop: composition did not provide connection, typertGateway, and clientModules");
|
|
342
|
+
}
|
|
343
|
+
const api = connection.createSharedFetchHandler("/api");
|
|
344
|
+
const assets = assetHandler(ctx, absoluteProject);
|
|
345
|
+
const streams = remoteStreamHandler(ctx);
|
|
346
|
+
const requests = /* @__PURE__ */ new Map();
|
|
347
|
+
let disposing;
|
|
348
|
+
const dispose = async () => {
|
|
349
|
+
disposing ??= (async () => {
|
|
350
|
+
for (const controller of requests.values()) controller.abort();
|
|
351
|
+
requests.clear();
|
|
352
|
+
await current?.fiber.dispose();
|
|
353
|
+
current = void 0;
|
|
354
|
+
})();
|
|
355
|
+
await disposing;
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
dshVersion: dshVersion(absoluteProject),
|
|
359
|
+
cancel(streamId) {
|
|
360
|
+
requests.get(streamId)?.abort();
|
|
361
|
+
},
|
|
362
|
+
async fetch(command, body) {
|
|
363
|
+
if (disposing !== void 0) throw new Error("dsh desktop: host is disposing");
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
requests.set(command.streamId, controller);
|
|
366
|
+
try {
|
|
367
|
+
const url = new URL(command.request.url);
|
|
368
|
+
const init = {
|
|
369
|
+
method: command.request.method,
|
|
370
|
+
headers: new Headers(command.request.headers.map(([name, value]) => [name, value])),
|
|
371
|
+
...body === null ? {} : {
|
|
372
|
+
body,
|
|
373
|
+
duplex: "half"
|
|
374
|
+
},
|
|
375
|
+
signal: controller.signal
|
|
376
|
+
};
|
|
377
|
+
const request = new Request(url, init);
|
|
378
|
+
const response = url.pathname === DESKTOP_STREAM_PATH ? await streams.fetch(request) : url.pathname.startsWith("/api/") ? await api.fetch(request) : await assets.fetch(request);
|
|
379
|
+
await writeResponse(encodeDesktopResponseStart(command.streamId, {
|
|
380
|
+
status: response.status,
|
|
381
|
+
headers: [...response.headers.entries()],
|
|
382
|
+
hasBody: response.body !== null
|
|
383
|
+
}));
|
|
384
|
+
if (response.body !== null) for await (const chunk of response.body) {
|
|
385
|
+
const bytes = Buffer.from(chunk);
|
|
386
|
+
for (let offset = 0; offset < bytes.byteLength; offset += DESKTOP_PIPE_CHUNK_BYTES) await writeResponse(encodeDesktopResponseData(command.streamId, bytes.subarray(offset, offset + DESKTOP_PIPE_CHUNK_BYTES)));
|
|
387
|
+
}
|
|
388
|
+
await writeResponse(encodeDesktopResponseEnd(command.streamId));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (!controller.signal.aborted) await writeResponse(encodeDesktopResponseError(command.streamId, error instanceof Error ? error.message : String(error)));
|
|
391
|
+
} finally {
|
|
392
|
+
requests.delete(command.streamId);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
dispose
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async function main() {
|
|
399
|
+
const projectDir = process.argv[2];
|
|
400
|
+
if (projectDir === void 0 || process.send === void 0) throw new Error("dsh desktop: expected project directory, byte pipes, and a Node IPC channel");
|
|
401
|
+
const option = process.argv[3];
|
|
402
|
+
if (option !== void 0 && option !== "--allow-linked-profile") throw new Error(`dsh desktop: unsupported internal option ${JSON.stringify(option)}`);
|
|
403
|
+
const requestPipe = createReadStream("", {
|
|
404
|
+
fd: 3,
|
|
405
|
+
autoClose: false
|
|
406
|
+
});
|
|
407
|
+
const responsePipe = createWriteStream("", {
|
|
408
|
+
fd: 4,
|
|
409
|
+
autoClose: false
|
|
410
|
+
});
|
|
411
|
+
let responseWriteTail = Promise.resolve();
|
|
412
|
+
const writeResponse = (frame) => {
|
|
413
|
+
const write = responseWriteTail.then(async () => {
|
|
414
|
+
if (responsePipe.destroyed) throw new Error("dsh desktop: Electron response pipe is unavailable");
|
|
415
|
+
if (!responsePipe.write(frame)) await once(responsePipe, "drain");
|
|
416
|
+
});
|
|
417
|
+
responseWriteTail = write.catch(() => void 0);
|
|
418
|
+
return write;
|
|
419
|
+
};
|
|
420
|
+
const send = (event) => {
|
|
421
|
+
if (process.send === void 0 || !process.connected) return;
|
|
422
|
+
try {
|
|
423
|
+
process.send(event);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error.code !== "ERR_IPC_CHANNEL_CLOSED") throw error;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const controller = await runDesktopHost(projectDir, writeResponse, { allowLinkedPackages: option !== void 0 });
|
|
429
|
+
send({
|
|
430
|
+
type: "ready",
|
|
431
|
+
protocolVersion: 3,
|
|
432
|
+
dshVersion: controller.dshVersion
|
|
433
|
+
});
|
|
434
|
+
const decoder = new DesktopHostRequestDecoder();
|
|
435
|
+
const requestBodies = /* @__PURE__ */ new Map();
|
|
436
|
+
const blockedRequests = /* @__PURE__ */ new Set();
|
|
437
|
+
const discardedRequestBodies = /* @__PURE__ */ new Set();
|
|
438
|
+
const runs = /* @__PURE__ */ new Set();
|
|
439
|
+
let lastStreamId = 0;
|
|
440
|
+
let requestedExitCode = 0;
|
|
441
|
+
let stopping;
|
|
442
|
+
const resumeRequestPipe = () => {
|
|
443
|
+
if (blockedRequests.size === 0) requestPipe.resume();
|
|
444
|
+
};
|
|
445
|
+
const stop = (exitCode = 0) => {
|
|
446
|
+
requestedExitCode = Math.max(requestedExitCode, exitCode);
|
|
447
|
+
stopping ??= (async () => {
|
|
448
|
+
requestPipe.pause();
|
|
449
|
+
requestPipe.removeAllListeners("data");
|
|
450
|
+
const stopped = /* @__PURE__ */ new Error("dsh desktop: Host is stopping");
|
|
451
|
+
for (const body of requestBodies.values()) body.error(stopped);
|
|
452
|
+
requestBodies.clear();
|
|
453
|
+
blockedRequests.clear();
|
|
454
|
+
discardedRequestBodies.clear();
|
|
455
|
+
requestPipe.destroy();
|
|
456
|
+
closeSync(3);
|
|
457
|
+
await controller.dispose();
|
|
458
|
+
await Promise.allSettled([...runs]);
|
|
459
|
+
await responseWriteTail.catch(() => void 0);
|
|
460
|
+
if (!responsePipe.destroyed) {
|
|
461
|
+
await new Promise((resolvePromise) => {
|
|
462
|
+
responsePipe.end(resolvePromise);
|
|
463
|
+
});
|
|
464
|
+
responsePipe.destroy();
|
|
465
|
+
}
|
|
466
|
+
closeSync(4);
|
|
467
|
+
if (process.connected) process.disconnect();
|
|
468
|
+
process.exitCode = requestedExitCode;
|
|
469
|
+
})();
|
|
470
|
+
return stopping;
|
|
471
|
+
};
|
|
472
|
+
const failTransport = (error) => {
|
|
473
|
+
send({
|
|
474
|
+
type: "fatal",
|
|
475
|
+
message: error instanceof Error ? error.message : String(error)
|
|
476
|
+
});
|
|
477
|
+
stop(1);
|
|
478
|
+
};
|
|
479
|
+
const beginRequest = (frame) => {
|
|
480
|
+
if (frame.streamId <= lastStreamId) throw new Error(`dsh desktop: Electron reused or reordered request stream ${String(frame.streamId)}`);
|
|
481
|
+
lastStreamId = frame.streamId;
|
|
482
|
+
let body = null;
|
|
483
|
+
if (frame.hasBody) body = new ReadableStream({
|
|
484
|
+
start(controllerOfBody) {
|
|
485
|
+
requestBodies.set(frame.streamId, controllerOfBody);
|
|
486
|
+
},
|
|
487
|
+
pull() {
|
|
488
|
+
blockedRequests.delete(frame.streamId);
|
|
489
|
+
resumeRequestPipe();
|
|
490
|
+
},
|
|
491
|
+
cancel() {
|
|
492
|
+
requestBodies.delete(frame.streamId);
|
|
493
|
+
blockedRequests.delete(frame.streamId);
|
|
494
|
+
controller.cancel(frame.streamId);
|
|
495
|
+
resumeRequestPipe();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
const run = controller.fetch({
|
|
499
|
+
streamId: frame.streamId,
|
|
500
|
+
request: {
|
|
501
|
+
url: frame.url,
|
|
502
|
+
method: frame.method,
|
|
503
|
+
headers: frame.headers
|
|
504
|
+
}
|
|
505
|
+
}, body);
|
|
506
|
+
runs.add(run);
|
|
507
|
+
run.catch(failTransport).finally(() => {
|
|
508
|
+
runs.delete(run);
|
|
509
|
+
const openBody = requestBodies.get(frame.streamId);
|
|
510
|
+
if (openBody === void 0) return;
|
|
511
|
+
openBody.error(/* @__PURE__ */ new Error("dsh desktop: response completed before the request body ended"));
|
|
512
|
+
requestBodies.delete(frame.streamId);
|
|
513
|
+
blockedRequests.delete(frame.streamId);
|
|
514
|
+
discardedRequestBodies.add(frame.streamId);
|
|
515
|
+
resumeRequestPipe();
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
const handleRequestFrame = (frame) => {
|
|
519
|
+
switch (frame.type) {
|
|
520
|
+
case "start":
|
|
521
|
+
beginRequest(frame);
|
|
522
|
+
return;
|
|
523
|
+
case "data": {
|
|
524
|
+
const body = requestBodies.get(frame.streamId);
|
|
525
|
+
if (body === void 0) {
|
|
526
|
+
if (discardedRequestBodies.has(frame.streamId)) return;
|
|
527
|
+
throw new Error(`dsh desktop: Electron sent body data for inactive stream ${String(frame.streamId)}`);
|
|
528
|
+
}
|
|
529
|
+
body.enqueue(frame.data);
|
|
530
|
+
if ((body.desiredSize ?? 0) <= 0) {
|
|
531
|
+
blockedRequests.add(frame.streamId);
|
|
532
|
+
requestPipe.pause();
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
case "end": {
|
|
537
|
+
const body = requestBodies.get(frame.streamId);
|
|
538
|
+
if (body === void 0) {
|
|
539
|
+
if (discardedRequestBodies.delete(frame.streamId)) return;
|
|
540
|
+
throw new Error(`dsh desktop: Electron ended inactive body stream ${String(frame.streamId)}`);
|
|
541
|
+
}
|
|
542
|
+
body.close();
|
|
543
|
+
requestBodies.delete(frame.streamId);
|
|
544
|
+
blockedRequests.delete(frame.streamId);
|
|
545
|
+
resumeRequestPipe();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
case "cancel":
|
|
549
|
+
if (frame.streamId > lastStreamId) throw new Error(`dsh desktop: Electron canceled unknown stream ${String(frame.streamId)}`);
|
|
550
|
+
requestBodies.get(frame.streamId)?.error(/* @__PURE__ */ new Error("dsh desktop: Electron canceled the request"));
|
|
551
|
+
requestBodies.delete(frame.streamId);
|
|
552
|
+
blockedRequests.delete(frame.streamId);
|
|
553
|
+
discardedRequestBodies.delete(frame.streamId);
|
|
554
|
+
controller.cancel(frame.streamId);
|
|
555
|
+
resumeRequestPipe();
|
|
556
|
+
return;
|
|
557
|
+
default:
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
requestPipe.on("data", (chunk) => {
|
|
561
|
+
try {
|
|
562
|
+
for (const frame of decoder.push(Buffer.from(chunk))) handleRequestFrame(frame);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
failTransport(error);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
requestPipe.once("end", () => {
|
|
568
|
+
if (stopping !== void 0) return;
|
|
569
|
+
try {
|
|
570
|
+
decoder.finish();
|
|
571
|
+
failTransport(/* @__PURE__ */ new Error("dsh desktop: Electron request pipe ended"));
|
|
572
|
+
} catch (error) {
|
|
573
|
+
failTransport(error);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
requestPipe.once("error", failTransport);
|
|
577
|
+
responsePipe.once("error", failTransport);
|
|
578
|
+
process.on("message", (message) => {
|
|
579
|
+
if (!isDesktopHostCommand(message)) {
|
|
580
|
+
send({
|
|
581
|
+
type: "fatal",
|
|
582
|
+
message: "dsh desktop: invalid Electron IPC command"
|
|
583
|
+
});
|
|
584
|
+
stop(1);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
stop();
|
|
588
|
+
});
|
|
589
|
+
process.once("disconnect", () => {
|
|
590
|
+
stop();
|
|
591
|
+
});
|
|
592
|
+
process.once("SIGTERM", () => {
|
|
593
|
+
stop();
|
|
594
|
+
});
|
|
595
|
+
process.once("SIGINT", () => {
|
|
596
|
+
stop();
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (import.meta.main) main().catch((error) => {
|
|
600
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
601
|
+
if (process.send !== void 0) process.send({
|
|
602
|
+
type: "fatal",
|
|
603
|
+
message
|
|
604
|
+
});
|
|
605
|
+
else process.stderr.write(`dsh desktop: ${message}\n`);
|
|
606
|
+
process.exitCode = 1;
|
|
607
|
+
});
|
|
608
|
+
//#endregion
|
|
609
|
+
export { DESKTOP_HOST_PROTOCOL_VERSION, runDesktopHost };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-desktop-host",
|
|
3
|
+
"description": "Private upstream-Node host process for the Electron desktop application",
|
|
4
|
+
"version": "0.1.5-rc.1",
|
|
5
|
+
"private": true,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib/index.js",
|
|
11
|
+
"config/desktop.cordis.patch.yml"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@deepseek-ai/cordis": "workspace:^",
|
|
15
|
+
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
|
16
|
+
"@deepseek-ai/dsh": "workspace:^",
|
|
17
|
+
"@deepseek-ai/dsh-api-gateway": "workspace:^",
|
|
18
|
+
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
|
19
|
+
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
|
20
|
+
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
|
21
|
+
"@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^",
|
|
22
|
+
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
|
23
|
+
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
|
|
24
|
+
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
|
25
|
+
"@deepseek-ai/dsh-launch-environment": "workspace:^",
|
|
26
|
+
"@deepseek-ai/dsh-web-frontend": "workspace:^"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-desktopify",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Desktop packaging tool for dsh workspaces: hosts the upstream dsh-desktop-host child over framed byte pipes, with dev (workspace-linked) and bundle (static, unsigned) modes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"desktop",
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@deepseek-ai/dsh": "^0.1.5-rc.1",
|
|
33
|
-
"@deepseek-ai/dsh-desktop-host": "^0.1.5-rc.1",
|
|
34
33
|
"commander": "^15.0.0",
|
|
35
34
|
"electron": "^44.3.0",
|
|
36
35
|
"electron-builder": "^26.16.1",
|