@morlay/dsh-desktop-shell 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/{appconfig-BZ9wPROk.mjs → appconfig-CSZh3JG7.mjs} +2 -2
- package/dist/appconfig.d.mts +21 -0
- package/dist/appconfig.mjs +1 -2
- package/dist/{dshhome-Dp89FOr-.mjs → dshhome-DAJ9dkn2.mjs} +2 -2
- package/dist/dshhome.d.mts +6 -0
- package/dist/dshhome.mjs +1 -2
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +1050 -139
- package/dist/inline-packages.d.mts +14 -0
- package/dist/inline-packages.mjs +21 -0
- package/dist/official-BmgBsdhq.mjs +5 -0
- package/dist/official.d.mts +4 -0
- package/dist/official.mjs +1 -2
- package/dist/preload-app.cjs +5 -1
- package/dist/profile-project.d.mts +20 -0
- package/dist/profile-project.mjs +1 -2
- package/dist/{seed-BF71UAuI.mjs → seed-Bxr0BsCk.mjs} +52 -5
- package/dist/seed.d.mts +21 -0
- package/dist/seed.mjs +1 -2
- package/package.json +44 -22
- package/src/__tests__/developer-endpoint.spec.ts +17 -0
- package/src/__tests__/document-marks.spec.ts +42 -0
- package/src/__tests__/dshhome.spec.ts +108 -0
- package/src/__tests__/host-process.spec.ts +205 -0
- package/src/__tests__/ipc.spec.ts +39 -0
- package/src/__tests__/profile-project.spec.ts +204 -0
- package/src/__tests__/seed-profile.spec.ts +189 -0
- package/src/__tests__/shell-env.spec.ts +131 -0
- package/src/__tests__/shell-import-boundary.spec.ts +69 -0
- package/src/__tests__/stream-uplink.spec.ts +49 -0
- package/src/appconfig.ts +79 -0
- package/src/developer-endpoint.ts +30 -0
- package/src/developer-menu.ts +198 -0
- package/src/directory-picker.ts +37 -0
- package/src/document-marks.ts +58 -0
- package/src/dshhome.ts +29 -0
- package/src/electron.ts +33 -0
- package/src/host-process.ts +533 -0
- package/src/index.ts +716 -0
- package/src/inline-packages.ts +18 -0
- package/src/ipc.ts +58 -0
- package/src/keybindings.ts +35 -0
- package/src/official.ts +8 -0
- package/src/preload-app.ts +147 -0
- package/src/profile-project.ts +138 -0
- package/src/seed.ts +183 -0
- package/src/shell-env.ts +31 -0
- package/src/shortcuts.ts +236 -0
- package/src/stream-uplink.ts +41 -0
- package/dist/official-Cz9Mry1n.mjs +0 -167
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/inline-packages.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 壳产物里要**内联**的包(而不是留在产物外)。
|
|
4
|
+
*
|
|
5
|
+
* 壳跑在 `app.asar` 里,它的解析面只有产物自身 + Electron 提供的东西:留在外面的裸引用会变成
|
|
6
|
+
* `ERR_MODULE_NOT_FOUND`。所以这几类必须内联——
|
|
7
|
+
* - `@morlay/dsh-desktop-host`:壳直接用它(wire 协议、paths、webServer 的替身);
|
|
8
|
+
* - 快捷键桥的原生侧要用上游的「设备偏好单写者」与原子写,它们同样进不了 asar 的解析面。
|
|
9
|
+
*
|
|
10
|
+
* 这份名单是**一个事实**:壳自己的构建([`tsdown.config.ts`](../../tsdown.config.ts))与 `@local/dshdev` 的
|
|
11
|
+
* 装配(`prepareShell`)都用它——两边不一致的话,装配出来的产物就会缺依赖。
|
|
12
|
+
*/
|
|
13
|
+
export declare const SHELL_INLINE_PACKAGES: readonly string[];
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/inline-packages.ts
|
|
2
|
+
/**
|
|
3
|
+
* 壳产物里要**内联**的包(而不是留在产物外)。
|
|
4
|
+
*
|
|
5
|
+
* 壳跑在 `app.asar` 里,它的解析面只有产物自身 + Electron 提供的东西:留在外面的裸引用会变成
|
|
6
|
+
* `ERR_MODULE_NOT_FOUND`。所以这几类必须内联——
|
|
7
|
+
* - `@morlay/dsh-desktop-host`:壳直接用它(wire 协议、paths、webServer 的替身);
|
|
8
|
+
* - 快捷键桥的原生侧要用上游的「设备偏好单写者」与原子写,它们同样进不了 asar 的解析面。
|
|
9
|
+
*
|
|
10
|
+
* 这份名单是**一个事实**:壳自己的构建([`tsdown.config.ts`](../../tsdown.config.ts))与 `@local/dshdev` 的
|
|
11
|
+
* 装配(`prepareShell`)都用它——两边不一致的话,装配出来的产物就会缺依赖。
|
|
12
|
+
*/
|
|
13
|
+
const SHELL_INLINE_PACKAGES = [
|
|
14
|
+
"@morlay/dsh-desktop-host",
|
|
15
|
+
"@deepseek-ai/dsh-client-shortcuts",
|
|
16
|
+
"@deepseek-ai/dsh-atomic-write",
|
|
17
|
+
"@deepseek-ai/dsh-util-values",
|
|
18
|
+
"@deepseek-ai/dsh-util-crypto"
|
|
19
|
+
];
|
|
20
|
+
//#endregion
|
|
21
|
+
export { SHELL_INLINE_PACKAGES };
|
package/dist/official.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { DESKTOP_HOST_PACKAGE, OFFICIAL_PROFILE_BUNDLES, OFFICIAL_PROFILE_PACKAGES, OFFICIAL_RUNTIME_PACKAGES };
|
|
1
|
+
export { t as DESKTOP_HOST_PACKAGE, n as OFFICIAL_PROFILE_BUNDLES } from "./official-BmgBsdhq.mjs";
|
package/dist/preload-app.cjs
CHANGED
|
@@ -34,6 +34,8 @@ function syncNativeTheme(platform, root, send) {
|
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/ipc.ts
|
|
37
|
+
/** 壳把宿主回环的 ws 端口经 preload 递进页面(`additionalArguments`,与 scheme 同一手法)。 */
|
|
38
|
+
const DESKTOP_WS_PORT_ARGUMENT = "--dsh-desktop-ws-port";
|
|
37
39
|
const DESKTOP_IPC = {
|
|
38
40
|
directoryPick: "dsh-desktop:directory-pick",
|
|
39
41
|
nativeThemeSet: "dsh-desktop:native-theme-set",
|
|
@@ -81,7 +83,9 @@ function createStreamUplink(send, end) {
|
|
|
81
83
|
//#endregion
|
|
82
84
|
//#region src/preload-app.ts
|
|
83
85
|
const scheme = process.argv.find((argument) => argument.startsWith(`${DESKTOP_SCHEME_ARGUMENT}=`))?.slice(21);
|
|
84
|
-
|
|
86
|
+
const wsPort = process.argv.find((argument) => argument.startsWith(`${DESKTOP_WS_PORT_ARGUMENT}=`))?.slice(22);
|
|
87
|
+
if (scheme !== void 0 && scheme !== "") {
|
|
88
|
+
console.log(`[dsh-shell] preload: scheme=${String(scheme)} wsPort=${String(wsPort)} webFrame=${typeof electron.webFrame}`);
|
|
85
89
|
electron.contextBridge.exposeInMainWorld("__DSH_DIRECTORY_PICKER__", { pick: () => electron.ipcRenderer.invoke(DESKTOP_IPC.directoryPick) });
|
|
86
90
|
electron.contextBridge.exposeInMainWorld("__DSH_DESKTOP_STREAM__", { open(endpoint, payload, handlers) {
|
|
87
91
|
let id;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
//#region src/profile-project.d.ts
|
|
3
|
+
export declare const PROFILE_WORKSPACE_NAME = "pnpm-workspace.yaml";
|
|
4
|
+
export declare const PROFILE_RUNTIME_REPORT_NAME = "desktop-runtime-packages.json";
|
|
5
|
+
export declare const PROFILE_VENDOR_DIR_NAME = "vendor";
|
|
6
|
+
export interface RuntimeOverride {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly target: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runtimeOverrides(profileDir: string, runtimeDir: string): Promise<RuntimeOverride[]>;
|
|
11
|
+
export declare function workspaceWithOverrides(text: string, overrides: readonly RuntimeOverride[]): string;
|
|
12
|
+
export type ProfileInstallSpawn = typeof spawn;
|
|
13
|
+
export interface ProfileInstallOptions {
|
|
14
|
+
readonly node: string;
|
|
15
|
+
readonly pnpmEntry: string;
|
|
16
|
+
readonly profileDir: string;
|
|
17
|
+
readonly spawn?: ProfileInstallSpawn;
|
|
18
|
+
}
|
|
19
|
+
export declare function installProfile(options: ProfileInstallOptions): Promise<void>;
|
|
20
|
+
//#endregion
|
package/dist/profile-project.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { PROFILE_RUNTIME_REPORT_NAME, PROFILE_VENDOR_DIR_NAME, PROFILE_WORKSPACE_NAME, installProfile, runtimeOverrides, workspaceWithOverrides };
|
|
1
|
+
export { t as PROFILE_RUNTIME_REPORT_NAME, n as PROFILE_VENDOR_DIR_NAME, r as PROFILE_WORKSPACE_NAME, i as installProfile, a as runtimeOverrides, o as workspaceWithOverrides } from "./profile-project-BgQILDbu.mjs";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { n as PROFILE_PATCH_NAME, t as PROFILE_NAME } from "./appconfig-
|
|
1
|
+
import { n as PROFILE_PATCH_NAME, t as PROFILE_NAME } from "./appconfig-CSZh3JG7.mjs";
|
|
2
2
|
import { access, chmod, copyFile, lstat, mkdir, readFile, readdir, readlink, rm, symlink, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
//#region src/seed.ts
|
|
5
6
|
const SEED_DIR_NAME = "dsh-home";
|
|
6
7
|
const SEED_HASH_NAME = ".seed-hash";
|
|
@@ -32,13 +33,51 @@ async function isDirectory(path) {
|
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
/** 解压随包的依赖归档:实体文件、闭包完整(替代逐包 `link:` 那种写法)。 */
|
|
37
|
+
async function extractProfileModules(archive, profileDir) {
|
|
38
|
+
await new Promise((done, fail) => {
|
|
39
|
+
const child = spawn("tar", [
|
|
40
|
+
"-xzf",
|
|
41
|
+
archive,
|
|
42
|
+
"-C",
|
|
43
|
+
profileDir
|
|
44
|
+
], { stdio: [
|
|
45
|
+
"ignore",
|
|
46
|
+
"ignore",
|
|
47
|
+
"inherit"
|
|
48
|
+
] });
|
|
49
|
+
child.on("error", fail);
|
|
50
|
+
child.on("close", (code) => code === 0 ? done() : fail(/* @__PURE__ */ new Error(`tar -xzf ${archive} exited with ${String(code)}`)));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const PROFILE_MODULES_ARCHIVE_NAME = "profile-node-modules.tar";
|
|
54
|
+
/** 依赖面是否真的在:`node_modules` 存在且非空。 */
|
|
55
|
+
async function hasDependencies(profileDir) {
|
|
56
|
+
const modules = join(profileDir, "node_modules");
|
|
57
|
+
if (!await isDirectory(modules)) return false;
|
|
58
|
+
return (await readdir(modules)).length > 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 把随包的种子种成一份可用的 profile:清单 + 各 bundle 自己的 patch 从种子拷(用户那份 patch 除外),
|
|
62
|
+
* **依赖面**由随包归档解压而来。种完才算数——调用方要等它返回才起宿主。
|
|
63
|
+
*
|
|
64
|
+
* 判"已是最新"要同时满足:指纹一致 **且** 依赖面真的在(归档在场时)。指纹只说明"清单与闭包是哪一份",
|
|
65
|
+
* 不说明"它被解压出来了":少了后半句,一次解压失败(或谁清过 `node_modules`)就永远补不回来,宿主会一直
|
|
66
|
+
* 带着残缺 profile 起来。
|
|
67
|
+
* @param seedDir - 随包的种子根(`Resources/seed`)。
|
|
68
|
+
* @param home - 数据面根(`$DSH_HOME`);profile 落在 `<home>/profiles/<name>`。
|
|
69
|
+
* @returns 种了(`seeded`)、已是最新(`up-to-date`)、或随包根本没有这份种(`no-seed`)。
|
|
70
|
+
* @throws 归档解压失败、或解压后没有依赖面时。
|
|
71
|
+
*/
|
|
35
72
|
async function ensureSeedProfile(seedDir, home) {
|
|
36
73
|
const seedProfile = join(seedDir, "profiles", PROFILE_NAME);
|
|
37
|
-
if (!await isDirectory(seedProfile)) return
|
|
74
|
+
if (!await isDirectory(seedProfile)) return "no-seed";
|
|
38
75
|
const profileDir = join(home, "profiles", PROFILE_NAME);
|
|
76
|
+
const archive = join(seedDir, PROFILE_MODULES_ARCHIVE_NAME);
|
|
77
|
+
const hasArchive = await pathExists(archive);
|
|
39
78
|
const seedHash = await readSeedHash(join(seedProfile, SEED_HASH_NAME));
|
|
40
79
|
if (seedHash !== "") try {
|
|
41
|
-
if (!(await lstat(profileDir)).isSymbolicLink() && await readSeedHash(join(profileDir, ".seed-hash")) === seedHash) return
|
|
80
|
+
if (!(await lstat(profileDir)).isSymbolicLink() && await readSeedHash(join(profileDir, ".seed-hash")) === seedHash && (!hasArchive || await hasDependencies(profileDir))) return "up-to-date";
|
|
42
81
|
} catch {}
|
|
43
82
|
const patchPath = join(profileDir, PROFILE_PATCH_NAME);
|
|
44
83
|
const userPatch = await readFile(patchPath, "utf8").catch(() => void 0);
|
|
@@ -51,11 +90,19 @@ async function ensureSeedProfile(seedDir, home) {
|
|
|
51
90
|
if (error.code !== "ENOENT") throw error;
|
|
52
91
|
}
|
|
53
92
|
await copySeed(seedProfile, profileDir);
|
|
93
|
+
if (hasArchive) {
|
|
94
|
+
await rm(join(profileDir, "node_modules"), {
|
|
95
|
+
recursive: true,
|
|
96
|
+
force: true
|
|
97
|
+
});
|
|
98
|
+
await extractProfileModules(archive, profileDir);
|
|
99
|
+
if (!await hasDependencies(profileDir)) throw new Error(`dsh desktop: ${archive} did not yield a dependency closure at ${join(profileDir, "node_modules")}`);
|
|
100
|
+
}
|
|
54
101
|
if (userPatch !== void 0) {
|
|
55
102
|
await mkdir(dirname(patchPath), { recursive: true });
|
|
56
103
|
await writeFile(patchPath, userPatch);
|
|
57
104
|
}
|
|
58
|
-
return
|
|
105
|
+
return "seeded";
|
|
59
106
|
}
|
|
60
107
|
async function copySeed(source, target) {
|
|
61
108
|
await mkdir(target, { recursive: true });
|
|
@@ -91,4 +138,4 @@ async function copySeed(source, target) {
|
|
|
91
138
|
await visit(source, "");
|
|
92
139
|
}
|
|
93
140
|
//#endregion
|
|
94
|
-
export { ensureSeedProfile as i,
|
|
141
|
+
export { ensureSeedProfile as a, SEED_RUNTIME_DIR_NAME as i, SEED_DIR_NAME as n, SEED_HASH_NAME as r, PROFILE_MODULES_ARCHIVE_NAME as t };
|
package/dist/seed.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/seed.d.ts
|
|
2
|
+
export declare const SEED_DIR_NAME = "dsh-home";
|
|
3
|
+
export declare const SEED_HASH_NAME = ".seed-hash";
|
|
4
|
+
export declare const SEED_RUNTIME_DIR_NAME = "runtime";
|
|
5
|
+
export declare const PROFILE_MODULES_ARCHIVE_NAME = "profile-node-modules.tar";
|
|
6
|
+
/** 一次"种 profile"的结果:调用方据此记录、并在打包形态把 `no-seed` 当致命。 */
|
|
7
|
+
export type SeedOutcome = "seeded" | "up-to-date" | "no-seed";
|
|
8
|
+
/**
|
|
9
|
+
* 把随包的种子种成一份可用的 profile:清单 + 各 bundle 自己的 patch 从种子拷(用户那份 patch 除外),
|
|
10
|
+
* **依赖面**由随包归档解压而来。种完才算数——调用方要等它返回才起宿主。
|
|
11
|
+
*
|
|
12
|
+
* 判"已是最新"要同时满足:指纹一致 **且** 依赖面真的在(归档在场时)。指纹只说明"清单与闭包是哪一份",
|
|
13
|
+
* 不说明"它被解压出来了":少了后半句,一次解压失败(或谁清过 `node_modules`)就永远补不回来,宿主会一直
|
|
14
|
+
* 带着残缺 profile 起来。
|
|
15
|
+
* @param seedDir - 随包的种子根(`Resources/seed`)。
|
|
16
|
+
* @param home - 数据面根(`$DSH_HOME`);profile 落在 `<home>/profiles/<name>`。
|
|
17
|
+
* @returns 种了(`seeded`)、已是最新(`up-to-date`)、或随包根本没有这份种(`no-seed`)。
|
|
18
|
+
* @throws 归档解压失败、或解压后没有依赖面时。
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureSeedProfile(seedDir: string, home: string): Promise<SeedOutcome>;
|
|
21
|
+
//#endregion
|
package/dist/seed.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { SEED_DIR_NAME, SEED_HASH_NAME, SEED_RUNTIME_DIR_NAME, ensureSeedProfile };
|
|
1
|
+
export { t as PROFILE_MODULES_ARCHIVE_NAME, n as SEED_DIR_NAME, r as SEED_HASH_NAME, i as SEED_RUNTIME_DIR_NAME, a as ensureSeedProfile } from "./seed-Bxr0BsCk.mjs";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-desktop-shell",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Electron shell for dsh desktop apps: serves the app's own <name>:// scheme, boots the desktop profile through the @morlay/dsh-desktop-host child over byte pipes, and installs the profile seed on first launch. The CLI that packages a workspace around it is @
|
|
3
|
+
"version": "0.2.7",
|
|
4
|
+
"description": "Electron shell for dsh desktop apps: serves the app's own <name>:// scheme, boots the desktop profile through the @morlay/dsh-desktop-host child over byte pipes, and installs the profile seed on first launch. The CLI that packages a workspace around it is @local/dshdev.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"desktop",
|
|
7
7
|
"dsh",
|
|
@@ -15,43 +15,65 @@
|
|
|
15
15
|
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
+
"src",
|
|
18
19
|
"locale/*.json",
|
|
19
20
|
"dist"
|
|
20
21
|
],
|
|
21
22
|
"type": "module",
|
|
22
|
-
"main": "./
|
|
23
|
-
"module": "./dist/index.mjs",
|
|
23
|
+
"main": "./main.mjs",
|
|
24
24
|
"exports": {
|
|
25
|
-
".":
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"./
|
|
30
|
-
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"./appconfig": {
|
|
30
|
+
"types": "./dist/appconfig.d.mts",
|
|
31
|
+
"default": "./dist/appconfig.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./dshhome": {
|
|
34
|
+
"types": "./dist/dshhome.d.mts",
|
|
35
|
+
"default": "./dist/dshhome.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./official": {
|
|
38
|
+
"types": "./dist/official.d.mts",
|
|
39
|
+
"default": "./dist/official.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./profile-project": {
|
|
42
|
+
"types": "./dist/profile-project.d.mts",
|
|
43
|
+
"default": "./dist/profile-project.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./seed": {
|
|
46
|
+
"types": "./dist/seed.d.mts",
|
|
47
|
+
"default": "./dist/seed.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./inline-packages": {
|
|
50
|
+
"types": "./dist/inline-packages.d.mts",
|
|
51
|
+
"default": "./dist/inline-packages.mjs"
|
|
52
|
+
},
|
|
31
53
|
"./package.json": "./package.json",
|
|
32
54
|
"./locale/*.json": "./locale/*.json"
|
|
33
55
|
},
|
|
34
56
|
"dependencies": {
|
|
35
|
-
"
|
|
57
|
+
"@deepseek-ai/dsh-util-crypto": "^0.2.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-util-values": "^0.2.1-rc.1",
|
|
59
|
+
"@morlay/dsh-desktop-host": "0.2.7"
|
|
36
60
|
},
|
|
37
61
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh": "0.2.0-rc.2",
|
|
39
|
-
"@deepseek-ai/dsh-app-boot": "0.2.0-rc.2",
|
|
40
|
-
"@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
|
|
41
|
-
"@deepseek-ai/dsh-client-shortcuts": "0.2.0-rc.2",
|
|
42
62
|
"@local/devkit": "0.1.0",
|
|
43
|
-
"@
|
|
44
|
-
"@types/node": "^26.5.0"
|
|
63
|
+
"@oxc-node/core": "^0.1.6",
|
|
64
|
+
"@types/node": "^26.5.0",
|
|
65
|
+
"electron": "44.7.0"
|
|
45
66
|
},
|
|
46
67
|
"peerDependencies": {
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
68
|
+
"@deepseek-ai/dsh-app-boot": "^0.2.1-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-atomic-write": "^0.2.1-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-shortcuts": "^0.2.1-rc.1"
|
|
49
71
|
},
|
|
50
72
|
"engines": {
|
|
51
|
-
"node": ">=24.11.0"
|
|
73
|
+
"node": ">=24.11.0",
|
|
74
|
+
"dsh": ">= 0.2.1-rc.1 < 0.2.2"
|
|
52
75
|
},
|
|
53
76
|
"scripts": {
|
|
54
|
-
"build": "pnpm exec tsdown"
|
|
55
|
-
"gen:official-packages": "tsx scripts/gen-official-packages.ts"
|
|
77
|
+
"build": "pnpm exec tsdown"
|
|
56
78
|
}
|
|
57
79
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { nodeInspectorDevtoolsUrl } from "../developer-endpoint.ts";
|
|
3
|
+
|
|
4
|
+
describe("后端调试前端地址", () => {
|
|
5
|
+
it("把 host 回报的端点拼成应用 origin 下的 DevTools 入口", () => {
|
|
6
|
+
expect(nodeInspectorDevtoolsUrl("dsh-custom-next", "ws://127.0.0.1:9229/7f0d-4a2b")).toBe(
|
|
7
|
+
"dsh-custom-next://app/node-devtools/devtools_app.html" +
|
|
8
|
+
"?ws=127.0.0.1:9229/7f0d-4a2b&panel=console&disableLocaleInfoBar=true",
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("非 WebSocket 端点与带查询分隔符的端点都拒绝", () => {
|
|
13
|
+
expect(() => nodeInspectorDevtoolsUrl("dsh-app", "http://127.0.0.1:9229/x")).toThrow();
|
|
14
|
+
expect(() => nodeInspectorDevtoolsUrl("dsh-app", "ws://127.0.0.1:9229/x?y=1&z=2")).toThrow();
|
|
15
|
+
expect(() => nodeInspectorDevtoolsUrl("dsh-app", "not a url")).toThrow();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
WINDOWS_TITLEBAR_HEIGHT,
|
|
5
|
+
markDocumentPlatform,
|
|
6
|
+
markWindowsTitlebar,
|
|
7
|
+
syncNativeTheme,
|
|
8
|
+
} from "../document-marks.ts";
|
|
9
|
+
|
|
10
|
+
describe("桌面文档标记", () => {
|
|
11
|
+
it("macOS 打上 data-platform,客户端据此启用侧边栏顶部拖拽条与 logo 下沉", () => {
|
|
12
|
+
markDocumentPlatform("darwin", document);
|
|
13
|
+
expect(document.documentElement.dataset.platform).toBe("darwin");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("Windows 打上 caption 标记与标题栏高度(其它平台不打)", () => {
|
|
17
|
+
markWindowsTitlebar("linux", document);
|
|
18
|
+
expect(document.documentElement.hasAttribute("data-windows-titlebar")).toBe(false);
|
|
19
|
+
|
|
20
|
+
markWindowsTitlebar("win32", document);
|
|
21
|
+
expect(document.documentElement.hasAttribute("data-windows-titlebar")).toBe(true);
|
|
22
|
+
expect(document.documentElement.style.getPropertyValue("--dsh-windows-titlebar-height")).toBe(
|
|
23
|
+
`${String(WINDOWS_TITLEBAR_HEIGHT)}px`,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("只有 macOS 把主题来源转发给主进程,且只在值变化时发", async () => {
|
|
28
|
+
const sent: string[] = [];
|
|
29
|
+
syncNativeTheme("win32", document, (source) => sent.push(source));
|
|
30
|
+
document.documentElement.setAttribute("data-ds-theme-source", "dark");
|
|
31
|
+
expect(sent).toEqual([]);
|
|
32
|
+
|
|
33
|
+
syncNativeTheme("darwin", document, (source) => sent.push(source));
|
|
34
|
+
expect(sent).toEqual(["dark"]);
|
|
35
|
+
document.documentElement.setAttribute("data-ds-theme-source", "dark");
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
expect(sent).toEqual(["dark"]);
|
|
38
|
+
document.documentElement.setAttribute("data-ds-theme-source", "light");
|
|
39
|
+
await Promise.resolve();
|
|
40
|
+
expect(sent).toEqual(["dark", "light"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join, normalize, resolve, sep } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import type { AppConfig } from "../appconfig.ts";
|
|
6
|
+
import { resolveDshHome, xdgDataHome } from "../dshhome.ts";
|
|
7
|
+
|
|
8
|
+
const ORIGINAL = {
|
|
9
|
+
override: process.env.DSH_APP_DSH_HOME,
|
|
10
|
+
xdg: process.env.XDG_DATA_HOME,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const roots: string[] = [];
|
|
14
|
+
|
|
15
|
+
async function tempDir(): Promise<string> {
|
|
16
|
+
const root = await mkdtemp(resolve(tmpdir(), "dshdev-home-"));
|
|
17
|
+
roots.push(root);
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function config(dataDir: string, name = "dsh-custom"): AppConfig {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
displayName: name,
|
|
25
|
+
id: "ai.deepseek.dsh.custom",
|
|
26
|
+
version: "0.1.5",
|
|
27
|
+
profile: "desktop",
|
|
28
|
+
dataDir,
|
|
29
|
+
window: { width: 1280, height: 800, minWidth: 800, minHeight: 600 },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
|
35
|
+
if (ORIGINAL.override === undefined) delete process.env.DSH_APP_DSH_HOME;
|
|
36
|
+
else process.env.DSH_APP_DSH_HOME = ORIGINAL.override;
|
|
37
|
+
if (ORIGINAL.xdg === undefined) delete process.env.XDG_DATA_HOME;
|
|
38
|
+
else process.env.XDG_DATA_HOME = ORIGINAL.xdg;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("xdgDataHome", () => {
|
|
42
|
+
it.skipIf(process.platform !== "darwin")(
|
|
43
|
+
"uses the macOS application support directory and ignores XDG_DATA_HOME",
|
|
44
|
+
async () => {
|
|
45
|
+
process.env.XDG_DATA_HOME = await tempDir();
|
|
46
|
+
|
|
47
|
+
expect(xdgDataHome()).toBe(join(homedir(), "Library", "Application Support"));
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
it.skipIf(process.platform === "darwin")(
|
|
52
|
+
"honours XDG_DATA_HOME and otherwise falls back to ~/.local/share",
|
|
53
|
+
async () => {
|
|
54
|
+
const configured = await tempDir();
|
|
55
|
+
process.env.XDG_DATA_HOME = configured;
|
|
56
|
+
expect(xdgDataHome()).toBe(normalize(configured));
|
|
57
|
+
|
|
58
|
+
process.env.XDG_DATA_HOME = " ";
|
|
59
|
+
expect(xdgDataHome()).toBe(join(homedir(), ".local", "share"));
|
|
60
|
+
|
|
61
|
+
delete process.env.XDG_DATA_HOME;
|
|
62
|
+
expect(xdgDataHome()).toBe(join(homedir(), ".local", "share"));
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveDshHome", () => {
|
|
68
|
+
it("appends the app name to the platform data home for xdg", () => {
|
|
69
|
+
delete process.env.DSH_APP_DSH_HOME;
|
|
70
|
+
|
|
71
|
+
expect(resolveDshHome(config("xdg"))).toBe(join(xdgDataHome(), "dsh-custom"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reports no concrete home for env mode", () => {
|
|
75
|
+
delete process.env.DSH_APP_DSH_HOME;
|
|
76
|
+
|
|
77
|
+
expect(resolveDshHome(config("env"))).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("takes an absolute path as-is", async () => {
|
|
81
|
+
delete process.env.DSH_APP_DSH_HOME;
|
|
82
|
+
const explicit = join(await tempDir(), "home");
|
|
83
|
+
|
|
84
|
+
expect(resolveDshHome(config(explicit))).toBe(explicit);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("lets the environment override every configuration", async () => {
|
|
88
|
+
const override = await tempDir();
|
|
89
|
+
process.env.DSH_APP_DSH_HOME = override;
|
|
90
|
+
expect(resolveDshHome(config("xdg"))).toBe(override);
|
|
91
|
+
expect(resolveDshHome(config("env"))).toBe(override);
|
|
92
|
+
|
|
93
|
+
process.env.DSH_APP_DSH_HOME = " ";
|
|
94
|
+
expect(resolveDshHome(config("env"))).toBeUndefined();
|
|
95
|
+
expect(resolveDshHome(config("xdg"))).toBe(join(xdgDataHome(), "dsh-custom"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects a non-absolute path that is not a known mode", () => {
|
|
99
|
+
delete process.env.DSH_APP_DSH_HOME;
|
|
100
|
+
|
|
101
|
+
expect(() => resolveDshHome(config(`.${sep}data`))).toThrow(
|
|
102
|
+
/dataDir must be xdg, env, or an absolute path/u,
|
|
103
|
+
);
|
|
104
|
+
expect(() => resolveDshHome(config("~/data"))).toThrow(
|
|
105
|
+
/dataDir must be xdg, env, or an absolute path/u,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|