dsh-x-core 0.1.22 → 0.1.23
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/assets/dsh-preview.png +0 -0
- package/assets/dsh.ico +0 -0
- package/assets/icon.ico +0 -0
- package/assets/tray.ico +0 -0
- package/assets/tray.png +0 -0
- package/core-npm.js +201 -183
- package/desktop-plugins.js +86 -0
- package/desktop.js +200 -0
- package/native-notify.js +74 -0
- package/package.json +1 -1
- package/packs/dsh-x-workbench/README.md +61 -0
- package/packs/dsh-x-workbench/dspack.json +4 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/LICENSE-APACHE-2.0.txt +202 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/NOTICE.md +6 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/SKILL.md +697 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/authentication.md +313 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/commands.md +300 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/profiling.md +120 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/proxy-support.md +194 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/session-management.md +201 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/snapshot-refs.md +229 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/references/video-recording.md +173 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/authenticated-session.sh +105 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/capture-workflow.sh +69 -0
- package/packs/dsh-x-workbench/home/skills/agent-browser/templates/form-automation.sh +62 -0
- package/packs/dsh-x-workbench/home/skills/electron/LICENSE-APACHE-2.0.txt +202 -0
- package/packs/dsh-x-workbench/home/skills/electron/NOTICE.md +6 -0
- package/packs/dsh-x-workbench/home/skills/electron/SKILL.md +243 -0
- package/packs/dsh-x-workbench/manifest.json +35 -0
- package/perf/patch-hooks.mjs +14 -1
- package/perf/plugin-profile.mjs +204 -0
- package/perf/register.mjs +4 -0
- package/platform.js +1 -1
- package/public/booting.html +475 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/icon.png +0 -0
- package/public/index.html +9277 -8176
- package/public/internal-tabs.js +45 -41
- package/public/launcher.css +285 -98
- package/public/mascot.js +1 -1
- package/public/theme.css +33 -6
- package/public/water.js +1 -1
- package/registry.js +38 -3
- package/server.js +1058 -118
- package/settings.js +78 -1
package/assets/dsh-preview.png
CHANGED
|
Binary file
|
package/assets/dsh.ico
CHANGED
|
Binary file
|
package/assets/icon.ico
CHANGED
|
Binary file
|
package/assets/tray.ico
CHANGED
|
Binary file
|
package/assets/tray.png
CHANGED
|
Binary file
|
package/core-npm.js
CHANGED
|
@@ -1,183 +1,201 @@
|
|
|
1
|
-
// 核心的 npm 分发:查 registry 的 dist-tags → 下载 tarball → 校验 → 解压。
|
|
2
|
-
//
|
|
3
|
-
// 安装包只负责外壳和 Electron;核心作为一个 npm 包独立发版,启动器在运行时从 registry
|
|
4
|
-
// 取新版本,解到暂存目录后仍走 apply-update.mjs 换核心目录。
|
|
5
|
-
// 核心包跨平台(三套 filemanager 和 node 包装都在里面),Windows / mac / Linux 用同一个
|
|
6
|
-
// dsh-x-core;只有外壳/Electron 按平台分开。
|
|
7
|
-
// 完整性用 npm 的 dist.integrity(sha512)校验;registry 走设置里的下载源(官方/镜像)。
|
|
8
|
-
|
|
9
|
-
import { spawnSync } from 'node:child_process'
|
|
10
|
-
import { createHash } from 'node:crypto'
|
|
11
|
-
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
|
|
14
|
-
import { netFetch } from './proxy.js'
|
|
15
|
-
import { cmpVer, parseVer, VERSION_CHANNEL } from './version.js'
|
|
16
|
-
|
|
17
|
-
/** 核心包跨平台:Windows / mac / Linux 共用同一个 dsh-x-core。 */
|
|
18
|
-
export const CORE_PACKAGE = String(process.env.DSH_CORE_PACKAGE || 'dsh-x-core').trim()
|
|
19
|
-
|
|
20
|
-
export const CORE_REGISTRIES = {
|
|
21
|
-
official: 'https://registry.npmjs.org',
|
|
22
|
-
mirror: 'https://registry.npmmirror.com',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 核心包上限:app 层约 15 MB,超过这个数八成是错误页或别的文件。 */
|
|
26
|
-
export const CORE_MAX_BYTES = 128 * 1024 * 1024
|
|
27
|
-
|
|
28
|
-
const CHANNEL_TAG = {
|
|
29
|
-
[VERSION_CHANNEL.OFFICIAL]: 'latest',
|
|
30
|
-
[VERSION_CHANNEL.RC]: 'rc',
|
|
31
|
-
[VERSION_CHANNEL.ALPHA]: 'alpha',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 测试通道可以显式指定 registry;否则按下载源在官方/镜像之间选。 */
|
|
35
|
-
export function coreRegistry(source = 'official') {
|
|
36
|
-
const override = String(process.env.DSH_CORE_REGISTRY || '').trim().replace(/\/+$/, '')
|
|
37
|
-
if (override) return override
|
|
38
|
-
return CORE_REGISTRIES[source] || CORE_REGISTRIES.official
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function coreMetadataUrl(registry, name = CORE_PACKAGE) {
|
|
42
|
-
return `${String(registry).replace(/\/+$/, '')}/${name.startsWith('@') ? name.replace('/', '%2F') : name}`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function coreDistTag(channel = VERSION_CHANNEL.OFFICIAL) {
|
|
46
|
-
return CHANNEL_TAG[channel] || 'latest'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 从 npm 元数据里挑当前通道对应的版本;挑不到返回 null。 */
|
|
50
|
-
export function resolveCoreRelease(metadata, channel = VERSION_CHANNEL.OFFICIAL) {
|
|
51
|
-
const tags = metadata?.['dist-tags'] || {}
|
|
52
|
-
const version = String(tags[coreDistTag(channel)] || tags.latest || '')
|
|
53
|
-
const info = version ? metadata?.versions?.[version] : null
|
|
54
|
-
if (!info) return null
|
|
55
|
-
const dist = info.dist || {}
|
|
56
|
-
return {
|
|
57
|
-
version,
|
|
58
|
-
tarball: String(dist.tarball || ''),
|
|
59
|
-
integrity: String(dist.integrity || ''),
|
|
60
|
-
shasum: String(dist.shasum || ''),
|
|
61
|
-
unpackedSize: Number(dist.unpackedSize) || 0,
|
|
62
|
-
// 发布脚本写进核心 package.json:这份核心是按哪个 Electron 版本构建/验证的
|
|
63
|
-
electron: String(info.dshX?.electron || ''),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function fetchCoreMetadata({ registry = coreRegistry(), name = CORE_PACKAGE, signal } = {}) {
|
|
68
|
-
const url = coreMetadataUrl(registry, name)
|
|
69
|
-
const res = await netFetch(url, {
|
|
70
|
-
redirect: 'follow',
|
|
71
|
-
headers: { accept: 'application/json', 'user-agent': 'dsh-x' },
|
|
72
|
-
signal,
|
|
73
|
-
})
|
|
74
|
-
if (!res.ok) throw new Error(`读取 ${name} 的 npm 元数据失败:HTTP ${res.status}`)
|
|
75
|
-
return res.json()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 查核心更新:返回 { available, current, latest, release }。 */
|
|
79
|
-
export async function checkCoreUpdate({ current, source = 'official', channel = VERSION_CHANNEL.OFFICIAL, signal } = {}) {
|
|
80
|
-
const metadata = await fetchCoreMetadata({ registry: coreRegistry(source), signal })
|
|
81
|
-
const release = resolveCoreRelease(metadata, channel)
|
|
82
|
-
if (!release) return { available: false, current, latest: '' }
|
|
83
|
-
const next = parseVer(release.version)
|
|
84
|
-
const cur = parseVer(current)
|
|
85
|
-
return { available: Boolean(next && cur && cmpVer(next, cur) > 0), current, latest: release.version, release }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
// 核心的 npm 分发:查 registry 的 dist-tags → 下载 tarball → 校验 → 解压。
|
|
2
|
+
//
|
|
3
|
+
// 安装包只负责外壳和 Electron;核心作为一个 npm 包独立发版,启动器在运行时从 registry
|
|
4
|
+
// 取新版本,解到暂存目录后仍走 apply-update.mjs 换核心目录。
|
|
5
|
+
// 核心包跨平台(三套 filemanager 和 node 包装都在里面),Windows / mac / Linux 用同一个
|
|
6
|
+
// dsh-x-core;只有外壳/Electron 按平台分开。
|
|
7
|
+
// 完整性用 npm 的 dist.integrity(sha512)校验;registry 走设置里的下载源(官方/镜像)。
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
11
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { netFetch } from './proxy.js'
|
|
15
|
+
import { cmpVer, parseVer, VERSION_CHANNEL } from './version.js'
|
|
16
|
+
|
|
17
|
+
/** 核心包跨平台:Windows / mac / Linux 共用同一个 dsh-x-core。 */
|
|
18
|
+
export const CORE_PACKAGE = String(process.env.DSH_CORE_PACKAGE || 'dsh-x-core').trim()
|
|
19
|
+
|
|
20
|
+
export const CORE_REGISTRIES = {
|
|
21
|
+
official: 'https://registry.npmjs.org',
|
|
22
|
+
mirror: 'https://registry.npmmirror.com',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 核心包上限:app 层约 15 MB,超过这个数八成是错误页或别的文件。 */
|
|
26
|
+
export const CORE_MAX_BYTES = 128 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
const CHANNEL_TAG = {
|
|
29
|
+
[VERSION_CHANNEL.OFFICIAL]: 'latest',
|
|
30
|
+
[VERSION_CHANNEL.RC]: 'rc',
|
|
31
|
+
[VERSION_CHANNEL.ALPHA]: 'alpha',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 测试通道可以显式指定 registry;否则按下载源在官方/镜像之间选。 */
|
|
35
|
+
export function coreRegistry(source = 'official') {
|
|
36
|
+
const override = String(process.env.DSH_CORE_REGISTRY || '').trim().replace(/\/+$/, '')
|
|
37
|
+
if (override) return override
|
|
38
|
+
return CORE_REGISTRIES[source] || CORE_REGISTRIES.official
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function coreMetadataUrl(registry, name = CORE_PACKAGE) {
|
|
42
|
+
return `${String(registry).replace(/\/+$/, '')}/${name.startsWith('@') ? name.replace('/', '%2F') : name}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function coreDistTag(channel = VERSION_CHANNEL.OFFICIAL) {
|
|
46
|
+
return CHANNEL_TAG[channel] || 'latest'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 从 npm 元数据里挑当前通道对应的版本;挑不到返回 null。 */
|
|
50
|
+
export function resolveCoreRelease(metadata, channel = VERSION_CHANNEL.OFFICIAL) {
|
|
51
|
+
const tags = metadata?.['dist-tags'] || {}
|
|
52
|
+
const version = String(tags[coreDistTag(channel)] || tags.latest || '')
|
|
53
|
+
const info = version ? metadata?.versions?.[version] : null
|
|
54
|
+
if (!info) return null
|
|
55
|
+
const dist = info.dist || {}
|
|
56
|
+
return {
|
|
57
|
+
version,
|
|
58
|
+
tarball: String(dist.tarball || ''),
|
|
59
|
+
integrity: String(dist.integrity || ''),
|
|
60
|
+
shasum: String(dist.shasum || ''),
|
|
61
|
+
unpackedSize: Number(dist.unpackedSize) || 0,
|
|
62
|
+
// 发布脚本写进核心 package.json:这份核心是按哪个 Electron 版本构建/验证的
|
|
63
|
+
electron: String(info.dshX?.electron || ''),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchCoreMetadata({ registry = coreRegistry(), name = CORE_PACKAGE, signal } = {}) {
|
|
68
|
+
const url = coreMetadataUrl(registry, name)
|
|
69
|
+
const res = await netFetch(url, {
|
|
70
|
+
redirect: 'follow',
|
|
71
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-x' },
|
|
72
|
+
signal,
|
|
73
|
+
})
|
|
74
|
+
if (!res.ok) throw new Error(`读取 ${name} 的 npm 元数据失败:HTTP ${res.status}`)
|
|
75
|
+
return res.json()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 查核心更新:返回 { available, current, latest, release }。 */
|
|
79
|
+
export async function checkCoreUpdate({ current, source = 'official', channel = VERSION_CHANNEL.OFFICIAL, signal } = {}) {
|
|
80
|
+
const metadata = await fetchCoreMetadata({ registry: coreRegistry(source), signal })
|
|
81
|
+
const release = resolveCoreRelease(metadata, channel)
|
|
82
|
+
if (!release) return { available: false, current, latest: '' }
|
|
83
|
+
const next = parseVer(release.version)
|
|
84
|
+
const cur = parseVer(current)
|
|
85
|
+
return { available: Boolean(next && cur && cmpVer(next, cur) > 0), current, latest: release.version, release }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 一次元数据查询里看多个通道,取高于当前版本里最高的那个(「接受预览版」开关用)。
|
|
90
|
+
* 只查一次 registry:三个 dist-tag 在同一份文档里,分三次查纯属浪费。
|
|
91
|
+
*/
|
|
92
|
+
export async function checkCoreUpdateAny({ current, source = 'official', channels = [], signal } = {}) {
|
|
93
|
+
const metadata = await fetchCoreMetadata({ registry: coreRegistry(source), signal })
|
|
94
|
+
const cur = parseVer(current)
|
|
95
|
+
let best = null
|
|
96
|
+
for (const channel of channels) {
|
|
97
|
+
const release = resolveCoreRelease(metadata, channel)
|
|
98
|
+
const next = release ? parseVer(release.version) : null
|
|
99
|
+
if (!next || !cur || cmpVer(next, cur) <= 0) continue
|
|
100
|
+
if (!best || cmpVer(next, parseVer(best.version)) > 0) best = release
|
|
101
|
+
}
|
|
102
|
+
if (!best) return { available: false, current, latest: '' }
|
|
103
|
+
return { available: true, current, latest: best.version, release: best }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 下载 tarball,边下边算 sha512;有 integrity 就按 npm 的 SRI 校验,否则退回 sha1。 */
|
|
107
|
+
export async function downloadCoreTarball(url, dest, { integrity = '', shasum = '', signal, onProgress, limit = CORE_MAX_BYTES } = {}) {
|
|
108
|
+
if (!url) throw new Error('核心包没有下载地址')
|
|
109
|
+
const res = await netFetch(url, { redirect: 'follow', signal })
|
|
110
|
+
if (!res.ok) throw new Error(`下载核心包失败:HTTP ${res.status}`)
|
|
111
|
+
const total = Number(res.headers.get('content-length')) || 0
|
|
112
|
+
if (total > limit) throw new Error(`核心包太大(${Math.round(total / 1048576)} MB,上限 ${Math.round(limit / 1048576)} MB)`)
|
|
113
|
+
const hash = createHash('sha512')
|
|
114
|
+
const out = createWriteStream(dest, { flags: 'w' })
|
|
115
|
+
let done = 0
|
|
116
|
+
const closed = new Promise((resolve, reject) => {
|
|
117
|
+
out.once('close', resolve)
|
|
118
|
+
out.once('error', reject)
|
|
119
|
+
})
|
|
120
|
+
try {
|
|
121
|
+
for await (const chunk of res.body) {
|
|
122
|
+
signal?.throwIfAborted()
|
|
123
|
+
done += chunk.length
|
|
124
|
+
if (done > limit) throw new Error('核心包太大,已停下')
|
|
125
|
+
hash.update(chunk)
|
|
126
|
+
if (!out.write(chunk)) {
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
out.once('drain', resolve)
|
|
129
|
+
out.once('error', reject)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
onProgress?.({ done, total })
|
|
133
|
+
}
|
|
134
|
+
out.end()
|
|
135
|
+
await closed
|
|
136
|
+
} catch (error) {
|
|
137
|
+
out.destroy()
|
|
138
|
+
await closed.catch(() => {})
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
141
|
+
const digest = hash.digest('base64')
|
|
142
|
+
if (integrity) {
|
|
143
|
+
const expected = String(integrity).replace(/^sha512-/, '')
|
|
144
|
+
if (digest !== expected) throw new Error('核心包的 sha512 校验失败')
|
|
145
|
+
} else if (shasum) {
|
|
146
|
+
const sha1 = createHash('sha1').update(readFileSync(dest)).digest('hex')
|
|
147
|
+
if (sha1 !== String(shasum).toLowerCase()) throw new Error('核心包的 sha1 校验失败')
|
|
148
|
+
}
|
|
149
|
+
return { bytes: done, sha512: digest }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 解压用的 tar:Windows 上用系统自带的 bsdtar(Git 的 GNU tar 会把 C: 当远程主机)。 */
|
|
153
|
+
export function tarCommand() {
|
|
154
|
+
if (process.platform === 'win32') {
|
|
155
|
+
const system = join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
156
|
+
return existsSync(system) ? system : 'tar'
|
|
157
|
+
}
|
|
158
|
+
return '/usr/bin/tar'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 在 Windows 上打的 tarball 里 POSIX 包装可能没有可执行位;解到 mac 后补上。 */
|
|
162
|
+
function ensurePosixShims(dest) {
|
|
163
|
+
if (process.platform === 'win32') return
|
|
164
|
+
for (const name of ['node', 'npm', 'npx', 'pnpm', 'pnpx']) {
|
|
165
|
+
try { chmodSync(join(dest, 'node', name), 0o755) } catch { /* 没有就跳过 */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** npm tarball 里所有文件都在 package/ 下;解包时剥掉这一层。 */
|
|
170
|
+
export function extractCoreTarball(tarball, dest) {
|
|
171
|
+
rmSync(dest, { recursive: true, force: true })
|
|
172
|
+
mkdirSync(dest, { recursive: true })
|
|
173
|
+
const result = spawnSync(tarCommand(), ['-xzf', tarball, '-C', dest, '--strip-components=1'], { windowsHide: true, encoding: 'utf8' })
|
|
174
|
+
if (result.status !== 0) throw new Error(`解压核心包失败:${String(result.stderr || result.status).trim()}`)
|
|
175
|
+
if (!existsSync(join(dest, 'server.js')) || !existsSync(join(dest, 'package.json'))) {
|
|
176
|
+
throw new Error('核心包里缺少 server.js 或 package.json')
|
|
177
|
+
}
|
|
178
|
+
ensurePosixShims(dest)
|
|
179
|
+
return JSON.parse(readFileSync(join(dest, 'package.json'), 'utf8'))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function stagingExists(dir, marker = 'server.js') {
|
|
183
|
+
return Boolean(dir) && existsSync(join(dir, marker))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function stagingStats(dir) {
|
|
187
|
+
let files = 0
|
|
188
|
+
let bytes = 0
|
|
189
|
+
const walk = (current) => {
|
|
190
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
191
|
+
const path = join(current, entry.name)
|
|
192
|
+
if (entry.isDirectory()) walk(path)
|
|
193
|
+
else if (entry.isFile()) {
|
|
194
|
+
files += 1
|
|
195
|
+
bytes += statSync(path).size
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
walk(dir)
|
|
200
|
+
return { files, bytes }
|
|
201
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH-X 桌面增强:随启动器自动装进 web profile 的三个首方插件。
|
|
3
|
+
*
|
|
4
|
+
* 它们是普通的 dsh 插件(host / client 分开),只有装进 profile 才会加载。启动器在每次
|
|
5
|
+
* 拉起实例前检查一遍,缺哪个就补哪个;装法复用插件页那条链路(`dsh plugin add` + 写
|
|
6
|
+
* `dsh.profile.bundles`),所以从 npm 装、能在插件页更新。
|
|
7
|
+
*
|
|
8
|
+
* 只看「装没装」,不看行是否启用:用户在 dsh 插件页关掉的插件不会被这里重新打开。
|
|
9
|
+
*
|
|
10
|
+
* 纯函数(不读盘、不起进程),单测见 test/desktop-plugins.test.mjs;真正的安装留在
|
|
11
|
+
* server.js 的 addPlugin。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 三个首方插件。
|
|
16
|
+
*
|
|
17
|
+
* `spec` 钉精确版本:核心的 safeSpec 不接受 `^`;也不用 `latest`——新包首发会先出一个
|
|
18
|
+
* `0.0.0-stage` 占位版本,`@latest` 在那段时间装到的是空壳。升级交给插件页(它按
|
|
19
|
+
* registry 上的最新版本 add),这里只负责把缺的装上。
|
|
20
|
+
*
|
|
21
|
+
* `minDsh` 是插件 dsh.compatibility.dsh 的下限;同一版本的预发布(0.2.1-rc.4)算满足。
|
|
22
|
+
*/
|
|
23
|
+
export const DESKTOP_PLUGINS = Object.freeze([
|
|
24
|
+
{ name: 'dsh-x-desktop-notify', spec: '0.1.1', label: '原生通知', minDsh: '0.2.0' },
|
|
25
|
+
{ name: 'dsh-x-context-menu', spec: '0.1.1', label: '右键菜单', minDsh: '0.2.0' },
|
|
26
|
+
{ name: 'dsh-x-sidebar-browser', spec: '0.1.1', label: '侧栏浏览器控制', minDsh: '0.2.1-rc.2' },
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** dsh 自己的 desktop profile 归 dsh-desktop 管,DSH-X 不往里塞插件(同 --patch 的例外)。 */
|
|
30
|
+
export const DESKTOP_PLUGINS_SKIP_PROFILES = Object.freeze(['desktop'])
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `x.y.z` / `x.y.z-rc.1` → `[x, y, z]`;认不出来返回 null。
|
|
34
|
+
* @param {string} text - dsh 版本。
|
|
35
|
+
* @returns {number[] | null} 主次修订三个数字。
|
|
36
|
+
*/
|
|
37
|
+
export function parseDshVersion(text) {
|
|
38
|
+
const match = String(text ?? '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/)
|
|
39
|
+
if (!match) return null
|
|
40
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 版本够不够插件要求的下限。
|
|
45
|
+
*
|
|
46
|
+
* 只比主次修订:同一版本的预发布(`0.2.1-rc.4` 对 `0.2.1-rc.2`)算满足——插件的
|
|
47
|
+
* compatibility 写的是「从哪个版本开始有这些接口」,预发布同样有。
|
|
48
|
+
*
|
|
49
|
+
* @param {string} version - 要启动的 dsh 版本。
|
|
50
|
+
* @param {string} min - 插件要求的最低版本。
|
|
51
|
+
* @returns {boolean} 够不够;任一边认不出来都算不够(宁可不装,也不装坏一个 profile)。
|
|
52
|
+
*/
|
|
53
|
+
export function versionAtLeast(version, min) {
|
|
54
|
+
const left = parseDshVersion(version)
|
|
55
|
+
const right = parseDshVersion(min)
|
|
56
|
+
if (!left || !right) return false
|
|
57
|
+
for (let i = 0; i < 3; i += 1) {
|
|
58
|
+
if (left[i] > right[i]) return true
|
|
59
|
+
if (left[i] < right[i]) return false
|
|
60
|
+
}
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 这个 dsh 版本能装的桌面增强插件。
|
|
66
|
+
* @param {string} version - 要启动的 dsh 版本。
|
|
67
|
+
* @returns {{ name: string, spec: string, label: string, minDsh: string }[]} 子集。
|
|
68
|
+
*/
|
|
69
|
+
export function desktopPluginsForDsh(version) {
|
|
70
|
+
return DESKTOP_PLUGINS.filter((plugin) => versionAtLeast(version, plugin.minDsh))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* profile 还缺哪些桌面增强插件。
|
|
75
|
+
*
|
|
76
|
+
* 已经写在 `dependencies` 里就算装了(不管是 registry 版本、`link:` 还是 `file:`):
|
|
77
|
+
* 开发机上 link 的本地包不该被换成 npm 版。
|
|
78
|
+
*
|
|
79
|
+
* @param {object} manifest - profile 的 package.json 内容。
|
|
80
|
+
* @param {string} version - 要启动的 dsh 版本。
|
|
81
|
+
* @returns {{ name: string, spec: string, label: string, minDsh: string }[]} 需要安装的插件。
|
|
82
|
+
*/
|
|
83
|
+
export function desktopPluginsToInstall(manifest, version) {
|
|
84
|
+
const dependencies = manifest?.dependencies && typeof manifest.dependencies === 'object' ? manifest.dependencies : {}
|
|
85
|
+
return desktopPluginsForDsh(version).filter((plugin) => !Object.hasOwn(dependencies, plugin.name))
|
|
86
|
+
}
|