@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.
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env node
2
+ import { a as writeAppConfig, t as SEED_HASH_NAME } from "../seed-Ca0AMFtp.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { chmod, readFile } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { cpSync, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
9
+ import { Command } from "commander";
10
+ import { build } from "electron-builder";
11
+ import sharp from "sharp";
12
+ import { createHash } from "node:crypto";
13
+ import { pipeline } from "node:stream/promises";
14
+ import extractZip from "extract-zip";
15
+ import { extract } from "tar";
16
+ //#region src/cli/electron-builder.ts
17
+ /**
18
+ * Electron 壳打包(静态、无签名):electron-builder 的 `--dir` 语义 —— 产出
19
+ * 未打包的应用目录(macOS `.app` / Linux 目录 / Windows 目录),不做签名、
20
+ * notarize 或安装器,无需开发者账号。后端运行时(随包的 Node.js)与 profile
21
+ * 种子作为 `extraResources` 一起打包。
22
+ *
23
+ * 这里直接调用 electron-builder 的 Node API,壳配置以 TS 收敛在工具内部
24
+ * (不再有独立的 `electron-builder.config.mjs`,也不需要经环境变量回读工作区)。
25
+ */
26
+ /** 已安装 Electron 的版本;本机存在未打包发行版时一并给出(作为打包源)。 */
27
+ function installedElectron() {
28
+ const manifestPath = createRequire(import.meta.url).resolve("electron/package.json");
29
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
30
+ if (typeof manifest.version !== "string") throw new Error("dsh-desktopify: installed electron has no version");
31
+ const dist = join(dirname(manifestPath), "dist");
32
+ return {
33
+ version: manifest.version,
34
+ ...existsSync(dist) ? { dist } : {}
35
+ };
36
+ }
37
+ /**
38
+ * electron-builder 的 app 目录:只放运行时文件(`dist/` 壳产物 + 最小
39
+ * manifest)。工具包自身的 package.json 声明了 electron / electron-builder
40
+ * 等构建依赖,而 electron-builder 拒绝把它们当运行时依赖,因此壳以独立
41
+ * 目录打包;app 版本取目标工作区的版本。
42
+ */
43
+ function prepareShellAppDirectory(options) {
44
+ const { appRoot, buildRoot, appConfig } = options;
45
+ const appDir = join(buildRoot, "shell");
46
+ rmSync(appDir, {
47
+ recursive: true,
48
+ force: true
49
+ });
50
+ mkdirSync(appDir, { recursive: true });
51
+ cpSync(join(appRoot, "dist"), join(appDir, "dist"), { recursive: true });
52
+ const renderer = join(appRoot, "renderer");
53
+ if (existsSync(renderer)) cpSync(renderer, join(appDir, "renderer"), { recursive: true });
54
+ const tool = JSON.parse(readFileSync(join(appRoot, "package.json"), "utf8"));
55
+ writeFileSync(join(appDir, "package.json"), `${JSON.stringify({
56
+ name: tool.name ?? "dsh-desktop-shell",
57
+ version: appConfig.version,
58
+ ...tool.description === void 0 ? {} : { description: tool.description },
59
+ ...tool.author === void 0 ? {} : { author: tool.author },
60
+ main: "dist/index.mjs"
61
+ }, void 0, 2)}\n`);
62
+ return appDir;
63
+ }
64
+ /** 打包壳应用,返回 electron-builder 写出的产物路径。 */
65
+ async function buildDesktopApp(options) {
66
+ const { buildRoot, appConfig, icons, dir } = options;
67
+ const appDir = prepareShellAppDirectory(options);
68
+ const electron = installedElectron();
69
+ console.log(`desktop bundle: electron-builder appId=${appConfig.id} productName=${appConfig.name} version=${appConfig.version} electron=${electron.version}`);
70
+ const config = {
71
+ appId: appConfig.id,
72
+ productName: appConfig.name,
73
+ artifactName: "${productName}-${version}-${os}-${arch}.${ext}",
74
+ electronVersion: electron.version,
75
+ ...electron.dist === void 0 ? {} : { electronDist: electron.dist },
76
+ directories: { output: join(buildRoot, "artifacts") },
77
+ asar: true,
78
+ files: [
79
+ "dist/**",
80
+ "renderer/**/*",
81
+ "package.json"
82
+ ],
83
+ extraResources: [
84
+ {
85
+ from: join(buildRoot, "runtime"),
86
+ to: "runtime"
87
+ },
88
+ {
89
+ from: join(buildRoot, "seed"),
90
+ to: "seed"
91
+ },
92
+ {
93
+ from: join(buildRoot, "runtime", "appconfig.json"),
94
+ to: "appconfig.json"
95
+ }
96
+ ],
97
+ mac: {
98
+ category: "public.app-category.developer-tools",
99
+ identity: null,
100
+ target: ["dir"],
101
+ ...icons.mac === void 0 ? {} : { icon: icons.mac }
102
+ },
103
+ linux: {
104
+ category: "Development",
105
+ target: ["dir"],
106
+ ...icons.linux === void 0 ? {} : { icon: icons.linux }
107
+ },
108
+ win: {
109
+ target: ["dir"],
110
+ ...icons.win === void 0 ? {} : { icon: icons.win }
111
+ }
112
+ };
113
+ return build({
114
+ projectDir: appDir,
115
+ config,
116
+ publish: "never",
117
+ dir
118
+ });
119
+ }
120
+ //#endregion
121
+ //#region src/cli/icon.ts
122
+ /**
123
+ * Prepare platform icons from the workspace `dsh.desktop.icon` (an SVG):
124
+ * rasterize with sharp into the sizes each platform needs, and build the
125
+ * macOS `.icns` with `iconutil`. Results are written under the build root's
126
+ * `icon/` directory and summarized in `icon.json` for electron-builder.
127
+ */
128
+ /** macOS iconset sizes: [pixel, filename]. */
129
+ const MAC_ICONSET_SIZES = [
130
+ [16, "icon_16x16.png"],
131
+ [32, "icon_16x16@2x.png"],
132
+ [32, "icon_32x32.png"],
133
+ [64, "icon_32x32@2x.png"],
134
+ [128, "icon_128x128.png"],
135
+ [256, "icon_128x128@2x.png"],
136
+ [256, "icon_256x256.png"],
137
+ [512, "icon_256x256@2x.png"],
138
+ [512, "icon_512x512.png"],
139
+ [1024, "icon_512x512@2x.png"]
140
+ ];
141
+ /** Transparent canvas letterboxing(sharp `fit: "contain"` 默认黑底,SVG 上下会透出黑边)。 */
142
+ const CONTAIN_BACKGROUND = {
143
+ r: 0,
144
+ g: 0,
145
+ b: 0,
146
+ alpha: 0
147
+ };
148
+ /**
149
+ * Rasterize the workspace icon for the current platform.
150
+ * @param workspace - app workspace directory.
151
+ * @param buildRootDir - the tool's build cache root.
152
+ * @param icon - `dsh.desktop.icon` value (relative to the workspace).
153
+ * @returns the prepared icon paths (empty when no icon is declared).
154
+ */
155
+ async function prepareIcons(workspace, buildRootDir, icon) {
156
+ if (icon === void 0 || icon === "") return {};
157
+ const source = resolve(workspace, icon);
158
+ if (!existsSync(source)) throw new Error(`desktop bundle: workspace icon ${source} does not exist`);
159
+ const outDir = join(buildRootDir, "icon");
160
+ rmSync(outDir, {
161
+ recursive: true,
162
+ force: true
163
+ });
164
+ mkdirSync(outDir, { recursive: true });
165
+ const png512 = join(outDir, "icon-512.png");
166
+ await sharp(source).resize(512, 512, {
167
+ fit: "contain",
168
+ background: CONTAIN_BACKGROUND
169
+ }).png().toFile(png512);
170
+ let icons = {
171
+ linux: png512,
172
+ win: png512
173
+ };
174
+ if (process.platform === "darwin") {
175
+ const iconset = join(outDir, "icon.iconset");
176
+ mkdirSync(iconset, { recursive: true });
177
+ for (const [size, name] of MAC_ICONSET_SIZES) await sharp(source).resize(size, size, {
178
+ fit: "contain",
179
+ background: CONTAIN_BACKGROUND
180
+ }).png().toFile(join(iconset, name));
181
+ const icns = join(outDir, "icon.icns");
182
+ execFileSync("iconutil", [
183
+ "-c",
184
+ "icns",
185
+ iconset,
186
+ "-o",
187
+ icns
188
+ ]);
189
+ icons = {
190
+ ...icons,
191
+ mac: icns
192
+ };
193
+ }
194
+ writeFileSync(join(outDir, "icon.json"), `${JSON.stringify(icons, void 0, 2)}\n`);
195
+ return icons;
196
+ }
197
+ /** Official packages the tool injects into every runtime project it assembles. */
198
+ const OFFICIAL_RUNTIME_PACKAGES = [
199
+ "@deepseek-ai/dsh",
200
+ "@deepseek-ai/dsh-desktop-host",
201
+ ...[
202
+ "@deepseek-ai/cordis-plugin-group",
203
+ "@deepseek-ai/dsh-authorization",
204
+ "@deepseek-ai/dsh-bash-local",
205
+ "@deepseek-ai/dsh-code-runtime",
206
+ "@deepseek-ai/dsh-compaction",
207
+ "@deepseek-ai/dsh-fs",
208
+ "@deepseek-ai/dsh-hook-protocol",
209
+ "@deepseek-ai/dsh-jobs",
210
+ "@deepseek-ai/dsh-output-retention",
211
+ "@deepseek-ai/dsh-sandbox",
212
+ "@deepseek-ai/dsh-sdk-protocol",
213
+ "@deepseek-ai/dsh-session-query",
214
+ "@deepseek-ai/dsh-session-telemetry",
215
+ "@deepseek-ai/dsh-session-title-llm",
216
+ "@deepseek-ai/dsh-shell",
217
+ "@deepseek-ai/dsh-spill",
218
+ "@deepseek-ai/dsh-subagent-in-process-driver",
219
+ "@deepseek-ai/dsh-util-time",
220
+ "@deepseek-ai/dsh-util-workspace-path",
221
+ "@deepseek-ai/dsh-workflow"
222
+ ]
223
+ ];
224
+ /** Official profile bundles merged ahead of the app's own bundles. */
225
+ const OFFICIAL_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"];
226
+ //#endregion
227
+ //#region src/cli/workspace.ts
228
+ /**
229
+ * Shared workspace resolution for the desktopify scripts: the workspace is
230
+ * never hardcoded — it comes from `DSH_DESKTOP_WORKSPACE` (or the CLI
231
+ * positional argument, which the CLI forwards through that variable) and
232
+ * defaults to the current directory. Also carries the app-facing contract:
233
+ * `dsh.desktop` identity and the merged profile bundle list.
234
+ * @module @morlay/dsh-desktopify
235
+ */
236
+ /** The dsh profile the desktop shell hosts (upstream desktop semantics). */
237
+ const PROFILE_NAME = "desktop";
238
+ /** Resolve the app workspace directory (default: the current directory). */
239
+ function resolveWorkspace() {
240
+ return resolve(process.cwd());
241
+ }
242
+ /** Read and validate the workspace manifest. */
243
+ function workspaceManifest(workspace) {
244
+ const value = JSON.parse(readFileSync(join(workspace, "package.json"), "utf8"));
245
+ if (typeof value.name !== "string" || value.name === "") throw new Error(`dsh-desktopify: workspace ${workspace} has no package name`);
246
+ return {
247
+ ...value,
248
+ name: value.name
249
+ };
250
+ }
251
+ /**
252
+ * The `@deepseek-ai/dsh` dependency spec the app targets (`dsh.version`): a
253
+ * concrete release (`0.1.5-rc.1`), a range, or the `workspace:` protocol when
254
+ * the app lives in the same workspace as the vendored dsh.
255
+ */
256
+ function dshVersion(manifest) {
257
+ const version = manifest.dsh?.version;
258
+ if (version === void 0) return void 0;
259
+ if (typeof version !== "string" || version === "") throw new Error(`dsh-desktopify: workspace ${String(manifest.name)} has an invalid dsh.version (expected a dependency spec such as "0.1.5-rc.1" or "workspace:^")`);
260
+ return version;
261
+ }
262
+ /** The app's `dsh.desktop` configuration with tool defaults. */
263
+ function desktopConfig(manifest) {
264
+ const desktop = manifest.dsh?.desktop ?? {};
265
+ const window = desktop.window ?? {};
266
+ return {
267
+ id: desktop.id ?? "ai.deepseek.dsh.custom",
268
+ version: manifest.version ?? "0.0.1",
269
+ dshHome: desktop.dshHome ?? "xdg",
270
+ window: {
271
+ width: window.width ?? 1280,
272
+ height: window.height ?? 800,
273
+ minWidth: window.minWidth ?? 800,
274
+ minHeight: window.minHeight ?? 600
275
+ },
276
+ ...desktop.icon === void 0 ? {} : { icon: desktop.icon }
277
+ };
278
+ }
279
+ /** The app's declared profile bundles (validated). */
280
+ function appProfileBundles(manifest) {
281
+ const bundles = manifest.dsh?.profile?.bundles;
282
+ if (!Array.isArray(bundles) || !bundles.every((bundle) => typeof bundle === "string")) throw new Error(`dsh-desktopify: workspace ${String(manifest.name)} has no dsh.profile.bundles`);
283
+ return bundles;
284
+ }
285
+ /** Official bundles merged ahead of the app's own bundles. */
286
+ function mergedProfileBundles(manifest) {
287
+ return [...OFFICIAL_PROFILE_BUNDLES, ...appProfileBundles(manifest)];
288
+ }
289
+ /** Find the pnpm workspace root above a directory. */
290
+ function findWorkspaceRoot(workspace) {
291
+ let current = resolve(workspace);
292
+ for (;;) {
293
+ if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
294
+ const parent = dirname(current);
295
+ if (parent === current) throw new Error(`dsh-desktopify: no pnpm-workspace.yaml found above ${workspace}`);
296
+ current = parent;
297
+ }
298
+ }
299
+ /** The tool's build cache root inside the target workspace. */
300
+ function buildRoot(workspace) {
301
+ return join(workspace, "node_modules", ".dsh-desktopify");
302
+ }
303
+ //#endregion
304
+ //#region src/cli/prepare-runtime.ts
305
+ /**
306
+ * Download and verify the upstream Node.js runtime for the packaged backend.
307
+ * The backend runs under this bundled Node executable; the shell spawns it
308
+ * directly (no user Node required). Downloads are cached under the target
309
+ * workspace's `node_modules/.dsh-desktopify/downloads` and verified against
310
+ * the official SHASUMS256.
311
+ */
312
+ const NODE_VERSION = "24.17.0";
313
+ function target() {
314
+ const rawPlatform = process.env.DSH_DESKTOP_TARGET_PLATFORM ?? process.platform;
315
+ const rawArch = process.env.DSH_DESKTOP_TARGET_ARCH ?? process.arch;
316
+ const platform = rawPlatform === "win32" ? "win" : rawPlatform;
317
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win") throw new Error(`desktop runtime: unsupported platform ${rawPlatform}`);
318
+ if (rawArch !== "arm64" && rawArch !== "x64") throw new Error(`desktop runtime: unsupported architecture ${rawArch}`);
319
+ return {
320
+ platform,
321
+ arch: rawArch
322
+ };
323
+ }
324
+ async function download(url, path) {
325
+ const response = await fetch(url);
326
+ if (!response.ok) throw new Error(`desktop runtime: ${url} returned HTTP ${String(response.status)}`);
327
+ writeFileSync(path, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
328
+ }
329
+ async function prepareNode(platform, arch, buildRootDir) {
330
+ const extension = platform === "win" ? "zip" : "tar.gz";
331
+ const folder = `node-v${NODE_VERSION}-${platform}-${arch}`;
332
+ const archiveName = `${folder}.${extension}`;
333
+ const releaseRoot = `https://nodejs.org/download/release/v${NODE_VERSION}`;
334
+ const downloadRoot = join(buildRootDir, "downloads");
335
+ const archive = join(downloadRoot, archiveName);
336
+ const sums = join(downloadRoot, `node-v${NODE_VERSION}-SHASUMS256.txt`);
337
+ if (!existsSync(archive)) await download(`${releaseRoot}/${archiveName}`, archive);
338
+ if (!existsSync(sums)) await download(`${releaseRoot}/SHASUMS256.txt`, sums);
339
+ const line = (await readFile(sums, "utf8")).split(/\r?\n/u).find((candidate) => candidate.endsWith(` ${archiveName}`));
340
+ if (line === void 0) throw new Error(`desktop runtime: ${archiveName} is absent from Node.js SHASUMS256.txt`);
341
+ const expected = line.split(/\s+/u)[0];
342
+ if (createHash("sha256").update(await readFile(archive)).digest("hex") !== expected) throw new Error(`desktop runtime: checksum mismatch for ${archiveName}`);
343
+ const extraction = join(buildRootDir, "node-extract");
344
+ rmSync(extraction, {
345
+ recursive: true,
346
+ force: true
347
+ });
348
+ mkdirSync(extraction, { recursive: true });
349
+ if (platform === "win") await extractZip(archive, { dir: extraction });
350
+ else await extract({
351
+ cwd: extraction,
352
+ file: archive
353
+ });
354
+ const source = join(extraction, folder, platform === "win" ? "node.exe" : "bin/node");
355
+ const destinationRoot = join(buildRootDir, "runtime", "node");
356
+ const destination = join(destinationRoot, platform === "win" ? "node.exe" : "node");
357
+ rmSync(destinationRoot, {
358
+ recursive: true,
359
+ force: true
360
+ });
361
+ mkdirSync(destinationRoot, { recursive: true });
362
+ await pipeline(createReadStream(source), createWriteStream(destination, { flags: "wx" }));
363
+ if (platform !== "win") await chmod(destination, 493);
364
+ if (platform === (process.platform === "win32" ? "win" : process.platform) && (arch === process.arch || platform === "darwin" && arch === "x64" && process.arch === "arm64")) {
365
+ const result = spawnSync(destination, ["--version"], { encoding: "utf8" });
366
+ if (result.error !== void 0 || result.status !== 0 || result.stdout.trim() !== `v${NODE_VERSION}`) {
367
+ const detail = result.error?.message ?? result.signal ?? result.stderr.trim();
368
+ const outcome = detail === "" ? `exit ${String(result.status)}` : detail;
369
+ throw new Error(`desktop runtime: prepared Node.js ${NODE_VERSION} failed executable verification: ${outcome}`);
370
+ }
371
+ }
372
+ rmSync(extraction, {
373
+ recursive: true,
374
+ force: true
375
+ });
376
+ }
377
+ async function runPrepareRuntime(options) {
378
+ const { platform, arch } = target();
379
+ const buildRootDir = buildRoot(resolve(options.workspace ?? resolveWorkspace()));
380
+ const runtimeRoot = join(buildRootDir, "runtime");
381
+ mkdirSync(join(buildRootDir, "downloads"), { recursive: true });
382
+ mkdirSync(runtimeRoot, { recursive: true });
383
+ await prepareNode(platform, arch, buildRootDir);
384
+ writeFileSync(join(runtimeRoot, "versions.json"), `${JSON.stringify({
385
+ schemaVersion: 1,
386
+ node: NODE_VERSION
387
+ }, void 0, 2)}\n`);
388
+ console.log(`desktop runtime: prepared Node.js ${NODE_VERSION} for ${platform}-${arch}`);
389
+ }
390
+ //#endregion
391
+ //#region src/cli/official-deps.ts
392
+ /**
393
+ * Official `@deepseek-ai/*` dependency resolution for one target workspace.
394
+ *
395
+ * The app declares only its own dependencies plus `dsh.version` (the
396
+ * `@deepseek-ai/dsh` dependency spec it targets); the tool maintains the
397
+ * official surface (`../official.ts`). Specs are derived from the packages
398
+ * actually installed — the app workspace first, then the tool's own install —
399
+ * so nothing hardcodes the `workspace:` protocol and an app outside this
400
+ * repository resolves the same way:
401
+ * - `@deepseek-ai/dsh` → `dsh.version` when declared (concrete version,
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);
406
+ * - every other official package → `^<resolved>`.
407
+ *
408
+ * `hasTsx` reports whether the workspace can load TypeScript sources directly;
409
+ * the tool only injects `--import=tsx/esm` when it can.
410
+ * @module @morlay/dsh-desktopify
411
+ */
412
+ /** Installation anchor the upstream desktop host resolves profile bundles from. */
413
+ const DSH_PACKAGE = "@deepseek-ai/dsh";
414
+ /** Private byte-pipe backend installed beside dsh; never published to a registry. */
415
+ const DESKTOP_HOST_PACKAGE = "@deepseek-ai/dsh-desktop-host";
416
+ /**
417
+ * `node_modules` roots searched for the official surface, in priority order:
418
+ * the app's own install, its pnpm virtual store, the tool's install, and the
419
+ * tool's surrounding install (the repository store in-tree, the app's
420
+ * `node_modules` when the tool is installed as a dependency).
421
+ */
422
+ function officialSearchDirs(input) {
423
+ return [.../* @__PURE__ */ new Set([
424
+ join(input.workspace, "node_modules"),
425
+ join(input.workspaceRoot, "node_modules", ".pnpm", "node_modules"),
426
+ join(input.toolRoot, "node_modules"),
427
+ resolve(input.toolRoot, "..", ".."),
428
+ resolve(input.toolRoot, "..", "..", "node_modules", ".pnpm", "node_modules")
429
+ ])];
430
+ }
431
+ /** Resolve one official package from the workspace, then the tool. */
432
+ function resolveOfficialPackage(packageName, input) {
433
+ for (const modulesDir of officialSearchDirs(input)) {
434
+ const dir = join(modulesDir, ...packageName.split("/"));
435
+ const manifestPath = join(dir, "package.json");
436
+ if (!existsSync(manifestPath)) continue;
437
+ try {
438
+ const value = JSON.parse(readFileSync(manifestPath, "utf8"));
439
+ if (typeof value.version === "string" && value.version !== "") return {
440
+ dir: realpathSync(dir),
441
+ version: value.version
442
+ };
443
+ } catch {}
444
+ }
445
+ }
446
+ /**
447
+ * The tool's own `node_modules` root holding the official surface, used as a
448
+ * dev fallback for an app that declares only `dsh.version` and has not
449
+ * installed the official packages itself. Prefers the surrounding store (the
450
+ * repository virtual store in-tree, the app store when installed as a
451
+ * dependency) over the tool's direct dependencies.
452
+ */
453
+ function toolModulesDir(input) {
454
+ for (const modulesDir of [
455
+ resolve(input.toolRoot, "..", "..", "node_modules", ".pnpm", "node_modules"),
456
+ resolve(input.toolRoot, "..", ".."),
457
+ join(input.toolRoot, "node_modules")
458
+ ]) if (existsSync(join(modulesDir, "@deepseek-ai"))) return modulesDir;
459
+ }
460
+ /**
461
+ * Dependency specs for the complete official surface of one workspace.
462
+ * Throws when a package cannot be located, naming the missing package.
463
+ */
464
+ function officialDependencySpecs(input) {
465
+ const specs = {};
466
+ const missing = [];
467
+ for (const packageName of OFFICIAL_RUNTIME_PACKAGES) {
468
+ const resolved = resolveOfficialPackage(packageName, input);
469
+ if (packageName === "@deepseek-ai/dsh" && input.dshVersion !== void 0) {
470
+ specs[packageName] = input.dshVersion;
471
+ continue;
472
+ }
473
+ if (resolved === void 0) {
474
+ missing.push(packageName);
475
+ continue;
476
+ }
477
+ if (packageName === "@deepseek-ai/dsh-desktop-host") {
478
+ specs[packageName] = `link:${resolved.dir}`;
479
+ continue;
480
+ }
481
+ specs[packageName] = `^${resolved.version}`;
482
+ }
483
+ 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
+ return specs;
485
+ }
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
+ /**
492
+ * Dependency spec for the private desktop host in a `pnpm deploy` closure.
493
+ *
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.
499
+ */
500
+ 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}`;
504
+ const staged = join(input.workspace, ".dsh-desktopify-host");
505
+ rmSync(staged, {
506
+ recursive: true,
507
+ force: true
508
+ });
509
+ cpSync(host.dir, staged, {
510
+ recursive: true,
511
+ dereference: true,
512
+ filter: (source) => {
513
+ const rel = relative(host.dir, source);
514
+ return rel === "" || !rel.split(/[\\/]/u).includes("node_modules");
515
+ }
516
+ });
517
+ const manifestPath = join(staged, "package.json");
518
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
519
+ for (const field of ["dependencies", "peerDependencies"]) {
520
+ const dependencies = manifest[field];
521
+ if (typeof dependencies !== "object" || dependencies === null) continue;
522
+ for (const [name, spec] of Object.entries(dependencies)) {
523
+ if (typeof spec !== "string" || !spec.startsWith("workspace:")) continue;
524
+ const resolved = resolveOfficialPackage(name, input);
525
+ const configured = specs[name];
526
+ const replacement = configured === void 0 || configured.startsWith("workspace:") ? resolved === void 0 ? void 0 : `^${resolved.version}` : configured;
527
+ if (replacement !== void 0 && !replacement.startsWith("link:")) dependencies[name] = replacement;
528
+ }
529
+ }
530
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, void 0, 2)}\n`);
531
+ return "file:./.dsh-desktopify-host";
532
+ }
533
+ /** Whether the workspace can load TypeScript sources through tsx. */
534
+ function hasTsx(workspace, workspaceRoot) {
535
+ for (const dir of [workspace, workspaceRoot]) try {
536
+ createRequire(join(dir, "package.json")).resolve("tsx/esm");
537
+ return true;
538
+ } catch {}
539
+ return false;
540
+ }
541
+ //#endregion
542
+ //#region src/cli/prepare-seed.ts
543
+ /**
544
+ * Build the packaged profile seed: export the workspace's production
545
+ * dependency closure with `pnpm deploy`, then assemble `dsh-home/profiles/desktop`
546
+ * from the workspace whitelist (package.json, cordis.patch.yml, declared
547
+ * `files`) plus the flattened closure as `node_modules`, stamped with a
548
+ * `.seed-hash` fingerprint. The shell forces the runtime home's profile to
549
+ * this seed at startup.
550
+ *
551
+ * The fingerprint covers what can change under a bundled application without
552
+ * a version bump: the whitelisted workspace content, the root lockfile (the
553
+ * resolved dependency graph), and the closure content of every local source
554
+ * package — `workspace:^` dependencies resolve to the workspace tree
555
+ * (`@morlay/*`, vendored `@deepseek-ai/*`), so their sources can change while
556
+ * every version string stays the same.
557
+ *
558
+ * The official `@deepseek-ai/*` dependency surface is maintained by the tool:
559
+ * the official packages are injected into the workspace manifest before
560
+ * deploy (and removed afterwards), so the deploy closure carries the
561
+ * complete official tree (dsh, dsh-base, dsh-web-app, the peer packages).
562
+ * The closure is then flattened with pnpm's `nodeLinker: hoisted` — a
563
+ * traditional node_modules layout with every package at the top level, no
564
+ * virtual store, no external links — so the seed is fully self-contained.
565
+ */
566
+ /** 工具包根(源码形态 `src/cli` 与构建形态 `dist/cli` 同深度)。 */
567
+ const APP_ROOT$4 = resolve(import.meta.dirname, "..", "..");
568
+ function runPnpm(args, cwd) {
569
+ return new Promise((resolvePromise, reject) => {
570
+ const child = spawn("pnpm", args, {
571
+ cwd,
572
+ stdio: "inherit"
573
+ });
574
+ child.once("error", reject);
575
+ child.once("close", (code, signal) => {
576
+ if (code === 0) resolvePromise();
577
+ else reject(/* @__PURE__ */ new Error(`desktop seed: pnpm ${args.join(" ")} exited with ${String(code ?? signal)}`));
578
+ });
579
+ });
580
+ }
581
+ /**
582
+ * Export the workspace production closure into the deploy staging directory.
583
+ * The official packages are injected into the workspace manifest (specs
584
+ * derived from `dsh.version` and the installed packages, never a hardcoded
585
+ * `workspace:` protocol) and the root lockfile is refreshed
586
+ * (`--lockfile-only`) so the deploy resolves them; both are restored
587
+ * afterwards. The deploy closure is then flattened: `nodeLinker: hoisted`
588
+ * reinstalls it as a traditional node_modules layout (every package at the
589
+ * top level, no virtual store, no external links).
590
+ */
591
+ async function deployClosure(workspace, name, destination, input) {
592
+ const root = findWorkspaceRoot(workspace);
593
+ const manifestPath = join(workspace, "package.json");
594
+ const lockfilePath = join(root, "pnpm-lock.yaml");
595
+ const originalManifest = readFileSync(manifestPath, "utf8");
596
+ const originalLockfile = readFileSync(lockfilePath, "utf8");
597
+ const manifest = JSON.parse(originalManifest);
598
+ const specs = officialDependencySpecs(input);
599
+ specs[DESKTOP_HOST_PACKAGE] = stageDesktopHost(input, specs);
600
+ const dependencies = {
601
+ ...manifest.dependencies,
602
+ ...specs
603
+ };
604
+ writeFileSync(manifestPath, `${JSON.stringify({
605
+ ...manifest,
606
+ dependencies
607
+ }, void 0, 2)}\n`);
608
+ try {
609
+ await runPnpm(["install", "--lockfile-only"], root);
610
+ rmSync(destination, {
611
+ recursive: true,
612
+ force: true
613
+ });
614
+ mkdirSync(destination, { recursive: true });
615
+ await runPnpm([
616
+ ...root !== resolve(workspace) ? ["--filter", name] : [],
617
+ "deploy",
618
+ "--prod",
619
+ "--ignore-scripts",
620
+ destination
621
+ ], root);
622
+ if (!existsSync(join(destination, "node_modules"))) throw new Error(`desktop seed: pnpm deploy did not produce ${join(destination, "node_modules")}`);
623
+ const workspaceFile = join(destination, "pnpm-workspace.yaml");
624
+ if (existsSync(workspaceFile)) {
625
+ let content = readFileSync(workspaceFile, "utf8").replaceAll(": set this to true or false", ": true");
626
+ if (!content.includes("minimumReleaseAge")) content = `${content.trimEnd()}\nminimumReleaseAge: 0\n`;
627
+ if (!content.includes("nodeLinker")) content = `${content.trimEnd()}\nnodeLinker: hoisted\n`;
628
+ writeFileSync(workspaceFile, content);
629
+ }
630
+ await runPnpm([
631
+ "install",
632
+ "--no-frozen-lockfile",
633
+ "--ignore-scripts"
634
+ ], destination);
635
+ } finally {
636
+ writeFileSync(manifestPath, originalManifest);
637
+ writeFileSync(lockfilePath, originalLockfile);
638
+ rmSync(join(workspace, ".dsh-desktopify-host"), {
639
+ recursive: true,
640
+ force: true
641
+ });
642
+ }
643
+ }
644
+ /** Whitelisted workspace files that enter the seed and the fingerprint. */
645
+ function seedEntries(workspace, manifest) {
646
+ const entries = /* @__PURE__ */ new Set(["package.json", "cordis.patch.yml"]);
647
+ for (const file of manifest.files ?? []) {
648
+ const cleaned = file.replaceAll("\\", "/").replace(/^\.\//u, "");
649
+ if (cleaned === "" || cleaned === "." || cleaned.startsWith("/") || cleaned.startsWith("../")) continue;
650
+ entries.add(cleaned);
651
+ }
652
+ return [...entries].sort();
653
+ }
654
+ /** Directories never walked when hashing seed content (installs, caches, VCS). */
655
+ const TREE_SKIP_DIRS = /* @__PURE__ */ new Set([
656
+ ".bin",
657
+ ".cache",
658
+ ".git",
659
+ ".dsh-store",
660
+ ".pnpm-store",
661
+ ".turbo",
662
+ "node_modules"
663
+ ]);
664
+ /** Depth limit for the local package scan (vendored trees nest a few levels). */
665
+ const LOCAL_PACKAGE_SCAN_DEPTH = 6;
666
+ /** Feed one path (file, or directory expanded recursively) into the hash. */
667
+ function hashPath(hash, root, relativePath) {
668
+ const path = join(root, ...relativePath.split("/"));
669
+ if (!existsSync(path)) return;
670
+ const stat = statSync(path);
671
+ if (stat.isDirectory()) {
672
+ for (const entry of readdirSync(path).sort()) {
673
+ if (TREE_SKIP_DIRS.has(entry)) continue;
674
+ hashPath(hash, root, `${relativePath}/${entry}`);
675
+ }
676
+ return;
677
+ }
678
+ if (!stat.isFile()) return;
679
+ hash.update(relativePath);
680
+ hash.update("\0");
681
+ hash.update(readFileSync(path));
682
+ hash.update("\0");
683
+ }
684
+ /** Read a package name, tolerating unreadable or nameless manifests. */
685
+ function packageName(manifestPath) {
686
+ try {
687
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
688
+ return typeof manifest.name === "string" && manifest.name !== "" ? manifest.name : void 0;
689
+ } catch {
690
+ return;
691
+ }
692
+ }
693
+ /** Local source packages under the workspace root: package name → directory. */
694
+ function localPackageDirs(root) {
695
+ const found = /* @__PURE__ */ new Map();
696
+ const visit = (dir, depth) => {
697
+ if (depth > LOCAL_PACKAGE_SCAN_DEPTH) return;
698
+ let entries;
699
+ try {
700
+ entries = readdirSync(dir, { withFileTypes: true });
701
+ } catch {
702
+ return;
703
+ }
704
+ for (const entry of entries) {
705
+ if (!entry.isDirectory() || entry.name.startsWith(".") || TREE_SKIP_DIRS.has(entry.name)) continue;
706
+ const child = join(dir, entry.name);
707
+ const manifestPath = join(child, "package.json");
708
+ if (existsSync(manifestPath)) {
709
+ const name = packageName(manifestPath);
710
+ if (name !== void 0 && !found.has(name)) found.set(name, child);
711
+ }
712
+ visit(child, depth + 1);
713
+ }
714
+ };
715
+ visit(root, 0);
716
+ return found;
717
+ }
718
+ /** Top-level package names of a flattened (hoisted) closure. */
719
+ function closurePackageNames(modulesDir) {
720
+ const names = [];
721
+ for (const entry of readdirSync(modulesDir, { withFileTypes: true })) {
722
+ if (entry.name.startsWith(".") || TREE_SKIP_DIRS.has(entry.name)) continue;
723
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
724
+ if (entry.name.startsWith("@")) {
725
+ for (const scoped of readdirSync(join(modulesDir, entry.name)).sort()) if (!scoped.startsWith(".")) names.push(`${entry.name}/${scoped}`);
726
+ continue;
727
+ }
728
+ names.push(entry.name);
729
+ }
730
+ return names.sort();
731
+ }
732
+ /**
733
+ * Seed fingerprint: whitelisted workspace content, the root lockfile (the
734
+ * resolved dependency graph), and the closure content of every local source
735
+ * package — `workspace:^` dependencies keep their version while their content
736
+ * changes, so the closure itself is the only reliable signal.
737
+ */
738
+ function seedFingerprint(input) {
739
+ const hash = createHash("sha256");
740
+ hash.update("workspace\0");
741
+ for (const entry of input.entries) hashPath(hash, input.workspace, entry);
742
+ hash.update("lockfile\0");
743
+ const lockfile = join(input.workspaceRoot, "pnpm-lock.yaml");
744
+ if (existsSync(lockfile)) {
745
+ hash.update(readFileSync(lockfile));
746
+ hash.update("\0");
747
+ }
748
+ hash.update("local-closure\0");
749
+ const local = localPackageDirs(input.workspaceRoot);
750
+ for (const name of closurePackageNames(input.closureModulesDir)) {
751
+ if (!local.has(name)) continue;
752
+ hash.update(`${name}\0`);
753
+ hashPath(hash, input.closureModulesDir, name);
754
+ }
755
+ return hash.digest("hex");
756
+ }
757
+ async function runPrepareSeed(options) {
758
+ const workspace = resolve(options.workspace ?? resolveWorkspace());
759
+ const manifest = workspaceManifest(workspace);
760
+ const workspaceRoot = findWorkspaceRoot(workspace);
761
+ const dshVersion$2 = dshVersion(manifest);
762
+ const input = {
763
+ workspace,
764
+ workspaceRoot,
765
+ toolRoot: APP_ROOT$4,
766
+ ...dshVersion$2 === void 0 ? {} : { dshVersion: dshVersion$2 }
767
+ };
768
+ const buildRootDir = buildRoot(workspace);
769
+ const seedOutputRoot = join(buildRootDir, "seed");
770
+ const deployRoot = join(buildRootDir, "deploy");
771
+ const entries = seedEntries(workspace, manifest);
772
+ console.log(`desktop seed: workspace ${workspace} (${manifest.name})`);
773
+ console.log(`desktop seed: whitelist ${entries.join(", ")}`);
774
+ await deployClosure(workspace, manifest.name, deployRoot, input);
775
+ const fingerprint = seedFingerprint({
776
+ workspace,
777
+ workspaceRoot,
778
+ entries,
779
+ closureModulesDir: join(deployRoot, "node_modules")
780
+ });
781
+ rmSync(seedOutputRoot, {
782
+ recursive: true,
783
+ force: true
784
+ });
785
+ const profileDir = join(seedOutputRoot, "profiles", PROFILE_NAME);
786
+ mkdirSync(profileDir, { recursive: true });
787
+ for (const entry of entries) {
788
+ const source = join(workspace, ...entry.split("/"));
789
+ if (!existsSync(source)) continue;
790
+ const target = join(profileDir, ...entry.split("/"));
791
+ mkdirSync(dirname(target), { recursive: true });
792
+ cpSync(source, target, { recursive: true });
793
+ }
794
+ writeFileSync(join(profileDir, "package.json"), `${JSON.stringify({
795
+ ...JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")),
796
+ dsh: {
797
+ ...manifest.dsh,
798
+ profile: {
799
+ ...manifest.dsh?.profile,
800
+ bundles: mergedProfileBundles(manifest)
801
+ }
802
+ }
803
+ }, void 0, 2)}\n`);
804
+ cpSync(join(deployRoot, "node_modules"), join(profileDir, "node_modules"), {
805
+ recursive: true,
806
+ verbatimSymlinks: true
807
+ });
808
+ switchToPublishedExports(join(profileDir, "node_modules"));
809
+ writeFileSync(join(profileDir, SEED_HASH_NAME), fingerprint);
810
+ console.log(`desktop seed: wrote ${seedOutputRoot} (${fingerprint.slice(0, 12)})`);
811
+ }
812
+ /**
813
+ * Rewrite `@morlay/*` package exports to their published dist form. The
814
+ * flattened closure has every package at the top level.
815
+ */
816
+ function switchToPublishedExports(modulesDir) {
817
+ const scoped = join(modulesDir, "@morlay");
818
+ if (!existsSync(scoped)) return;
819
+ for (const entry of readdirSync(scoped, { withFileTypes: true })) {
820
+ if (!entry.isDirectory()) continue;
821
+ const manifestPath = join(scoped, entry.name, "package.json");
822
+ if (!existsSync(manifestPath)) continue;
823
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
824
+ const published = manifest.publishConfig?.exports;
825
+ if (published === void 0) continue;
826
+ writeFileSync(manifestPath, `${JSON.stringify({
827
+ ...manifest,
828
+ exports: published
829
+ }, void 0, 2)}\n`);
830
+ }
831
+ }
832
+ //#endregion
833
+ //#region src/cli/shell.ts
834
+ /** 工具包根(源码形态 `src/cli` 与构建形态 `dist/cli` 同深度)。 */
835
+ const APP_ROOT$3 = resolve(import.meta.dirname, "..", "..");
836
+ /** 壳入口产物(Electron 加载的主进程)。 */
837
+ const SHELL_ENTRY = join(APP_ROOT$3, "dist", "index.mjs");
838
+ /** 构建壳产物;发布形态没有源码时直接使用随包构建结果。 */
839
+ async function buildShell() {
840
+ if (!existsSync(join(APP_ROOT$3, "src", "index.ts"))) {
841
+ if (!existsSync(SHELL_ENTRY)) throw new Error(`dsh-desktopify: packaged shell build is missing ${SHELL_ENTRY}`);
842
+ console.log("dsh-desktopify: using the packaged shell build");
843
+ return;
844
+ }
845
+ }
846
+ //#endregion
847
+ //#region src/cli/bundle.ts
848
+ /**
849
+ * Bundle the workspace into a static, unsigned desktop application for the
850
+ * current platform: build the shell, prepare the Node.js runtime and the
851
+ * profile seed, then run electron-builder `--dir` (unpacked application
852
+ * directory — no signing, no notarization, no developer account).
853
+ *
854
+ * `--install` additionally installs the unpacked application into the
855
+ * platform's application directory (macOS `/Applications`, Linux
856
+ * `~/.local/lib` + a desktop entry, Windows `%LOCALAPPDATA%\Programs`).
857
+ *
858
+ * The workspace is read from `DSH_DESKTOP_WORKSPACE` (the CLI forwards its
859
+ * positional argument there) and defaults to the current directory. The
860
+ * shell configuration is written beside the executable as `appconfig.json`
861
+ * and passed to the in-process electron-builder run.
862
+ */
863
+ const APP_ROOT$2 = resolve(import.meta.dirname, "..", "..");
864
+ /** Install the unpacked application into the platform's application directory. */
865
+ function installApp(workspace, name) {
866
+ const buildRootDir = buildRoot(workspace);
867
+ const artifacts = join(buildRootDir, "artifacts");
868
+ if (process.platform === "darwin") {
869
+ const source = join(artifacts, "mac-arm64", `${name}.app`);
870
+ if (!existsSync(source)) throw new Error(`desktop bundle: missing built application ${source}`);
871
+ const target = join("/Applications", `${name}.app`);
872
+ rmSync(target, {
873
+ recursive: true,
874
+ force: true
875
+ });
876
+ cpSync(source, target, {
877
+ recursive: true,
878
+ verbatimSymlinks: true
879
+ });
880
+ console.log(`desktop bundle: installed ${target}`);
881
+ return;
882
+ }
883
+ if (process.platform === "linux") {
884
+ const source = join(artifacts, "linux-unpacked");
885
+ if (!existsSync(source)) throw new Error(`desktop bundle: missing built application ${source}`);
886
+ const target = join(homedir(), ".local", "lib", name);
887
+ rmSync(target, {
888
+ recursive: true,
889
+ force: true
890
+ });
891
+ cpSync(source, target, { recursive: true });
892
+ const applicationsDir = join(homedir(), ".local", "share", "applications");
893
+ mkdirSync(applicationsDir, { recursive: true });
894
+ writeFileSync(join(applicationsDir, `${name}.desktop`), [
895
+ "[Desktop Entry]",
896
+ "Type=Application",
897
+ `Name=${name}`,
898
+ `Exec=${join(target, name)}`,
899
+ "Terminal=false",
900
+ ""
901
+ ].join("\n"));
902
+ console.log(`desktop bundle: installed ${target} with desktop entry`);
903
+ return;
904
+ }
905
+ if (process.platform === "win32") {
906
+ const source = join(artifacts, "win-unpacked");
907
+ if (!existsSync(source)) throw new Error(`desktop bundle: missing built application ${source}`);
908
+ const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
909
+ const target = join(localAppData, "Programs", name);
910
+ rmSync(target, {
911
+ recursive: true,
912
+ force: true
913
+ });
914
+ cpSync(source, target, { recursive: true });
915
+ console.log(`desktop bundle: installed ${target}`);
916
+ return;
917
+ }
918
+ throw new Error(`desktop bundle: unsupported platform ${process.platform}`);
919
+ }
920
+ async function runBundle(options) {
921
+ const workspace = resolve(options.workspace ?? resolveWorkspace());
922
+ const manifest = workspaceManifest(workspace);
923
+ const buildRootDir = buildRoot(workspace);
924
+ const desktop = desktopConfig(manifest);
925
+ const appConfig = {
926
+ name: manifest.name,
927
+ id: desktop.id,
928
+ version: desktop.version,
929
+ dshHome: desktop.dshHome,
930
+ window: desktop.window,
931
+ profile: PROFILE_NAME
932
+ };
933
+ console.log(`desktop bundle: workspace ${workspace} (${appConfig.name}@${appConfig.version})`);
934
+ await buildShell();
935
+ await runPrepareRuntime({ workspace });
936
+ await runPrepareSeed({ workspace });
937
+ const icons = await prepareIcons(workspace, buildRootDir, desktop.icon);
938
+ mkdirSync(join(buildRootDir, "runtime"), { recursive: true });
939
+ writeAppConfig(join(buildRootDir, "runtime"), appConfig);
940
+ await buildDesktopApp({
941
+ appRoot: APP_ROOT$2,
942
+ buildRoot: buildRootDir,
943
+ appConfig,
944
+ icons,
945
+ dir: options.dir
946
+ });
947
+ console.log(`desktop bundle: artifacts in ${join(buildRootDir, "artifacts")}`);
948
+ if (options.install) installApp(workspace, appConfig.name);
949
+ }
950
+ //#endregion
951
+ //#region src/cli/dev.ts
952
+ /**
953
+ * Development launcher: build the shell, prepare a disposable project that
954
+ * links the current workspace (dsh CLI, desktop host, and the dependency
955
+ * closure), then launch the unpackaged Electron shell against it. The backend
956
+ * runs under the current Node.js executable in node mode
957
+ * (`ELECTRON_RUN_AS_NODE=1`) and loads the workspace's TypeScript plugin
958
+ * sources through `--import=tsx/esm` — only when the workspace actually has
959
+ * tsx installed.
960
+ *
961
+ * `--web` runs the web mode instead: prepare the `web` profile (merged
962
+ * bundles + linked dependency closure) and boot `dsh web` in the browser.
963
+ *
964
+ * The workspace is read from `DSH_DESKTOP_WORKSPACE` (the CLI forwards its
965
+ * positional argument there) and defaults to the current directory. The
966
+ * official `@deepseek-ai/*` dependency surface and the official profile
967
+ * bundles are maintained by the tool; the app declares its own dependencies,
968
+ * its `dsh.version`, and its bundles. Official packages are resolved from the
969
+ * app workspace first, so an app outside this repository works the same.
970
+ */
971
+ const APP_ROOT$1 = resolve(import.meta.dirname, "..", "..");
972
+ function debugPort(name, fallback) {
973
+ const value = process.env[name];
974
+ if (value === void 0 || value === "") return fallback;
975
+ const port = Number(value);
976
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error(`desktop development: ${name} must be an integer from 1 through 65535`);
977
+ return port;
978
+ }
979
+ /** Official resolution context for one workspace (app first, then the tool). */
980
+ function officialInput(workspace, workspaceRoot, manifest) {
981
+ const dshVersion$1 = dshVersion(manifest);
982
+ return {
983
+ workspace,
984
+ workspaceRoot,
985
+ toolRoot: APP_ROOT$1,
986
+ ...dshVersion$1 === void 0 ? {} : { dshVersion: dshVersion$1 }
987
+ };
988
+ }
989
+ /** The dsh CLI entry the profile boot and web server run from. */
990
+ function cliEntry(input) {
991
+ const dsh = resolveOfficialPackage(DSH_PACKAGE, input);
992
+ if (dsh === void 0) throw new Error(`desktop development: cannot resolve ${DSH_PACKAGE}`);
993
+ return join(dsh.dir, "lib", "bin.js");
994
+ }
995
+ async function run(command, args, cwd, environment = process.env) {
996
+ await new Promise((resolvePromise, reject) => {
997
+ const child = spawn(command, args, {
998
+ cwd,
999
+ env: environment,
1000
+ stdio: "inherit"
1001
+ });
1002
+ child.once("error", reject);
1003
+ child.once("exit", (code, signal) => {
1004
+ if (code === 0) resolvePromise();
1005
+ else reject(/* @__PURE__ */ new Error(`desktop development: ${args.join(" ")} exited with ${String(code ?? signal)}`));
1006
+ });
1007
+ });
1008
+ }
1009
+ function removeOwnedPath(path) {
1010
+ let stat;
1011
+ try {
1012
+ stat = lstatSync(path);
1013
+ } catch (error) {
1014
+ if (error.code === "ENOENT") return;
1015
+ throw error;
1016
+ }
1017
+ if (stat.isSymbolicLink()) {
1018
+ unlinkSync(path);
1019
+ return;
1020
+ }
1021
+ if (stat.isDirectory()) {
1022
+ rmSync(path, { recursive: true });
1023
+ return;
1024
+ }
1025
+ unlinkSync(path);
1026
+ }
1027
+ function linkDirectory(source, destination, skipExisting = false) {
1028
+ if (skipExisting) try {
1029
+ lstatSync(destination);
1030
+ return;
1031
+ } catch {}
1032
+ mkdirSync(dirname(destination), { recursive: true });
1033
+ symlinkSync(realpathSync(source), destination, process.platform === "win32" ? "junction" : "dir");
1034
+ }
1035
+ /** Mirror one dependency directory (virtual store or hoisted node_modules). */
1036
+ function mirrorDependencyLinks(sourceRoot, destinationRoot, skipExisting = false) {
1037
+ for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
1038
+ if (entry.name.startsWith(".")) continue;
1039
+ const source = join(sourceRoot, entry.name);
1040
+ if (entry.name.startsWith("@") && (entry.isDirectory() || entry.isSymbolicLink())) {
1041
+ mkdirSync(join(destinationRoot, entry.name), { recursive: true });
1042
+ for (const scoped of readdirSync(source, { withFileTypes: true })) {
1043
+ if (!scoped.isDirectory() && !scoped.isSymbolicLink()) continue;
1044
+ linkDirectory(join(source, scoped.name), join(destinationRoot, entry.name, scoped.name), skipExisting);
1045
+ }
1046
+ continue;
1047
+ }
1048
+ if (entry.isDirectory() || entry.isSymbolicLink()) linkDirectory(source, join(destinationRoot, entry.name), skipExisting);
1049
+ }
1050
+ }
1051
+ /** Replace the disposable project with links to the current built workspace. */
1052
+ function prepareDevelopmentProject(projectDir, workspace, input) {
1053
+ const manifest = workspaceManifest(workspace);
1054
+ const dsh = resolveOfficialPackage(DSH_PACKAGE, input);
1055
+ const host = resolveOfficialPackage(DESKTOP_HOST_PACKAGE, input);
1056
+ 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}`);
1058
+ const virtualStore = join(input.workspaceRoot, "node_modules", ".pnpm", "node_modules");
1059
+ const workspaceDependencyDir = existsSync(virtualStore) ? virtualStore : join(workspace, "node_modules");
1060
+ if (!existsSync(workspaceDependencyDir)) throw new Error(`desktop development: workspace dependency links are missing under ${workspaceDependencyDir}; run pnpm install`);
1061
+ if (!existsSync(join(host.dir, "lib", "index.js"))) throw new Error(`desktop development: ${DESKTOP_HOST_PACKAGE} is not built (${join(host.dir, "lib", "index.js")} is missing)`);
1062
+ const officialDependencies = officialDependencySpecs(input);
1063
+ removeOwnedPath(projectDir);
1064
+ mkdirSync(projectDir, { recursive: true });
1065
+ writeFileSync(join(projectDir, "package.json"), `${JSON.stringify({
1066
+ name: manifest.name,
1067
+ private: true,
1068
+ version: "0.0.0",
1069
+ dependencies: officialDependencies,
1070
+ dsh: { profile: { bundles: mergedProfileBundles(manifest) } }
1071
+ }, void 0, 2)}\n`);
1072
+ writeFileSync(join(projectDir, "desktop.cordis.yml"), "# Development composition root; the launcher owns this file.\n[]\n");
1073
+ const destinationModules = join(projectDir, "node_modules");
1074
+ mkdirSync(destinationModules, { recursive: true });
1075
+ mirrorDependencyLinks(workspaceDependencyDir, destinationModules);
1076
+ const toolStore = toolModulesDir(input);
1077
+ if (toolStore !== void 0 && resolve(toolStore) !== resolve(workspaceDependencyDir)) mirrorDependencyLinks(toolStore, destinationModules, true);
1078
+ const dshLink = join(destinationModules, "@deepseek-ai", "dsh");
1079
+ removeOwnedPath(dshLink);
1080
+ linkDirectory(dsh.dir, dshLink);
1081
+ const hostLink = join(destinationModules, "@deepseek-ai", "dsh-desktop-host");
1082
+ removeOwnedPath(hostLink);
1083
+ linkDirectory(host.dir, hostLink);
1084
+ return projectDir;
1085
+ }
1086
+ /**
1087
+ * Prepare the `web` profile for browser mode: install the workspace's own
1088
+ * dependencies into `{workspace}/.dsh-store/profiles/web` with
1089
+ * `dsh plugin --profile web add <pkg>@link:<path>` (the upstream command
1090
+ * initializes the profile with the official bundles and reconciles the
1091
+ * bundle list). Returns the profile directory.
1092
+ */
1093
+ async function prepareWebProfile(workspace, input) {
1094
+ const manifest = workspaceManifest(workspace);
1095
+ const home = join(workspace, ".dsh-store");
1096
+ const entry = cliEntry(input);
1097
+ if (!existsSync(entry)) throw new Error(`desktop development: missing built artifact ${entry}`);
1098
+ for (const packageName of Object.keys(manifest.dependencies ?? {})) {
1099
+ const link = resolveLinkTarget(workspace, packageName);
1100
+ await run(process.execPath, [
1101
+ entry,
1102
+ "plugin",
1103
+ "--profile",
1104
+ "web",
1105
+ "add",
1106
+ `${packageName}@link:${link}`
1107
+ ], workspace, {
1108
+ ...process.env,
1109
+ DSH_HOME: home
1110
+ });
1111
+ }
1112
+ return join(home, "profiles", "web");
1113
+ }
1114
+ /** Resolve a workspace dependency to its package directory (link target). */
1115
+ function resolveLinkTarget(workspace, packageName) {
1116
+ const resolved = createRequire(join(workspace, "package.json")).resolve(packageName);
1117
+ const marker = `${sep}src${sep}index`;
1118
+ const boundary = resolved.indexOf(marker);
1119
+ return boundary < 0 ? dirname(dirname(resolved)) : resolved.slice(0, boundary);
1120
+ }
1121
+ async function launchElectron(projectDir, buildRootDir, tsxImport) {
1122
+ const electron = createRequire(import.meta.url)("electron");
1123
+ if (typeof electron !== "string") throw new Error("desktop development: electron executable is unavailable");
1124
+ const mainPort = debugPort("DSH_DESKTOP_MAIN_INSPECT_PORT", 9229);
1125
+ const rendererPort = debugPort("DSH_DESKTOP_RENDERER_DEBUG_PORT", 9222);
1126
+ const hostPort = debugPort("DSH_DESKTOP_HOST_INSPECT_PORT", 9230);
1127
+ const developmentRoot = join(buildRootDir, "development");
1128
+ const home = resolve(join(developmentRoot, "home"));
1129
+ const userData = join(developmentRoot, "electron-user-data");
1130
+ const systemNode = process.env.DSH_DESKTOP_NODE_BINARY ?? process.env.npm_node_execpath ?? "node";
1131
+ const environment = {
1132
+ ...process.env,
1133
+ DSH_HOME: home,
1134
+ DSH_DESKTOP_APPCONFIG_DIR: join(buildRootDir, "runtime"),
1135
+ DSH_DESKTOP_DEV_PROJECT_DIR: projectDir,
1136
+ DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort),
1137
+ DSH_DESKTOP_NODE_BINARY: systemNode,
1138
+ DSH_DESKTOP_TSX_IMPORT: tsxImport ? "tsx/esm" : "",
1139
+ DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? "1",
1140
+ ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? "1"
1141
+ };
1142
+ console.log(`desktop development: DSH_HOME=${home}`);
1143
+ console.log(`desktop development: inspectors main=${String(mainPort)}, renderer=${String(rendererPort)}, host=${String(hostPort)}`);
1144
+ await run(electron, [
1145
+ `--inspect=127.0.0.1:${String(mainPort)}`,
1146
+ `--remote-debugging-port=${String(rendererPort)}`,
1147
+ `--user-data-dir=${userData}`,
1148
+ APP_ROOT$1
1149
+ ], APP_ROOT$1, environment);
1150
+ }
1151
+ async function runDev(options) {
1152
+ const workspace = resolve(options.workspace ?? resolveWorkspace());
1153
+ const repositoryRoot = findWorkspaceRoot(workspace);
1154
+ const manifest = workspaceManifest(workspace);
1155
+ const input = officialInput(workspace, repositoryRoot, manifest);
1156
+ const buildRootDir = buildRoot(workspace);
1157
+ if (!options.skipBuild) await buildShell();
1158
+ if (options.web) {
1159
+ const home = join(workspace, ".dsh-store");
1160
+ const profileDir = await prepareWebProfile(workspace, input);
1161
+ const port = process.env.PORT ?? "3080";
1162
+ const entry = cliEntry(input);
1163
+ if (!existsSync(entry)) throw new Error(`desktop development: missing built artifact ${entry}`);
1164
+ console.log(`desktop development: web mode DSH_HOME=${home} profile=${profileDir} port=${port}`);
1165
+ const nodeOptions = hasTsx(workspace, repositoryRoot) ? [process.env.NODE_OPTIONS, "--import=tsx/esm"].filter(Boolean).join(" ") : process.env.NODE_OPTIONS;
1166
+ await run(process.execPath, [
1167
+ entry,
1168
+ "web",
1169
+ "--port",
1170
+ port
1171
+ ], repositoryRoot, {
1172
+ ...process.env,
1173
+ DSH_HOME: home,
1174
+ ...nodeOptions === void 0 ? {} : { NODE_OPTIONS: nodeOptions }
1175
+ });
1176
+ return;
1177
+ }
1178
+ if (!existsSync(SHELL_ENTRY)) throw new Error(`desktop development: missing built artifact ${SHELL_ENTRY}`);
1179
+ const projectDir = prepareDevelopmentProject(join(buildRootDir, "development", "project"), workspace, input);
1180
+ const desktop = desktopConfig(manifest);
1181
+ mkdirSync(join(buildRootDir, "runtime"), { recursive: true });
1182
+ writeAppConfig(join(buildRootDir, "runtime"), {
1183
+ name: manifest.name,
1184
+ id: desktop.id,
1185
+ version: desktop.version,
1186
+ dshHome: "env",
1187
+ window: desktop.window,
1188
+ profile: PROFILE_NAME
1189
+ });
1190
+ await launchElectron(projectDir, buildRootDir, hasTsx(projectDir, projectDir));
1191
+ }
1192
+ //#endregion
1193
+ //#region src/cli/index.ts
1194
+ /**
1195
+ * dsh-desktopify CLI: package and run a dsh workspace as a desktop
1196
+ * application. The workspace is the first positional argument (default: the
1197
+ * current directory); `DSH_DESKTOP_WORKSPACE` overrides it.
1198
+ *
1199
+ * dsh-desktopify dev [--web] [--skip-build] [workspace]
1200
+ * launch the Electron shell against
1201
+ * the workspace (TS sources loaded
1202
+ * directly, no packaging); --web
1203
+ * boots `dsh web` in the browser
1204
+ * instead
1205
+ * dsh-desktopify bundle [--dir] [--install] [workspace]
1206
+ * build a static, unsigned desktop
1207
+ * application for the current
1208
+ * platform
1209
+ * dsh-desktopify build build the shell (tsdown)
1210
+ * dsh-desktopify prepare:runtime [workspace] download and verify the bundled
1211
+ * Node.js runtime
1212
+ * dsh-desktopify prepare:seed [workspace] prepare the packaged profile seed
1213
+ * @module @morlay/dsh-desktopify
1214
+ */
1215
+ /** 工具包根(源码形态 `src/cli` 与构建形态 `dist/cli` 同深度)。 */
1216
+ const APP_ROOT = resolve(import.meta.dirname, "..", "..");
1217
+ /** 包版本(读取包根 package.json,源码形态与发布形态一致)。 */
1218
+ function packageVersion() {
1219
+ return JSON.parse(readFileSync(resolve(APP_ROOT, "package.json"), "utf8")).version ?? "0.0.0";
1220
+ }
1221
+ /** Resolve the workspace argument (default: current directory) and expose it to child processes. */
1222
+ function resolveWorkspaceArg(workspace) {
1223
+ const resolved = resolve(workspace ?? process.cwd());
1224
+ process.env.DSH_DESKTOP_WORKSPACE = resolved;
1225
+ return resolved;
1226
+ }
1227
+ const program = new Command();
1228
+ program.name("dsh-desktopify").description("package and run a dsh workspace as a desktop application").version(packageVersion());
1229
+ program.command("dev").description("launch the Electron shell against the workspace (or `dsh web` with --web)").option("--web", "boot `dsh web` in the browser instead of the Electron shell").option("--skip-build", "skip rebuilding the shell").argument("[workspace]", "app workspace directory (default: current directory)").action(async (workspace, options) => {
1230
+ await runDev({
1231
+ workspace: resolveWorkspaceArg(workspace),
1232
+ web: options.web === true,
1233
+ skipBuild: options.skipBuild === true
1234
+ });
1235
+ });
1236
+ program.command("bundle").description("build a static, unsigned desktop application for the current platform").option("--dir", "produce an unpacked application directory (no installer)").option("--install", "install the built application into the platform application directory").argument("[workspace]", "app workspace directory (default: current directory)").action(async (workspace, options) => {
1237
+ await runBundle({
1238
+ workspace: resolveWorkspaceArg(workspace),
1239
+ dir: options.dir === true,
1240
+ install: options.install === true
1241
+ });
1242
+ });
1243
+ program.command("build").description("build the shell (tsdown)").action(buildShell);
1244
+ program.command("prepare:runtime").description("download and verify the bundled Node.js runtime").argument("[workspace]", "app workspace directory (default: current directory)").action(async (workspace) => {
1245
+ await runPrepareRuntime({ workspace: resolveWorkspaceArg(workspace) });
1246
+ });
1247
+ program.command("prepare:seed").description("prepare the packaged profile seed").argument("[workspace]", "app workspace directory (default: current directory)").action(async (workspace) => {
1248
+ await runPrepareSeed({ workspace: resolveWorkspaceArg(workspace) });
1249
+ });
1250
+ await program.parseAsync(process.argv);
1251
+ //#endregion
1252
+ export {};