@morlay/dsh-desktopify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/cli/index.mjs +1252 -0
- package/dist/index.mjs +860 -0
- package/dist/preload-app.cjs +4 -0
- package/dist/preload.cjs +4 -0
- package/dist/seed-Ca0AMFtp.mjs +152 -0
- package/package.json +51 -0
|
@@ -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 {};
|