@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
- - **后端**:直接复用上游已构建的 `@deepseek-ai/dsh-desktop-host`(字节管
41
- 道协议),不重复实现 boot 逻辑。
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:`,仓库外的独立项目同样可装配;`dsh-desktop-host` 未发布,
48
- 固定由工具引入(同一 workspace 用 `link:`,独立项目暂存后 `file:`)。
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` 已构建(dev 需要 dsh CLI 与 desktop-host 的
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
+ 后端产物。
@@ -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, isAbsolute, join, relative, resolve, sep } from "node:path";
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` → `link:<dir>`, or a staged `file:`
404
- * copy when the host lives outside the app workspace (the host is not
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 (resolved === void 0) {
474
- missing.push(packageName);
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
- if (packageName === "@deepseek-ai/dsh-desktop-host") {
478
- specs[packageName] = `link:${resolved.dir}`;
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 private desktop host in a `pnpm deploy` closure.
514
+ * Dependency spec for the tool's desktop host in a `pnpm deploy` closure.
493
515
  *
494
- * In-repo the host lives inside the same pnpm workspace, so a `link:` spec is
495
- * copied by deploy. When the app workspace is elsewhere, `pnpm deploy` refuses
496
- * paths outside it: stage a copy inside the app workspace (with the host's
497
- * `workspace:` dependencies rewritten to the specs already computed) and use a
498
- * `file:` spec. The staged directory is temporary and removed by the caller.
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 = resolveOfficialPackage(DESKTOP_HOST_PACKAGE, input);
502
- if (host === void 0) throw new Error(`dsh-desktopify: cannot resolve ${DESKTOP_HOST_PACKAGE}`);
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 = resolveOfficialPackage(DESKTOP_HOST_PACKAGE, input);
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: cannot resolve ${DESKTOP_HOST_PACKAGE}`);
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.0",
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",