@deepseek-harness-tui/dsh-tui 0.8.6 → 0.8.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 +13 -3
- package/bin/dsh-tui.js +269 -218
- package/cordis.patch.yml +48 -22
- package/lib/types/cc/figures.d.ts.map +1 -1
- package/lib/types/cc/figures.js +14 -13
- package/lib/types/cc/syntaxRuns.d.ts +16 -0
- package/lib/types/cc/syntaxRuns.d.ts.map +1 -0
- package/lib/types/cc/syntaxRuns.js +79 -0
- package/lib/types/commands.d.ts +14 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +20 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +19 -0
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -0
- package/lib/types/components/Chat/SubagentMessage.js +75 -0
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +2 -2
- package/lib/types/components/EffortTierBadge.js +1 -1
- package/lib/types/components/HelpMenu.d.ts +8 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +15 -4
- package/lib/types/components/LogoV2.d.ts +3 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +2 -2
- package/lib/types/components/MessageList.d.ts +6 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +17 -6
- package/lib/types/components/ModelPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +75 -15
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SplitDiffView.d.ts +1 -13
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +6 -105
- package/lib/types/components/SubagentCard.d.ts +9 -0
- package/lib/types/components/SubagentCard.d.ts.map +1 -0
- package/lib/types/components/SubagentCard.js +26 -0
- package/lib/types/components/SubagentDashboard.d.ts +13 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -0
- package/lib/types/components/SubagentDashboard.js +50 -0
- package/lib/types/components/SubagentDetailScene.d.ts +16 -0
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -0
- package/lib/types/components/SubagentDetailScene.js +141 -0
- package/lib/types/components/SyntaxText.d.ts +15 -0
- package/lib/types/components/SyntaxText.d.ts.map +1 -0
- package/lib/types/components/SyntaxText.js +34 -0
- package/lib/types/components/ThinkingToggle.d.ts +3 -10
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +4 -9
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +18 -7
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +2 -0
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +42 -6
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +15 -2
- package/lib/types/dsh-adapter/channel.d.ts +53 -2
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +294 -7
- package/lib/types/dsh-adapter/contract.d.ts +27 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +56 -10
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +9 -3
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +38 -0
- package/lib/types/dsh-adapter/shortcuts.js +3 -3
- package/lib/types/dsh-adapter/subagents.d.ts +87 -0
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -0
- package/lib/types/dsh-adapter/subagents.js +204 -0
- package/lib/types/herdr.d.ts +28 -0
- package/lib/types/herdr.d.ts.map +1 -0
- package/lib/types/herdr.js +133 -0
- package/lib/types/i18n.d.ts +215 -31
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +65 -14
- package/lib/types/ink/ink.d.ts +4 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +46 -15
- package/lib/types/minimalMode.d.ts +13 -0
- package/lib/types/minimalMode.d.ts.map +1 -0
- package/lib/types/minimalMode.js +17 -0
- package/lib/types/screens/Chat.d.ts +1 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +94 -43
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +6 -2
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +4 -2
- package/lib/types/theme.d.ts +8 -0
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +38 -14
- package/lib/types/tips.js +3 -3
- package/lib/types/update.d.ts +18 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +73 -2
- package/lib/types/utils/execFileNoThrow.d.ts +3 -20
- package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
- package/lib/types/utils/execFileNoThrow.js +53 -32
- package/lib/types/utils/externalEditor.d.ts +12 -11
- package/lib/types/utils/externalEditor.d.ts.map +1 -1
- package/lib/types/utils/externalEditor.js +11 -11
- package/package.json +64 -58
package/README.md
CHANGED
|
@@ -88,6 +88,14 @@ sh install.sh
|
|
|
88
88
|
|
|
89
89
|
`dsh-tui --resume` 恢复上次会话;Windows 也可用仓库里的 `dsh-tui.cmd`(等价)。
|
|
90
90
|
|
|
91
|
+
### Herdr
|
|
92
|
+
|
|
93
|
+
在 [Herdr](https://herdr.dev) pane 中直接运行 `dsh-tui` 即可,无需额外配置。
|
|
94
|
+
dsh-TUI 会通过 Herdr 提供的本地接口上报 `idle`、`working`、`blocked` 状态;
|
|
95
|
+
问卷或工具审批出现时标记为 `blocked`。离开 Herdr 运行时该集成完全不启用。
|
|
96
|
+
Herdr 的 `agent start --kind dsh-tui`、session 身份与服务重启后自动恢复仍需 Herdr
|
|
97
|
+
上游加入原生 agent kind;当前可正常保活、重连和观察手工启动的 pane。
|
|
98
|
+
|
|
91
99
|
在 VS Code 中运行的完整指南(内置终端直接使用 + companion 扩展
|
|
92
100
|
`dsh-tui-vscode`——**真实集成终端承载、体验与 Claude Code 官方扩展几乎一致、
|
|
93
101
|
已上架 VS Code Marketplace**)见
|
|
@@ -122,7 +130,7 @@ npm install -g @deepseek-harness-tui/dsh-tui@<profile-version>
|
|
|
122
130
|
| `Ctrl+R` | 历史消息搜索 |
|
|
123
131
|
| `/` | 会话内全文搜索(`n`/`N` 跳转) |
|
|
124
132
|
| `Ctrl+V` | 粘贴文本或文件管理器中的文件;图片显示为 `[Image #N]` 并作为持久附件发送 |
|
|
125
|
-
| `Ctrl+
|
|
133
|
+
| `Ctrl+G` | 用 `$VISUAL`/`$EDITOR`(如 nvim)打开当前输入编辑,保存退出后回填 |
|
|
126
134
|
| `?` | 快捷键菜单(仅输入框为空时响应) |
|
|
127
135
|
| `Shift+↑` | 消息选择模式(Enter 展开单条) |
|
|
128
136
|
| `Ctrl+P` | 切换启动时 loaded-context 面板(面板在屏时有效) |
|
|
@@ -145,7 +153,7 @@ macOS 自带 Terminal.app 会自行消费 `⌘` 快捷键,请继续使用 `Ctr
|
|
|
145
153
|
|---|---|
|
|
146
154
|
| 拖拽选择 | 应用内文本选区,**松开即复制**(OSC 52 + `wl-copy`/`xclip`/`xsel` 原生兜底;tmux 内走 `load-buffer -w`),复制后自动取消选区并弹出「已复制 N 个字符」提示 |
|
|
147
155
|
| 双击 / 三击 | 选词 / 选行,同样即选即复制 |
|
|
148
|
-
| 滚轮 |
|
|
156
|
+
| 滚轮 | 仅在 fullscreen 且鼠标跟踪启用时:Help 打开时滚动帮助,否则滚动消息列表(±3 行/格);默认 inline 模式不会把滚轮事件交给 TUI |
|
|
149
157
|
| `Esc` | 拖拽进行中取消选区(不复制) |
|
|
150
158
|
| 单击消息行 | 展开/收起该行 |
|
|
151
159
|
| 单击「加载更早消息」/「ctrl+e 显示前 N 条」 | 加载更早消息 / 展开全部 |
|
|
@@ -298,7 +306,9 @@ pnpm smoke
|
|
|
298
306
|
```
|
|
299
307
|
|
|
300
308
|
`lib/types/` 是忽略入库的生成目录;`pnpm build` 会从干净输出目录重新编译并运行
|
|
301
|
-
|
|
309
|
+
构建门禁。**Git URL 安装不受支持**(源 manifest 的 `@dsh-std/*` 为 workspace 依赖、
|
|
310
|
+
`vendor/dsh-std` 为子模块、且 pnpm ≥11 默认拒绝 git 依赖的 `prepare` 脚本);请安装
|
|
311
|
+
registry 包:`dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui`。渲染、问卷和工具卡
|
|
302
312
|
改动还需运行对应回归脚本。
|
|
303
313
|
|
|
304
314
|
|
package/bin/dsh-tui.js
CHANGED
|
@@ -1,47 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* dsh-tui —
|
|
3
|
+
* dsh-tui — 双态启动器(delegating launcher,0.8.7)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 输入 `dsh --profile dsh-tui`:
|
|
5
|
+
* 同一个文件按“自己住在哪”决定扮演的角色:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* add);profile 次版本更旧(反向错位)拒绝启动并给出对齐命令——
|
|
15
|
-
* 该方向 dsh CLI 会把启动器的 bundle patch 套到 profile 旧包上,
|
|
16
|
-
* 启动必然以模块解析错误崩溃;
|
|
17
|
-
* 4. 透传全部参数启动 `dsh --profile dsh-tui`。
|
|
7
|
+
* 全局安装副本(npm i -g 得到的 `dsh-tui` 命令)→ 瘦壳:
|
|
8
|
+
* 1. 找到 $DSH_HOME/profiles/dsh-tui 里的同包 bin;
|
|
9
|
+
* 2. 可读 → 原样转发 argv 委托它执行(完整逻辑永远来自 profile 副本,
|
|
10
|
+
* 版本随 /update 一起前进,启动器滞后问题从结构上消失);
|
|
11
|
+
* 3. 不可读(首次运行)→ 探测 dsh/pnpm 后自举
|
|
12
|
+
* `dsh plugin --profile dsh-tui add <本包>@<本包版本>`,成功后委托。
|
|
18
13
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* 该 flag 本身不再传给 dsh。
|
|
14
|
+
* profile 内副本(被委托执行,或 junction/源码目录里直接运行)→ 完整
|
|
15
|
+
* 启动逻辑(与 0.8.6 及之前一致):
|
|
16
|
+
* dsh 预检 / profile 版本核对 / --resume 与工作区目标拦截 /
|
|
17
|
+
* 旧环境变量警告 / `dsh --profile dsh-tui` 启动与退出码透传。
|
|
24
18
|
*
|
|
25
|
-
*
|
|
19
|
+
* 自举角色判定用 realpath:Windows Junction 轨(profile 指回仓库)与
|
|
20
|
+
* `pnpm run dev` 源码运行都会折叠成同一物理目录 → 走完整逻辑,不会
|
|
21
|
+
* 自己委托自己形成循环。
|
|
22
|
+
*
|
|
23
|
+
* 本文件必须保持零 lib/ 依赖:/update 的启动器迁移只覆写这一个文件
|
|
24
|
+
* (外加 package.json 的版本号),旧全局安装里不存在新 lib 助手时同样
|
|
25
|
+
* 可用。shellQuote 等小工具在此内联。
|
|
26
|
+
*
|
|
27
|
+
* 面向用户的消息走 MSG 双语表:与 TUI 的语言契约一致——
|
|
26
28
|
* `DSH_TUI_LANG` 显式指定时从其值,否则默认中文(同 src/i18n.ts 的缺省)。
|
|
27
29
|
*/
|
|
28
30
|
import { spawn, spawnSync } from 'node:child_process'
|
|
29
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
31
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
30
32
|
import { homedir } from 'node:os'
|
|
31
|
-
import { isAbsolute, join, resolve } from 'node:path'
|
|
33
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
32
34
|
import { fileURLToPath } from 'node:url'
|
|
33
|
-
import { gt, valid } from 'semver'
|
|
34
|
-
import { shellQuote } from '../lib/types/utils/shellQuote.js'
|
|
35
|
-
import { detectLegacyEnv, RENAMED_ENV } from '../lib/types/utils/paths.js'
|
|
36
35
|
|
|
37
|
-
const here = fileURLToPath(
|
|
38
|
-
const
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
37
|
+
const ownDir = dirname(here)
|
|
38
|
+
const readJson = p => {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(p, 'utf8'))
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const ownPackage = readJson(join(ownDir, 'package.json'))
|
|
46
|
+
const ownVersion = ownPackage?.name === '@deepseek-harness-tui/dsh-tui' ? ownPackage.version : undefined
|
|
39
47
|
const PACKAGE = '@deepseek-harness-tui/dsh-tui'
|
|
40
48
|
const PROFILE = 'dsh-tui'
|
|
41
49
|
|
|
42
|
-
// ---
|
|
43
|
-
// 与
|
|
44
|
-
//
|
|
50
|
+
// --- 内联小工具(见文件头:零 lib 依赖是迁移契约的一部分)---------------------
|
|
51
|
+
// 与 lib/types/utils/shellQuote.js 同语义的最小实现:cmd.exe 以空格拼接参数
|
|
52
|
+
// 且不做转义,含空格/引号的参数必须整体加引号(内层引号与反斜杠转义)。
|
|
53
|
+
const shellQuote = args =>
|
|
54
|
+
args.map(arg => {
|
|
55
|
+
const s = String(arg)
|
|
56
|
+
if (s === '') return '""'
|
|
57
|
+
if (!/[\s"^]/.test(s)) return s
|
|
58
|
+
return `"${s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`
|
|
59
|
+
})
|
|
60
|
+
const isWin = process.platform === 'win32'
|
|
61
|
+
const shellOpt = isWin ? { shell: true } : {}
|
|
62
|
+
// DEP0190(issue #148):shell:true + 非空参数数组触发语法级弃用告警——
|
|
63
|
+
// 转义后拼进命令字符串(空参数数组不触发),非 Windows 保持数组直传。
|
|
64
|
+
const cmd = (command, args) =>
|
|
65
|
+
isWin ? [`${command} ${shellQuote(args).join(' ')}`, []] : [command, args]
|
|
66
|
+
|
|
67
|
+
// 内联 semver(解析 + 严格大于):启动器可能在依赖不完整的环境里被执行
|
|
68
|
+
// (迁移、半损坏安装、测试沙箱),零外部依赖是自保底线。覆盖 semver 的
|
|
69
|
+
// 核心-先行版比较规则:先行版标识符逐段比(数字段按数值、小于字母段),
|
|
70
|
+
// 前缀相同时段数少者更旧,无先行版者最新。
|
|
71
|
+
const parseVersion = v => {
|
|
72
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v).trim())
|
|
73
|
+
return m
|
|
74
|
+
? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] === undefined ? null : m[4].split('.') }
|
|
75
|
+
: null
|
|
76
|
+
}
|
|
77
|
+
const isVersionNewer = (a, b) => {
|
|
78
|
+
const A = parseVersion(a)
|
|
79
|
+
const B = parseVersion(b)
|
|
80
|
+
if (A === null || B === null) return false
|
|
81
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
82
|
+
if (A[key] !== B[key]) return A[key] > B[key]
|
|
83
|
+
}
|
|
84
|
+
if (A.pre === null) return B.pre !== null
|
|
85
|
+
if (B.pre === null) return false
|
|
86
|
+
for (let i = 0; i < Math.max(A.pre.length, B.pre.length); i++) {
|
|
87
|
+
const x = A.pre[i]
|
|
88
|
+
const y = B.pre[i]
|
|
89
|
+
if (x === undefined) return false
|
|
90
|
+
if (y === undefined) return true
|
|
91
|
+
if (x === y) continue
|
|
92
|
+
const xn = /^\d+$/.test(x)
|
|
93
|
+
const yn = /^\d+$/.test(y)
|
|
94
|
+
if (xn && yn) return Number(x) > Number(y)
|
|
95
|
+
if (xn !== yn) return yn
|
|
96
|
+
return x > y
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
45
101
|
const lang = (process.env.DSH_TUI_LANG ?? process.env.CC_TUI_LANG) === 'en' ? 'en' : 'zh'
|
|
46
102
|
const MSG = {
|
|
47
103
|
noDsh: {
|
|
@@ -64,10 +120,6 @@ const MSG = {
|
|
|
64
120
|
en: `[dsh-tui] Plugin install failed. Retry manually later:\n dsh plugin --profile ${PROFILE} add -w ${PACKAGE}@${ownVersion}`,
|
|
65
121
|
zh: `[dsh-tui] 插件安装失败。可稍后手工重试:\n dsh plugin --profile ${PROFILE} add -w ${PACKAGE}@${ownVersion}`,
|
|
66
122
|
},
|
|
67
|
-
// no-op 假成功(自举后复查发现判定文件依旧缺失):pnpm 把包文件残缺的
|
|
68
|
-
// profile 视为 already up to date,重装什么都不做却报告成功;继续启动
|
|
69
|
-
// 只会以 cannot resolve profile bundle 崩溃,而崩溃提示的 `plugin
|
|
70
|
-
// install` 同样 no-op——用户会陷入「每步都成功、每次都崩」的循环。
|
|
71
123
|
bootstrapUnreadable: {
|
|
72
124
|
en: dir =>
|
|
73
125
|
`[dsh-tui] install reported success but the plugin package is still unreadable under:\n` +
|
|
@@ -82,60 +134,16 @@ const MSG = {
|
|
|
82
134
|
` 恢复方法:\n` +
|
|
83
135
|
` rm -rf ${dir} 后重新运行 dsh-tui`,
|
|
84
136
|
},
|
|
85
|
-
// Non-fatal skew comes in two directions and each needs its own repair:
|
|
86
|
-
// a profile NEWER than the launcher (typical after /update) must point at
|
|
87
|
-
// the global install, while a profile older only in patch must point back
|
|
88
|
-
// at the profile. The old generic message told forward-skew users to run
|
|
89
|
-
// /update again — an "Already up to date" loop.
|
|
90
|
-
profileOutdated: {
|
|
91
|
-
en: (installed, own) =>
|
|
92
|
-
`[dsh-tui] note: the profile is running v${installed} but this launcher is v${own}.\n` +
|
|
93
|
-
` Align the profile to this launcher:\n` +
|
|
94
|
-
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}`,
|
|
95
|
-
zh: (installed, own) =>
|
|
96
|
-
`[dsh-tui] 提示:profile 内运行的是 v${installed},启动器是 v${own}。\n` +
|
|
97
|
-
` 请把 profile 对齐到启动器版本:\n` +
|
|
98
|
-
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}`,
|
|
99
|
-
},
|
|
100
|
-
launcherOutdated: {
|
|
101
|
-
en: (installed, own) =>
|
|
102
|
-
`[dsh-tui] note: the profile is already v${installed}, but the global launcher is still v${own}.\n` +
|
|
103
|
-
` Align the global dsh-tui launcher to the profile:\n` +
|
|
104
|
-
` npm install -g ${PACKAGE}@${installed}\n` +
|
|
105
|
-
` (if you installed it globally with pnpm: pnpm add -g ${PACKAGE}@${installed})`,
|
|
106
|
-
zh: (installed, own) =>
|
|
107
|
-
`[dsh-tui] 提示:profile 已是 v${installed},但全局启动器仍是 v${own}。\n` +
|
|
108
|
-
` 请把全局 dsh-tui 启动器对齐到 profile 版本:\n` +
|
|
109
|
-
` npm install -g ${PACKAGE}@${installed}\n` +
|
|
110
|
-
` (如果你使用 pnpm 全局安装:pnpm add -g ${PACKAGE}@${installed})`,
|
|
111
|
-
},
|
|
112
|
-
// Reverse skew (issue #183): the dsh CLI reads the bundle patch from the
|
|
113
|
-
// FIRST copy found from its own install anchor — this globally installed
|
|
114
|
-
// launcher — while the plugin modules load from the profile's copy. A
|
|
115
|
-
// launcher minor NEWER than the profile means the patch may reference
|
|
116
|
-
// subpath exports the profile's older package does not have, and boot
|
|
117
|
-
// crashes opaquely (ERR_PACKAGE_PATH_NOT_EXPORTED) before any TUI code
|
|
118
|
-
// runs. Fail loud with the fix instead. (Forward skew degrades to a
|
|
119
|
-
// working local-workspace fallback since 0.7.2 — soft note below.)
|
|
120
|
-
profileOlderThanLauncher: {
|
|
121
|
-
en: (installed, own) =>
|
|
122
|
-
`[dsh-tui] cannot start: the profile runs v${installed} but this launcher is v${own}.\n` +
|
|
123
|
-
` The launcher's bundle patch would be applied to the profile's older package,\n` +
|
|
124
|
-
` which does not export everything the patch references — boot would crash.\n` +
|
|
125
|
-
` Align the profile with the launcher:\n` +
|
|
126
|
-
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}\n` +
|
|
127
|
-
` (or update everything to the latest release: dsh plugin --profile ${PROFILE} add ${PACKAGE}@latest)`,
|
|
128
|
-
zh: (installed, own) =>
|
|
129
|
-
`[dsh-tui] 无法启动:profile 内运行的是 v${installed},而启动器是 v${own}。\n` +
|
|
130
|
-
` 启动器的 bundle patch 会套用到 profile 里的旧版包上,其中缺少 patch 引用\n` +
|
|
131
|
-
` 的子路径导出——启动会以模块解析错误崩溃。请让 profile 与启动器对齐:\n` +
|
|
132
|
-
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}\n` +
|
|
133
|
-
` (或全部升到最新:dsh plugin --profile ${PROFILE} add ${PACKAGE}@latest)`,
|
|
134
|
-
},
|
|
135
137
|
launchFailed: {
|
|
136
138
|
en: err => `[dsh-tui] Failed to launch: ${err.message}`,
|
|
137
139
|
zh: err => `[dsh-tui] 启动失败:${err.message}`,
|
|
138
140
|
},
|
|
141
|
+
delegateFailed: {
|
|
142
|
+
en: path =>
|
|
143
|
+
`[dsh-tui] cannot launch the profile copy:\n ${path}\nReinstall the global launcher:\n npm install -g ${PACKAGE}@latest`,
|
|
144
|
+
zh: path =>
|
|
145
|
+
`[dsh-tui] 无法启动 profile 内副本:\n ${path}\n请重装全局启动器:\n npm install -g ${PACKAGE}@latest`,
|
|
146
|
+
},
|
|
139
147
|
profileExited: {
|
|
140
148
|
en: code => `[dsh-tui] dsh profile exited with code ${code}. Run it directly for diagnostics:\n dsh --profile ${PROFILE}`,
|
|
141
149
|
zh: code => `[dsh-tui] dsh profile 已退出(退出码 ${code})。可直接运行以下命令查看诊断:\n dsh --profile ${PROFILE}`,
|
|
@@ -151,55 +159,59 @@ const msg = key => MSG[key][lang]
|
|
|
151
159
|
// 长会话 OOM——与仓库根 dsh-tui.cmd 保持一致,强制 production。
|
|
152
160
|
process.env.NODE_ENV ??= 'production'
|
|
153
161
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// DEP0190(issue #148):Node ≥22 对「shell:true + 非空参数数组」的调用
|
|
161
|
-
// 发出弃用警告,判定是语法级的——参数已过 shellQuote 也一样告警;且未来
|
|
162
|
-
// 大版本可能升级为运行时错误。把转义后的参数拼进命令字符串传入
|
|
163
|
-
// (shell:true + 空参数数组不触发),非 Windows 路径保持数组直传。
|
|
164
|
-
const cmd = (command, args) =>
|
|
165
|
-
isWin ? [`${command} ${shellQuote(args).join(' ')}`, []] : [command, args]
|
|
166
|
-
|
|
167
|
-
// --- 1. dsh CLI 预检 ---------------------------------------------------------
|
|
168
|
-
const probe = spawnSync(...cmd('dsh', ['--version']), { stdio: 'pipe', ...shellOpt })
|
|
169
|
-
if (probe.error || probe.status !== 0) {
|
|
170
|
-
console.error(msg('noDsh'))
|
|
171
|
-
process.exit(1)
|
|
162
|
+
const sameDir = (a, b) => {
|
|
163
|
+
try {
|
|
164
|
+
return realpathSync(resolve(a)) === realpathSync(resolve(b))
|
|
165
|
+
} catch {
|
|
166
|
+
return resolve(a) === resolve(b)
|
|
167
|
+
}
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
// --- 2. profile 自举与版本检查 -------------------------------------------------
|
|
175
170
|
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
176
171
|
const profileDir = join(dshHome, 'profiles', PROFILE)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
172
|
+
const profilePkgDir = join(profileDir, 'node_modules', '@deepseek-harness-tui', 'dsh-tui')
|
|
173
|
+
const profileBin = join(profilePkgDir, 'bin', 'dsh-tui.js')
|
|
174
|
+
const installedPkgPath = join(profilePkgDir, 'package.json')
|
|
175
|
+
const runningInsideProfile = sameDir(ownDir, profilePkgDir)
|
|
176
|
+
|
|
177
|
+
const forwardExit = child => {
|
|
178
|
+
child.on('error', err => {
|
|
179
|
+
console.error(msg('launchFailed')(err))
|
|
180
|
+
process.exit(1)
|
|
181
|
+
})
|
|
182
|
+
child.on('exit', (code, signal) => {
|
|
183
|
+
if (signal) {
|
|
184
|
+
process.kill(process.pid, signal)
|
|
185
|
+
} else {
|
|
186
|
+
if (code !== null && code !== 0) console.error(msg('profileExited')(code))
|
|
187
|
+
process.exit(code ?? 0)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
187
190
|
}
|
|
188
|
-
|
|
191
|
+
|
|
192
|
+
// 首次运行自举:探测 dsh 与 pnpm,随后 `dsh plugin add` 固定到与启动器一
|
|
193
|
+
// 致的版本(避免 pnpm store 缓存带来的旧版漂移)。-w 重试(issue #239)与
|
|
194
|
+
// 「no-op 假成功」复查(issue #209)在此集中实现,瘦壳与完整逻辑共用。
|
|
195
|
+
const profileReady = () => {
|
|
196
|
+
try {
|
|
197
|
+
readFileSync(installedPkgPath, 'utf8')
|
|
198
|
+
return true
|
|
199
|
+
} catch {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const bootstrapProfile = () => {
|
|
204
|
+
const probe = spawnSync(...cmd('dsh', ['--version']), { stdio: 'pipe', ...shellOpt })
|
|
205
|
+
if (probe.error || probe.status !== 0) {
|
|
206
|
+
console.error(msg('noDsh'))
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
189
209
|
const pnpmProbe = spawnSync(...cmd('pnpm', ['--version']), { stdio: 'pipe', ...shellOpt })
|
|
190
210
|
if (pnpmProbe.error || pnpmProbe.status !== 0) {
|
|
191
211
|
console.error(msg('noPnpm'))
|
|
192
212
|
process.exit(1)
|
|
193
213
|
}
|
|
194
214
|
console.log(msg('bootstrapStart'))
|
|
195
|
-
// pnpm ≥11 在带 pnpm-workspace.yaml 的 profile 目录里可能拒绝向
|
|
196
|
-
// workspace root 写依赖(ERR_PNPM_ADDING_TO_ROOT,issue #239):识别该
|
|
197
|
-
// 错误时带 -w 重试一次。签名在 stdout——pnpm 的错误报告写 stdout(经
|
|
198
|
-
// dsh 原样转发),stderr 上只有 dsh 自己的失败说明,识别 stderr 永远
|
|
199
|
-
// 匹配不到(PR #241 的回归)。因此首次 add 的 stdout/stderr 都走 pipe
|
|
200
|
-
// 捕获,结束后回放(失败回 stderr 保诊断,成功回 stdout 保进度,代价
|
|
201
|
-
// 是不再实时);-w 重试大概率成功,恢复 inherit 让进度实时可见。手工
|
|
202
|
-
// 重试提示同步带 -w。
|
|
203
215
|
const runAdd = (extraArgs, capture) => spawnSync(
|
|
204
216
|
...cmd('dsh', ['plugin', '--profile', PROFILE, 'add', ...extraArgs, `${PACKAGE}@${ownVersion}`]),
|
|
205
217
|
{ stdio: capture ? ['inherit', 'pipe', 'pipe'] : 'inherit', ...shellOpt },
|
|
@@ -219,116 +231,155 @@ if (installedVersion === undefined) {
|
|
|
219
231
|
console.error(msg('installFailed'))
|
|
220
232
|
process.exit(add.status ?? 1)
|
|
221
233
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
if (!profileReady()) {
|
|
235
|
+
console.error(msg('bootstrapUnreadable')(profileDir))
|
|
236
|
+
process.exit(1)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── 全局副本:瘦壳角色 ───────────────────────────────────────────────────────
|
|
241
|
+
// DSH_TUI_NO_DELEGATE=1 是测试/调试逃生口:强制走完整逻辑(verify-launcher
|
|
242
|
+
// 的沙箱用它直接驱动全量路径;现场排查委托链时同样可用)。
|
|
243
|
+
if (!runningInsideProfile && ownVersion !== undefined && process.env.DSH_TUI_NO_DELEGATE !== '1') {
|
|
244
|
+
if (!profileReady()) bootstrapProfile()
|
|
245
|
+
// 委托 profile 内副本执行全部启动逻辑。外层代际通过
|
|
246
|
+
// DSH_TUI_LAUNCHER_VERSION 交代(/update 的对齐诊断沿用该契约)。
|
|
226
247
|
try {
|
|
227
|
-
readFileSync(
|
|
248
|
+
readFileSync(profileBin, 'utf8')
|
|
228
249
|
} catch {
|
|
229
|
-
console.error(
|
|
250
|
+
console.error(msg('delegateFailed')(profileBin))
|
|
230
251
|
process.exit(1)
|
|
231
252
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
process.env.DSH_TUI_LAUNCHER_VERSION = ownVersion
|
|
254
|
+
const child = spawn(process.execPath, [profileBin, ...process.argv.slice(2)], {
|
|
255
|
+
stdio: 'inherit',
|
|
256
|
+
env: process.env,
|
|
257
|
+
})
|
|
258
|
+
forwardExit(child)
|
|
259
|
+
} else {
|
|
260
|
+
// ─── profile 副本(或源码运行):完整启动逻辑 ─────────────────────────────
|
|
261
|
+
// dsh CLI 预检(缺失时给安装指引,先于一切 profile 逻辑)。
|
|
262
|
+
{
|
|
263
|
+
const probe = spawnSync(...cmd('dsh', ['--version']), { stdio: 'pipe', ...shellOpt })
|
|
264
|
+
if (probe.error || probe.status !== 0) {
|
|
265
|
+
console.error(msg('noDsh'))
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
241
268
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// newer release or a mirror dist-tag is in play.
|
|
248
|
-
const installedSemver = valid(installedVersion)
|
|
249
|
-
const ownSemver = valid(ownVersion)
|
|
250
|
-
if (installedSemver !== null && ownSemver !== null && gt(installedSemver, ownSemver)) {
|
|
251
|
-
console.error(MSG.launcherOutdated[lang](installedVersion, ownVersion))
|
|
252
|
-
} else {
|
|
253
|
-
console.error(MSG.profileOutdated[lang](installedVersion, ownVersion))
|
|
269
|
+
let installedVersion
|
|
270
|
+
try {
|
|
271
|
+
installedVersion = JSON.parse(readFileSync(installedPkgPath, 'utf8')).version
|
|
272
|
+
} catch {
|
|
273
|
+
installedVersion = undefined
|
|
254
274
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// env 双写(新旧名都设),文件读取新路径优先、旧路径兜底。
|
|
260
|
-
// 支持的形态(对齐 issue #53 的诉求):
|
|
261
|
-
// --resume <id> / --resume=<id> 恢复指定会话
|
|
262
|
-
// --resume / -c / --continue 恢复最近一次会话(读 resume.txt)
|
|
263
|
-
// 其余位置参数原样透传给 dsh CLI,由插件经 ctx.cmdlineArgs 读取(初始 prompt)。
|
|
264
|
-
const setResumeEnv = sessionId => {
|
|
265
|
-
process.env.DSH_TUI_RESUME_SESSION = sessionId
|
|
266
|
-
process.env.DSH_CC_RESUME_SESSION = sessionId
|
|
267
|
-
}
|
|
268
|
-
const readLastResumeTarget = () => {
|
|
269
|
-
for (const dir of ['.dsh-tui', '.dsh-cc']) {
|
|
275
|
+
// 残骸/未初始化 profile:与旧启动器一致地就地自举(add 固定到本包版
|
|
276
|
+
// 本,成功后版本天然对齐),而不是拒绝启动。
|
|
277
|
+
if (installedVersion === undefined) {
|
|
278
|
+
bootstrapProfile()
|
|
270
279
|
try {
|
|
271
|
-
|
|
272
|
-
if (sessionId) return sessionId
|
|
280
|
+
installedVersion = JSON.parse(readFileSync(installedPkgPath, 'utf8')).version
|
|
273
281
|
} catch {
|
|
274
|
-
|
|
282
|
+
installedVersion = undefined
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
285
|
+
if (installedVersion !== undefined && ownVersion !== undefined && installedVersion !== ownVersion && !runningInsideProfile) {
|
|
286
|
+
const majorMinor = v => v.split('-')[0].split('.').slice(0, 2).map(Number)
|
|
287
|
+
const [installedMajor, installedMinor] = majorMinor(installedVersion)
|
|
288
|
+
const [ownMajor, ownMinor] = majorMinor(ownVersion)
|
|
289
|
+
if (installedMajor < ownMajor || (installedMajor === ownMajor && installedMinor < ownMinor)) {
|
|
290
|
+
console.error(
|
|
291
|
+
`[dsh-tui] cannot start: the profile runs v${installedVersion} but this launcher is v${ownVersion}.\n` +
|
|
292
|
+
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${ownVersion}`,
|
|
293
|
+
)
|
|
294
|
+
process.exit(1)
|
|
295
|
+
}
|
|
296
|
+
const installedNewer = installedVersion !== undefined && ownVersion !== undefined && isVersionNewer(installedVersion, ownVersion)
|
|
297
|
+
if (installedNewer) {
|
|
298
|
+
console.error(
|
|
299
|
+
`[dsh-tui] note: the profile is already v${installedVersion}; this launcher copy is v${ownVersion}.\n` +
|
|
300
|
+
` npm install -g ${PACKAGE}@${installedVersion}`,
|
|
301
|
+
)
|
|
302
|
+
} else {
|
|
303
|
+
// profile 更旧但同 minor(patch 级错位):允许启动,指引用 add 把
|
|
304
|
+
// profile 对齐到启动器版本(精确版本,@latest 可能越过对齐点)。
|
|
305
|
+
console.error(
|
|
306
|
+
`[dsh-tui] note: the profile is running v${installedVersion} but this launcher is v${ownVersion}.\n` +
|
|
307
|
+
` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${ownVersion}`,
|
|
308
|
+
)
|
|
289
309
|
}
|
|
290
|
-
// 裸形态(含 -c/--continue)回退到 resume.txt。
|
|
291
|
-
if (!sessionId) sessionId = readLastResumeTarget()
|
|
292
|
-
if (sessionId) setResumeEnv(sessionId)
|
|
293
|
-
} else if (
|
|
294
|
-
process.env.DSH_TUI_WORKSPACE_TARGET === undefined
|
|
295
|
-
&& !a.startsWith('-')
|
|
296
|
-
&& (isAbsolute(a) || /^[a-z][a-z0-9+.-]*:\/\//iu.test(a) || existsSync(resolve(a)))
|
|
297
|
-
) {
|
|
298
|
-
// A workspace target is launcher syntax, not an argument for the profile
|
|
299
|
-
// app. The registry resolves local paths/file URLs and provider URIs.
|
|
300
|
-
process.env.DSH_TUI_WORKSPACE_TARGET = a
|
|
301
|
-
} else {
|
|
302
|
-
args.push(a)
|
|
303
310
|
}
|
|
304
|
-
}
|
|
305
311
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
// --resume / 工作区目标拦截(issue #120/#53 的启动器契约)。
|
|
313
|
+
const setResumeEnv = sessionId => {
|
|
314
|
+
process.env.DSH_TUI_RESUME_SESSION = sessionId
|
|
315
|
+
process.env.DSH_CC_RESUME_SESSION = sessionId
|
|
316
|
+
}
|
|
317
|
+
const readLastResumeTarget = () => {
|
|
318
|
+
for (const dir of ['.dsh-tui', '.dsh-cc']) {
|
|
319
|
+
try {
|
|
320
|
+
const sessionId = readFileSync(join(homedir(), dir, 'resume.txt'), 'utf8').trim()
|
|
321
|
+
if (sessionId) return sessionId
|
|
322
|
+
} catch {
|
|
323
|
+
// 没有历史会话可恢复——静默忽略,正常冷启动。
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return ''
|
|
327
|
+
}
|
|
328
|
+
const args = []
|
|
329
|
+
const argv = process.argv.slice(2)
|
|
330
|
+
for (let i = 0; i < argv.length; i++) {
|
|
331
|
+
const a = argv[i]
|
|
332
|
+
if (a === '--resume' || a === '-c' || a === '--continue' || a.startsWith('--resume=')) {
|
|
333
|
+
let sessionId = ''
|
|
334
|
+
if (a.startsWith('--resume=')) {
|
|
335
|
+
sessionId = a.slice('--resume='.length).trim()
|
|
336
|
+
} else if (a === '--resume' && argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
|
|
337
|
+
sessionId = argv[++i].trim()
|
|
338
|
+
}
|
|
339
|
+
if (!sessionId) sessionId = readLastResumeTarget()
|
|
340
|
+
if (sessionId) setResumeEnv(sessionId)
|
|
341
|
+
} else if (
|
|
342
|
+
process.env.DSH_TUI_WORKSPACE_TARGET === undefined
|
|
343
|
+
&& !a.startsWith('-')
|
|
344
|
+
&& (isAbsolute(a) || /^[a-z][a-z0-9+.-]*:\/\//iu.test(a) || existsSync(resolve(a)))
|
|
345
|
+
) {
|
|
346
|
+
process.env.DSH_TUI_WORKSPACE_TARGET = a
|
|
347
|
+
} else {
|
|
348
|
+
args.push(a)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
310
351
|
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
352
|
+
// 旧环境变量警告(必须在 TUI 渲染前输出,fullscreen 下写 stderr 会破坏
|
|
353
|
+
// 界面)。与 utils/paths 的 RENAMED_ENV 同表,内联以维持零 lib 依赖。
|
|
354
|
+
const RENAMED_ENV = {
|
|
355
|
+
CC_TUI_THEME: 'DSH_TUI_THEME',
|
|
356
|
+
CC_TUI_LANG: 'DSH_TUI_LANG',
|
|
357
|
+
CC_TUI_PERSONA: 'DSH_TUI_PERSONA',
|
|
358
|
+
CC_TUI_PRESET: 'DSH_TUI_PRESET',
|
|
359
|
+
CC_TUI_DISABLE_MOUSE: 'DSH_TUI_DISABLE_MOUSE',
|
|
360
|
+
CC_TUI_DEBUG: 'DSH_TUI_DEBUG',
|
|
361
|
+
CC_TUI_COMPACT_RATIO: 'DSH_TUI_COMPACT_RATIO',
|
|
362
|
+
CC_TUI_COMPACT_RETAIN: 'DSH_TUI_COMPACT_RETAIN',
|
|
363
|
+
DSH_CC_UPDATED_FROM: 'DSH_TUI_UPDATED_FROM',
|
|
364
|
+
DSH_CC_RENDER_LOG: 'DSH_TUI_RENDER_LOG',
|
|
365
|
+
DSH_CC_SESSION_ROOT: 'DSH_TUI_SESSION_ROOT',
|
|
366
|
+
DSH_CC_WORKSPACE: 'DSH_TUI_WORKSPACE',
|
|
367
|
+
}
|
|
368
|
+
for (const oldName of Object.keys(RENAMED_ENV)) {
|
|
369
|
+
if (process.env[oldName] !== undefined) {
|
|
370
|
+
console.error(msg('legacyEnv')(oldName, RENAMED_ENV[oldName]))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
317
373
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
...shellOpt,
|
|
322
|
-
})
|
|
323
|
-
child.on('error', err => {
|
|
324
|
-
console.error(MSG.launchFailed[lang](err))
|
|
325
|
-
process.exit(1)
|
|
326
|
-
})
|
|
327
|
-
child.on('exit', (code, signal) => {
|
|
328
|
-
if (signal) {
|
|
329
|
-
process.kill(process.pid, signal)
|
|
330
|
-
} else {
|
|
331
|
-
if (code !== null && code !== 0) console.error(MSG.profileExited[lang](code))
|
|
332
|
-
process.exit(code ?? 0)
|
|
374
|
+
// 启动:被委托场景下本副本自己的版本即对齐诊断所见的启动器代际。
|
|
375
|
+
if (process.env.DSH_TUI_LAUNCHER_VERSION === undefined && ownVersion !== undefined) {
|
|
376
|
+
process.env.DSH_TUI_LAUNCHER_VERSION = ownVersion
|
|
333
377
|
}
|
|
334
|
-
|
|
378
|
+
|
|
379
|
+
const child = spawn(...cmd('dsh', ['--profile', PROFILE, ...args]), {
|
|
380
|
+
stdio: 'inherit',
|
|
381
|
+
env: process.env,
|
|
382
|
+
...shellOpt,
|
|
383
|
+
})
|
|
384
|
+
forwardExit(child)
|
|
385
|
+
}
|