dsh-crossterm 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +24 -22
- package/client/client.js +10 -2
- package/cordis.patch.yml +24 -19
- package/lib/bus.js +1 -1
- package/lib/cloud/home.js +10 -1
- package/lib/index.js +18 -1
- package/lib/web.js +141 -28
- package/locale/en.json +3 -2
- package/locale/zh.json +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.7.1 — 2026-10-19
|
|
4
|
+
|
|
5
|
+
### 「更新」按钮在别的机器上装不上:三级兜底 + 全步骤回执
|
|
6
|
+
|
|
7
|
+
- 现象(User said, paraphrased):别的设备点卡片里的「更新」,看起来装不上。
|
|
8
|
+
- 根因:旧实现只有两条路 —— PATH 里的 `dsh`,或 `%USERPROFILE%\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\{node,pnpm}`
|
|
9
|
+
这一固定目录。桌面版默认**不把 `dsh` 放进 PATH**,别的机器也不一定有这套运行时目录,
|
|
10
|
+
于是直接落到「找不到可用的 dsh 命令」,卡片只显示一行字,看不出卡在哪一步。
|
|
11
|
+
- 桌面版的 `dsh` 真身(`F:\dsh\resources\runtime\cli\bin\dsh.cmd`):
|
|
12
|
+
`set ELECTRON_RUN_AS_NODE=1` + `DeepSeek Harness.exe --expose-internals <resources>\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js %*`,
|
|
13
|
+
而 `dsh plugin ...` 内部就是在 profile 目录里跑 pnpm(`--profile` 必填)。所以不依赖 PATH 也能调起同一个 CLI。
|
|
14
|
+
- 现行三级兜底(`lib/web.js` 的 `updatePlugin(term, info, latest)`,按顺序尝试,第一个成功即止,每步都记档):
|
|
15
|
+
1. `dsh plugin --profile <p> add dsh-crossterm@latest`;
|
|
16
|
+
2. DSH 自带 CLI:`process.execPath --expose-internals <resources>\app.asar\...\cli.js plugin --profile <p> add ...`
|
|
17
|
+
(env 加 `ELECTRON_RUN_AS_NODE=1`);
|
|
18
|
+
3. 直接 pnpm:`pnpmCandidates()` 依次找 `<DSH_HOME>\dsh-runtimes\...\dependencies\pnpm\bin\pnpm.cjs`
|
|
19
|
+
与 `<安装目录>\resources\runtime\pnpm\bin\pnpm.cjs`;node 用自带 `node.exe`,没有就拿宿主 Electron 当 node。
|
|
20
|
+
- 每步记录 `{label, cmd, code, ms, out}`;装完读 `<profile>\node_modules\dsh-crossterm\package.json` **核对真实版本**,
|
|
21
|
+
回执区分「已更新到 X,重启 DSH 后生效」「已经是最新(X)」「更新没成功(当前装着 X)」。
|
|
22
|
+
- 卡片:失败时把每步退出码、耗时、输出尾部(各 400 字)直接显示在卡片里,不用去翻宿主日志。
|
|
23
|
+
- 更新日志:`<busDir>\logs\update.log` 追加 JSON 行(含 profile / before / installed / latest / steps),
|
|
24
|
+
别的会话排查时直接读这个文件。
|
|
25
|
+
- 顺带补 0.7.0 漏的字段:`GET /crossterm/api/status` 的 `home.layout` 现在带 `rulesVersion` / `rulesFailed` / `accountSource`。
|
|
26
|
+
- 探针 `F:\dsh\001X\crossterm-work\probe-update-plan.mjs` **14 项全 PASS**。
|
|
27
|
+
- 要不要跟着动手:**不用**,重启 DSH 即生效(本机是 `link:` 源码版,重启加载即可)。
|
|
28
|
+
|
|
29
|
+
## 0.7.0 — 2026-10-19
|
|
30
|
+
|
|
31
|
+
### 入口行升格为「总开关」,排到组件列表第一位
|
|
32
|
+
- 用户要求(paraphrased):把入口那一行放到第一个,开关做成和插件开关一样的**总开关**,并标注清楚。
|
|
33
|
+
- `cordis.patch.yml` 顺序变化:`crossterm-ui`(裸包名 `dsh-crossterm`,`config.part: ui`)从列表末尾
|
|
34
|
+
移到**第一位**,其后依次是 session / terminal / device / account。五行指向同一个包,顺序只影响展示。
|
|
35
|
+
- **总开关是真开关**,不是只关卡片(`lib/index.js` 的 `apply()`):
|
|
36
|
+
- `part === 'ui'` 时 `root.provide('crossterm', { master: true, part, since })`;
|
|
37
|
+
- 其余四个组件改为 `root.inject(['agents', 'crossterm'], ...)` —— **总开关关掉时服务不存在,
|
|
38
|
+
这四个回调根本不会执行**,工具、心跳、云同步、云端中转站、会话备份一起停;
|
|
39
|
+
- 总开关开着时,四个组件之间仍可各自单独开关;
|
|
40
|
+
- 总开关 3 秒内没出现时打一行 `console.warn`(`总开关(入口组件)未启用,组件「X」已跳过挂载`)方便排查;
|
|
41
|
+
- 用 cordis 服务注入而不是「读配置 / 看加载顺序」,所以谁先加载都不影响结果。
|
|
42
|
+
- 标注清楚:`locale/zh.json` 的 `meta.title` 改为 **跨端互通(总开关)**,`meta.description` 写明
|
|
43
|
+
「关掉这一项,下面四个组件全部停用……开着它时四个组件可各自单独开关」;
|
|
44
|
+
新增 `meta.shortTitle = 跨端互通`,配置卡片标题用短名(`GET /crossterm/api/about` 多返回一个 `shortTitle`)。
|
|
45
|
+
- 探针 `probe-about.mjs` 改为断言 `title === '跨端互通(总开关)'` 且 `shortTitle === '跨端互通'`。
|
|
46
|
+
|
|
3
47
|
## 0.6.2 — 2026-10-19
|
|
4
48
|
|
|
5
49
|
### 插件页:插件名/简介/组件表/更新日志
|
package/README.md
CHANGED
|
@@ -12,28 +12,30 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
12
12
|
|
|
13
13
|
## 零、简介与本版更新(每次发版重写这一节)
|
|
14
14
|
|
|
15
|
-
**插件名称**:`dsh-crossterm`(插件页显示
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| 组件名称 |
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
15
|
+
**插件名称**:`dsh-crossterm`(插件页显示 **跨端互通(总开关)**)| **插件简介**:同一 DSH 账号下的会话、终端、设备之间互通 —— 特征码链、内联邮件、云端中转站与自动备份。
|
|
16
|
+
|
|
17
|
+
**组件列表(插件页第一个是总开关,后面四个是业务组件,名称与简介来自各自的 manifest)**
|
|
18
|
+
|
|
19
|
+
| 顺序 | 组件名称 | 作用 | 工具 |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| ① | 跨端互通(总开关) | 插件总开关:关掉它,下面四个组件全部停用(工具、心跳、云同步、中转站、备份一起停);开着时四个组件可各自单独开关。裸包名行,承载插件标题/简介与配置卡片,不挂工具 | — |
|
|
22
|
+
| ② | 跨会话 | 同一终端里的多个会话之间互通:特征码链、内联邮件、收件箱 | `term_link` `term_unlink` `term_broadcast` `term_inbox` `term_config` |
|
|
23
|
+
| ③ | 跨终端 | 同一台机器上的多个 DSH 终端/进程之间互发邮件与文件 | `term_whoami` `term_list` `term_send` `term_send_files` |
|
|
24
|
+
| ④ | 跨设备 | 经云端总线,在多台机器的 DSH 之间互发邮件与文件 | `term_cloud` |
|
|
25
|
+
| ⑤ | 跨账号 | 预留位:后续用于跨账号互通,当前不挂工具 | — |
|
|
26
|
+
|
|
27
|
+
> 总开关为什么必须用裸包名:浏览器 client 半端只对裸包名行生效(`dsh-client-modules` 的
|
|
28
|
+
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
|
+
> 所以它单独做总开关,标题带「(总开关)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
|
+
|
|
31
|
+
**本版更新(0.7.1)**
|
|
32
|
+
|
|
33
|
+
- **修「别的机器点更新装不上」**:更新不再只依赖 PATH 里的 `dsh`,改成三级兜底 ——
|
|
34
|
+
① PATH 里的 `dsh`;② DSH 自带 CLI(`Electron --expose-internals <安装目录>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js`);
|
|
35
|
+
③ 直接调自带 pnpm(`<DSH_HOME>\dsh-runtimes\...\dependencies\pnpm` 或 `<安装目录>\resources\runtime\pnpm`)。
|
|
36
|
+
- 装完会读 `<profile>\node_modules\dsh-crossterm\package.json` **核对真实版本**,分别提示「已更新到 X」「已经是最新」「更新没成功」。
|
|
37
|
+
- 失败时卡片直接把每步的退出码、耗时、输出尾部显示出来;全过程写 `<总线目录>\logs\update.log`。
|
|
38
|
+
- 顺带补上 `status.home.layout` 里缺的 `rulesVersion` / `rulesFailed` / `accountSource`。
|
|
37
39
|
|
|
38
40
|
## 一、安装与卸载
|
|
39
41
|
|
package/client/client.js
CHANGED
|
@@ -255,7 +255,15 @@ window.__ModuleLoader__.load({
|
|
|
255
255
|
setMsg("正在更新…");
|
|
256
256
|
try {
|
|
257
257
|
const data = await call("/crossterm/api/update", "POST");
|
|
258
|
-
|
|
258
|
+
const text = (data && data.message) || (data && data.ok ? "已更新" : "未能更新");
|
|
259
|
+
const steps = (data && data.steps) || [];
|
|
260
|
+
// 失败时把每条尝试(dsh / DSH 自带 cli / pnpm)的退出码与输出带上,方便远端排查
|
|
261
|
+
const detail = data && data.ok
|
|
262
|
+
? ""
|
|
263
|
+
: steps.length
|
|
264
|
+
? "\n\n" + steps.map((s) => `[${s.label}] 退出码 ${s.code} · ${Math.round((s.ms || 0) / 1000)}s` + (s.out ? `\n${String(s.out).slice(-400)}` : "")).join("\n\n")
|
|
265
|
+
: "";
|
|
266
|
+
setMsg(text + detail);
|
|
259
267
|
} catch (error) {
|
|
260
268
|
setMsg("更新失败:" + String((error && error.message) || error));
|
|
261
269
|
} finally {
|
|
@@ -373,7 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
373
381
|
h(
|
|
374
382
|
"div",
|
|
375
383
|
null,
|
|
376
|
-
h("div", { style: S.title }, `${(about && about.title) || "跨端互通"} ${version}`),
|
|
384
|
+
h("div", { style: S.title }, `${(about && (about.shortTitle || about.title)) || "跨端互通"} ${version}`),
|
|
377
385
|
h("div", { style: S.sub }, subtitle),
|
|
378
386
|
),
|
|
379
387
|
// h("span", { style: S.chevron }, open ? "\u2303" : "\u2304"),
|
package/cordis.patch.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# ============================================================================
|
|
2
|
-
# dsh-crossterm —— DSH bundle patch
|
|
2
|
+
# dsh-crossterm —— DSH bundle patch
|
|
3
3
|
# ============================================================================
|
|
4
4
|
# 安装(本地目录,对每个要互通的 profile 各装一次):
|
|
5
5
|
# dsh plugin --profile desktop add link:F:\dsh\001X\dsh-crossterm
|
|
@@ -7,30 +7,41 @@
|
|
|
7
7
|
# 卸载:
|
|
8
8
|
# dsh plugin --profile desktop remove dsh-crossterm
|
|
9
9
|
#
|
|
10
|
-
# 每个 insert 行在 DSH「插件 →
|
|
11
|
-
#
|
|
10
|
+
# 每个 insert 行在 DSH「插件 → 跨端互通 → 包含的组件」里是一行组件,可单独开关。
|
|
11
|
+
# 五行指向同一个包,只是走不同子路径 / 不同 config.part:
|
|
12
12
|
#
|
|
13
|
-
# dsh-crossterm
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# dsh-crossterm/parts/
|
|
18
|
-
# dsh-crossterm/parts/
|
|
13
|
+
# ① dsh-crossterm 总开关(入口):提供 crossterm 服务,插件页标题/简介/配置卡片,
|
|
14
|
+
# 不挂任何工具(config.part: ui)。
|
|
15
|
+
# 关掉它 → 服务消失 → 下面四个组件全部停用
|
|
16
|
+
# (工具、心跳、云同步、中转站、会话备份一起停)。
|
|
17
|
+
# ② dsh-crossterm/parts/session 跨会话:term_link / term_unlink / term_broadcast / term_inbox / term_config
|
|
18
|
+
# ③ dsh-crossterm/parts/terminal 跨终端:term_whoami / term_list / term_send / term_send_files
|
|
19
|
+
# ④ dsh-crossterm/parts/device 跨设备:term_cloud(云端总线同步)
|
|
20
|
+
# ⑤ dsh-crossterm/parts/account 跨账号:预留占位,暂不挂工具
|
|
19
21
|
#
|
|
20
22
|
# 组件名来自 package.json 的 exports 映射:
|
|
21
23
|
# "./parts/session/package.json" -> "./parts/session.manifest.json"
|
|
22
24
|
# 每个组件显示自己的中文名,实际加载的仍是 lib/index.js,靠 config.part 决定挂哪组工具。
|
|
23
25
|
#
|
|
24
|
-
#
|
|
25
|
-
# locatePkgJson 对子路径行直接返回 undefined
|
|
26
|
-
# locale/zh.json 的 meta ——
|
|
27
|
-
#
|
|
26
|
+
# 为什么总开关必须用「裸包名」:浏览器的 client 半端只对裸包名行生效
|
|
27
|
+
# (dsh-client-modules 的 locatePkgJson 对子路径行直接返回 undefined),而插件页顶部标题
|
|
28
|
+
# 与裸包名行共用同一份 locale/zh.json 的 meta —— 所以裸包名行单独做总开关,
|
|
29
|
+
# 标题写「跨端互通(总开关)」,四个业务组件排在它后面、名字仍是跨会话/跨终端/跨设备/跨账号。
|
|
30
|
+
#
|
|
31
|
+
# 总开关是「服务注入」实现的:ui 行 root.provide('crossterm'),其余四行 root.inject 它。
|
|
32
|
+
# 谁先加载不影响结果(cordis 的 inject 会等服务出现),顺序只为让列表好看。
|
|
28
33
|
#
|
|
29
34
|
# 五个组件在同一 DSH 进程里共用一份总线实例:心跳只起一次,云同步只在 device 组件启用
|
|
30
35
|
# 且 config.json 里 cloudEnabled=true 时才跑。安装/改动后需重启对应的 DSH 进程。
|
|
31
36
|
# ============================================================================
|
|
32
37
|
|
|
33
38
|
- insert:
|
|
39
|
+
- id: crossterm-ui
|
|
40
|
+
# 总开关(入口):裸包名,提供 crossterm 服务 + 浏览器半端的配置卡片 + 插件页标题/简介。
|
|
41
|
+
# 关掉这一行等于整插件停用;它自己不挂工具。
|
|
42
|
+
name: dsh-crossterm
|
|
43
|
+
config:
|
|
44
|
+
part: ui
|
|
34
45
|
- id: crossterm-session
|
|
35
46
|
name: dsh-crossterm/parts/session
|
|
36
47
|
config:
|
|
@@ -47,9 +58,3 @@
|
|
|
47
58
|
name: dsh-crossterm/parts/account
|
|
48
59
|
config:
|
|
49
60
|
part: account
|
|
50
|
-
- id: crossterm-ui
|
|
51
|
-
# 裸包名:浏览器 client 半端只认这一行,插件页配置卡片挂在这里。
|
|
52
|
-
# 也是插件页顶部标题/简介的来源(locale/zh.json 的 meta)。
|
|
53
|
-
name: dsh-crossterm
|
|
54
|
-
config:
|
|
55
|
-
part: ui
|
package/lib/bus.js
CHANGED
package/lib/cloud/home.js
CHANGED
|
@@ -119,7 +119,16 @@ export function createCloudHome({ cfg, device, pan, busDir, deliver, names, log
|
|
|
119
119
|
return {
|
|
120
120
|
home: `${cfg.cloudHome || DEFAULT_HOME}`,
|
|
121
121
|
layout: layout
|
|
122
|
-
? {
|
|
122
|
+
? {
|
|
123
|
+
account: layout.account,
|
|
124
|
+
role: layout.role,
|
|
125
|
+
deviceId: layout.deviceId,
|
|
126
|
+
paths: layout.paths,
|
|
127
|
+
ensuredAt: layout.ensuredAt,
|
|
128
|
+
rulesVersion: layout.rulesVersion || 0,
|
|
129
|
+
rulesFailed: layout.rulesFailed || 0,
|
|
130
|
+
accountSource: layout.accountSource || '',
|
|
131
|
+
}
|
|
123
132
|
: null,
|
|
124
133
|
relay: { ...relay.stats(), intervalMs: relayTimer },
|
|
125
134
|
backup: { ...backupSummary(backupState), intervalMs: backupTimer },
|
package/lib/index.js
CHANGED
|
@@ -1859,7 +1859,24 @@ export default {
|
|
|
1859
1859
|
name: PLUGIN,
|
|
1860
1860
|
apply(root, config) {
|
|
1861
1861
|
const part = normalizePart(config?.part)
|
|
1862
|
-
|
|
1862
|
+
// 「入口」这一行就是总开关:它提供 crossterm 服务,其余四个组件都 inject 它。
|
|
1863
|
+
// 关掉这一行 → 服务不存在 → 另外四个组件的 inject 回调根本不会执行,
|
|
1864
|
+
// 工具、心跳、云同步、中转站、会话备份一起停(不是只关卡片)。
|
|
1865
|
+
// 反过来,只要总开关开着,四个组件各自还能单独关。
|
|
1866
|
+
const isMaster = part === 'ui'
|
|
1867
|
+
if (isMaster) {
|
|
1868
|
+
root.provide('crossterm', { master: true, part, since: Date.now() })
|
|
1869
|
+
} else {
|
|
1870
|
+
const timer = setTimeout(() => {
|
|
1871
|
+
let alive = true
|
|
1872
|
+
try {
|
|
1873
|
+
alive = Boolean(root.get?.('crossterm'))
|
|
1874
|
+
} catch { alive = true }
|
|
1875
|
+
if (!alive) console.warn(`[dsh-crossterm] 总开关(入口组件)未启用,组件「${PART_LABEL[part]}」已跳过挂载`)
|
|
1876
|
+
}, 3000)
|
|
1877
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
1878
|
+
}
|
|
1879
|
+
root.inject(isMaster ? ['agents'] : ['agents', 'crossterm'], (ctx) => {
|
|
1863
1880
|
// 四个组件跑在同一个进程里,共用一份总线实例:各自只挂自己那组工具,
|
|
1864
1881
|
// 心跳/云同步只在第一个组件加载时启动一次,最后一个组件卸载时才停。
|
|
1865
1882
|
const term = acquireTerm(ctx)
|
package/lib/web.js
CHANGED
|
@@ -163,6 +163,7 @@ function run(cmd, args, cwd, opts) {
|
|
|
163
163
|
cwd,
|
|
164
164
|
shell,
|
|
165
165
|
windowsHide: true,
|
|
166
|
+
env: opts && opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
166
167
|
})
|
|
167
168
|
} catch (error) {
|
|
168
169
|
resolve({ code: -1, out: String((error && error.message) || error) })
|
|
@@ -197,19 +198,46 @@ function updateErrorText(result) {
|
|
|
197
198
|
return tail
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// DSH 安装根目录:process.resourcesPath = <root>\resources,上一级就是 <root>
|
|
202
|
+
function dshRoot() {
|
|
203
|
+
if (process.resourcesPath) return path.dirname(process.resourcesPath)
|
|
204
|
+
return path.dirname(process.execPath || '')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 桌面版把 `dsh` 命令实现成「让 Electron 以 node 模式跑 cli.js」:
|
|
208
|
+
// <root>\DeepSeek Harness.exe --expose-internals <root>\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js
|
|
209
|
+
// 所以不依赖 PATH 也能调起同一个 CLI(`dsh plugin add` 内部其实就是 pnpm add)。
|
|
210
|
+
function cliScript() {
|
|
211
|
+
const res = process.resourcesPath || path.join(dshRoot(), 'resources')
|
|
212
|
+
return path.join(res, 'app.asar', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function dshHome(cfg) {
|
|
203
216
|
const busDir = (cfg && cfg.busDir) || ''
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
return busDir ? path.dirname(busDir) : path.join(os.homedir(), '.dsh')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// pnpm 可能落在两处:DSH_HOME 的运行时依赖里,或安装目录的 resources\runtime\pnpm
|
|
221
|
+
function pnpmCandidates(cfg) {
|
|
222
|
+
const deps = path.join(dshHome(cfg), 'dsh-runtimes', 'dsh-primary-runtime', 'dependencies')
|
|
223
|
+
const res = process.resourcesPath || path.join(dshRoot(), 'resources')
|
|
224
|
+
return [
|
|
225
|
+
path.join(deps, 'pnpm', 'bin', 'pnpm.cjs'),
|
|
226
|
+
path.join(res, 'runtime', 'pnpm', 'bin', 'pnpm.cjs'),
|
|
227
|
+
].filter((p) => p && fs.existsSync(p))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 没有独立 node.exe 时用宿主自己的 Electron 以 node 模式跑脚本
|
|
231
|
+
function nodeTool(cfg) {
|
|
232
|
+
const deps = path.join(dshHome(cfg), 'dsh-runtimes', 'dsh-primary-runtime', 'dependencies')
|
|
233
|
+
const bundled = path.join(deps, 'node', 'bin', 'node.exe')
|
|
234
|
+
if (fs.existsSync(bundled)) return { cmd: bundled, env: {} }
|
|
235
|
+
return { cmd: process.execPath, env: { ELECTRON_RUN_AS_NODE: '1' } }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function runtimeTools(cfg) {
|
|
239
|
+
const pnpm = pnpmCandidates(cfg)[0] || ''
|
|
240
|
+
return { node: nodeTool(cfg).cmd, pnpm, ok: Boolean(pnpm) }
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
function looksMissingCommand(result) {
|
|
@@ -218,16 +246,91 @@ function looksMissingCommand(result) {
|
|
|
218
246
|
)
|
|
219
247
|
}
|
|
220
248
|
|
|
221
|
-
|
|
249
|
+
/** 读 profile 里实际装着的版本(registry 模式下 node_modules/<pkg>/package.json) */
|
|
250
|
+
function installedVersion(info) {
|
|
251
|
+
try {
|
|
252
|
+
const doc = JSON.parse(fs.readFileSync(path.join(info.dir, 'node_modules', PKG, 'package.json'), 'utf8'))
|
|
253
|
+
return typeof doc.version === 'string' ? doc.version : ''
|
|
254
|
+
} catch {
|
|
255
|
+
return ''
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 更新过程落一条日志,别的会话/排查时能直接看:<busDir>\logs\update.log
|
|
260
|
+
function appendUpdateLog(cfg, record) {
|
|
261
|
+
try {
|
|
262
|
+
const dir = path.join((cfg && cfg.busDir) || path.join(os.homedir(), '.dsh', 'crossterm'), 'logs')
|
|
263
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
264
|
+
fs.appendFileSync(path.join(dir, 'update.log'), `${JSON.stringify({ at: new Date().toISOString(), ...record })}\n`, 'utf8')
|
|
265
|
+
} catch {
|
|
266
|
+
/* 日志写不进去不影响更新本身 */
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 更新已安装的 registry 版:按「PATH 里的 dsh → DSH 自带 CLI → 直接 pnpm」依次尝试,
|
|
272
|
+
* 每条都记进 steps(命令、退出码、耗时、输出尾部),最后核对 node_modules 里的真实版本。
|
|
273
|
+
*/
|
|
274
|
+
async function updatePlugin(term, info, latest) {
|
|
222
275
|
const target = `${PKG}@latest`
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
const before = installedVersion(info)
|
|
277
|
+
const steps = []
|
|
278
|
+
const attempt = async (label, cmd, args, opts) => {
|
|
279
|
+
const started = Date.now()
|
|
280
|
+
const res = await run(cmd, args, (opts && opts.cwd) || info.dir, opts)
|
|
281
|
+
steps.push({
|
|
282
|
+
label,
|
|
283
|
+
cmd: `${path.basename(cmd)} ${args.join(' ')}`.slice(0, 200),
|
|
284
|
+
code: res.code,
|
|
285
|
+
ms: Date.now() - started,
|
|
286
|
+
out: (res.out || '').trim().slice(-600),
|
|
287
|
+
})
|
|
288
|
+
return res
|
|
289
|
+
}
|
|
290
|
+
const finish = (via) => {
|
|
291
|
+
const installed = installedVersion(info)
|
|
292
|
+
const updated = Boolean(installed) && installed !== before
|
|
293
|
+
const upToDate = Boolean(installed) && Boolean(latest) && compareVersion(installed, latest) >= 0
|
|
294
|
+
const ok = updated || upToDate
|
|
295
|
+
appendUpdateLog(term.cfg, { profile: info.name, ok, updated, via, before, installed, latest: latest || '', steps })
|
|
296
|
+
return { ok, updated, via, before, installed, latest: latest || '', steps }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { cmd: node, env } = nodeTool(term.cfg)
|
|
300
|
+
const cli = cliScript()
|
|
301
|
+
const plan = [{ label: 'dsh', cmd: 'dsh', args: ['plugin', '--profile', info.name, 'add', target], opts: {} }]
|
|
302
|
+
if (fs.existsSync(cli)) {
|
|
303
|
+
plan.push({
|
|
304
|
+
label: 'cli',
|
|
305
|
+
cmd: process.execPath,
|
|
306
|
+
args: ['--expose-internals', cli, 'plugin', '--profile', info.name, 'add', target],
|
|
307
|
+
opts: { shell: false, env: { ELECTRON_RUN_AS_NODE: '1' } },
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
for (const pnpm of pnpmCandidates(term.cfg)) {
|
|
311
|
+
plan.push({ label: 'pnpm', cmd: node, args: [pnpm, 'add', target], opts: { shell: false, env } })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let last = null
|
|
315
|
+
for (const step of plan) {
|
|
316
|
+
const res = await attempt(step.label, step.cmd, step.args, step.opts)
|
|
317
|
+
last = res
|
|
318
|
+
if (res.code === 0) {
|
|
319
|
+
const result = finish(step.label)
|
|
320
|
+
if (result.ok) return result
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
appendUpdateLog(term.cfg, { profile: info.name, ok: false, updated: false, before, latest: latest || '', steps })
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
updated: false,
|
|
327
|
+
via: 'none',
|
|
328
|
+
before,
|
|
329
|
+
installed: installedVersion(info),
|
|
330
|
+
latest: latest || '',
|
|
331
|
+
steps,
|
|
332
|
+
out: steps.map((s) => `[${s.label}] 退出码 ${s.code}\n${s.out}`).join('\n\n') || updateErrorText(last),
|
|
333
|
+
}
|
|
231
334
|
}
|
|
232
335
|
|
|
233
336
|
function statusOf(term) {
|
|
@@ -316,6 +419,8 @@ function aboutOf() {
|
|
|
316
419
|
name: PKG,
|
|
317
420
|
version: VERSION,
|
|
318
421
|
title: meta.title || PKG,
|
|
422
|
+
// 插件页顶部标题要带「(总开关)」才看得懂这一行是什么,但卡片头用短名更清爽
|
|
423
|
+
shortTitle: meta.shortTitle || meta.title || PKG,
|
|
319
424
|
description: meta.description || '',
|
|
320
425
|
components,
|
|
321
426
|
changelog: readChangelog(),
|
|
@@ -564,17 +669,25 @@ export function registerWeb(ctx, term, log) {
|
|
|
564
669
|
})
|
|
565
670
|
return
|
|
566
671
|
}
|
|
567
|
-
const
|
|
672
|
+
const latest = await latestVersion().catch(() => '')
|
|
673
|
+
const result = await updatePlugin(term, info, latest)
|
|
674
|
+
const installed = result.installed || ''
|
|
675
|
+
const steps = (result.steps || []).map((s) => ({ label: s.label, code: s.code, ms: s.ms, out: s.out }))
|
|
676
|
+
let message
|
|
677
|
+
if (result.ok && result.updated) message = `已更新到 ${installed},重启 DSH 后生效`
|
|
678
|
+
else if (result.ok) message = `已经是最新(${installed || latest}),不用更新`
|
|
679
|
+
else if (installed) message = `更新没成功(当前装着 ${installed}):${result.out || '命令都返回失败'}`
|
|
680
|
+
else message = `更新失败:${result.out || '未知原因'}`
|
|
568
681
|
json(res, 200, {
|
|
569
682
|
ok: result.ok,
|
|
570
683
|
mode: 'registry',
|
|
571
684
|
via: result.via,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
685
|
+
before: result.before || '',
|
|
686
|
+
installed,
|
|
687
|
+
latest,
|
|
688
|
+
needRestart: Boolean(result.ok && result.updated),
|
|
689
|
+
steps,
|
|
690
|
+
message,
|
|
578
691
|
})
|
|
579
692
|
},
|
|
580
693
|
}),
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "CrossTerm",
|
|
4
|
-
"
|
|
3
|
+
"title": "CrossTerm (master switch)",
|
|
4
|
+
"shortTitle": "CrossTerm",
|
|
5
|
+
"description": "Master switch: turning this off stops everything below — tools, heartbeat, cloud sync, cloud relay and session backups for all four components (cross-session / cross-terminal / cross-device / cross-account). While it is on, each component can still be switched off individually."
|
|
5
6
|
}
|
|
6
7
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "
|
|
4
|
-
"
|
|
3
|
+
"title": "跨端互通(总开关)",
|
|
4
|
+
"shortTitle": "跨端互通",
|
|
5
|
+
"description": "总开关:关掉这一项,下面四个组件(跨会话 / 跨终端 / 跨设备 / 跨账号)全部停用 —— 工具、心跳、云同步、云端中转站与会话备份一起停。开着它时,四个组件可各自单独开关。插件简介:同一 DSH 账号下的会话、终端、设备之间互通。"
|
|
5
6
|
}
|
|
6
7
|
}
|