Skip to content
Open
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
15f4138
refactor(channel): build the channel core against a host interface, n…
MomentDerek Oct 5, 2026
42291df
feat(host): Phase 1 spike — Claude kernel on a bare Cordis root
MomentDerek Oct 5, 2026
0338890
feat(host): adopt the startup session after the first frame; TUI-owne…
MomentDerek Oct 5, 2026
f566f79
feat(host): start the Claude kernel from the package's own entry
MomentDerek Oct 5, 2026
e498a43
docs(host): hand off Phase 1 testing
MomentDerek Oct 5, 2026
f0a52d5
test(host): tui-test acceptance for the standalone entry
MomentDerek Oct 6, 2026
67dc704
fix(host): Phase 1 acceptance findings
MomentDerek Oct 6, 2026
d10da7e
docs(host): record Phase 1 acceptance
MomentDerek Oct 6, 2026
3706857
fix(ink): drop raw terminal writes after shutdown detach
MomentDerek Oct 6, 2026
82a1a5f
feat(channel): attach DSH extensions on startup adoption (D1)
MomentDerek Oct 6, 2026
9f44b71
docs(host): plan Phase 2, record D1 and the root-model spike
MomentDerek Oct 6, 2026
e5c604d
fix(host): keep crash resume markers on the standalone entry
MomentDerek Oct 7, 2026
80f3a4b
feat(host): single-root DSH kernel in the standalone entry
MomentDerek Oct 7, 2026
c24afba
docs(host): record Phase 2.0 and 2.3
MomentDerek Oct 7, 2026
07ec0de
feat(host): entry owns signals and exit for in-process DSH
MomentDerek Oct 7, 2026
d7c9063
feat(host): in-process DSH by default in the standalone entry
MomentDerek Oct 7, 2026
7226af1
fix(scripts): leave dsh-purge out of verify-installed-startup's profi…
MomentDerek Oct 7, 2026
2dbd641
docs(host): record Phase 2.4 and 2.5
MomentDerek Oct 7, 2026
78c39af
docs(host): record the decision to depend on @deepseek-ai/dsh
MomentDerek Oct 7, 2026
52f3439
feat(host): tighten the root guard during entry composition; plugin a…
MomentDerek Oct 8, 2026
6590f9a
feat(host): depend on @deepseek-ai/dsh; single-source host contract
MomentDerek Oct 8, 2026
dc08ff5
docs(host): record Phase 2.6 and 2.7
MomentDerek Oct 8, 2026
7d446ad
fix(host): let the composition settle before disposing the entry root
MomentDerek Oct 8, 2026
4da0d81
docs(host): record the dsh-hmr dispose deadlock and its workaround
MomentDerek Oct 8, 2026
783be54
fix(scripts): carry the named settings section into verify-settings-c…
MomentDerek Oct 8, 2026
3bd72d7
docs(host): record the rebase onto upstream/main 85d49e53
MomentDerek Oct 8, 2026
9cd5c62
docs(host): mark two stale records and retarget the upstream repro
MomentDerek Oct 8, 2026
4b6094a
fix(host): route the kernel the entry found instead of a literal 'dsh'
MomentDerek Oct 8, 2026
2d12c82
docs(host): record the entry's codex dispatch defect and its fix
MomentDerek Oct 8, 2026
ad9eae0
docs: adopt the entry-owning positioning across guides
MomentDerek Oct 8, 2026
f473407
fix(host): settle the merge seams against the registry (#1380 follow-up)
MomentDerek Oct 8, 2026
0fdffa9
docs(host): hand Phase 2's remainder to the next session
MomentDerek Oct 8, 2026
b8fde22
docs(host): record PR #1216 closed and its branch dropped
MomentDerek Oct 8, 2026
1869832
docs(host): note the orchestration-only handoff convention
MomentDerek Oct 8, 2026
cbb122c
docs(host): rule D1 single root and the direct-launch decision
MomentDerek Oct 8, 2026
d4cf8b1
feat(host): admit third-party plugins by manifest identity, keep regi…
MomentDerek Oct 8, 2026
b31d92b
fix(host): read the crash-exit live agent through ctx.get
MomentDerek Oct 8, 2026
e997011
feat(host): route the entry kernel into apply and honor a late theme …
MomentDerek Oct 8, 2026
665d7a7
feat(host): compose a lite profile for non-DSH kernels (5.7 step 1)
MomentDerek Oct 8, 2026
dc636ca
chore(scripts): add home two-step and update/resolve diagnostics
MomentDerek Oct 8, 2026
f278726
fix(host): stop the admission loader from tearing down the DSH compos…
MomentDerek Oct 8, 2026
0f83c1b
fix(host): own the kernel-start timer by its fiber
MomentDerek Oct 9, 2026
a23f614
feat(host): compose a lite profile for non-DSH kernels (5.7)
MomentDerek Oct 9, 2026
6d098e1
perf(cli): enable Node's compile cache for the entry process
MomentDerek Oct 9, 2026
5264279
docs(host): record 5.7, the compile cache and the standing follow-ups
MomentDerek Oct 9, 2026
46ce00d
fix(host): restore the #1380 registry adaptations the rebase dropped
MomentDerek Oct 9, 2026
b1efbcd
perf(startup): trim the first frame with a path import and an async d…
MomentDerek Oct 9, 2026
2eb20c7
fix(host): pass the deployment route the entry's resolveAgent calls n…
MomentDerek Oct 9, 2026
a366d09
perf(build): bundle lib/types in place to trim the first frame
MomentDerek Oct 9, 2026
79c9c32
fix(host): say noDsh in the entry and stop there
MomentDerek Oct 9, 2026
85680e7
docs(host): record the lib bundle and the second pass at the noDsh ou…
MomentDerek Oct 9, 2026
dead590
fix(test): mirror the bundled lib in verify-update
MomentDerek Oct 9, 2026
c423d78
perf(startup): yield the update check past the first frame, import se…
MomentDerek Oct 9, 2026
a2b035f
fix(kernel): find the profile for the SDK install on the host entry
MomentDerek Oct 9, 2026
8119bc0
fix(launchpad): skip the preset roster warmup on kernels without /preset
MomentDerek Oct 9, 2026
4018dc7
docs(host): describe the light profile on non-DSH kernels and tidy th…
MomentDerek Oct 10, 2026
c923980
fix(test): read update.ts's body chunk in verify-update's source checks
MomentDerek Oct 10, 2026
aef2045
fix(test): follow the bundled lib in two compiled-module regressions
MomentDerek Oct 10, 2026
14fa5a3
fix(host): drop inherited resume flags on a fresh restart again
MomentDerek Oct 10, 2026
8aab1e7
fix(test): packaged layout for relocated presets, kernel list for sta…
MomentDerek Oct 10, 2026
de8a200
fix(ci): pin the entry's host types when checking an older DSH line
MomentDerek Oct 10, 2026
636dcbf
fix(channel): rebuild subagent control on /new, look DSH agents up sa…
MomentDerek Oct 10, 2026
e42bc10
chore(host): drop unused exports and one-off probes
MomentDerek Oct 10, 2026
61a29dc
docs(host): drop the implementation log and condense the startup meas…
MomentDerek Oct 10, 2026
5241b50
chore(host): trim the PR and drop the dsh CLI dev dependency
MomentDerek Oct 10, 2026
1acedcc
Merge remote-tracking branch 'upstream/main' into feat/standalone-host
MomentDerek Oct 10, 2026
fe4f6ff
chore(host): move the acceptance harness and lib bundling out of the PR
MomentDerek Oct 10, 2026
665e73f
docs(host): trim process narration from comments and the design doc
MomentDerek Oct 10, 2026
47ad0a2
refactor(host): drop the DSH_TUI_HOST_ENTRY_DSH switch
MomentDerek Oct 10, 2026
b68f7de
refactor(host): share one composition path between the DSH and light …
MomentDerek Oct 10, 2026
91005f0
refactor(host): trim the light-profile plan and the admission loader
MomentDerek Oct 10, 2026
37687d2
chore(host): drop unconsumed boot marks and file-local exports
MomentDerek Oct 10, 2026
b746318
test(host): merge duplicate checks and drop the dsh-hmr repro
MomentDerek Oct 10, 2026
c21a1c0
refactor(host): slim the replica snapshot and revert channel ctx renames
MomentDerek Oct 10, 2026
58d3f90
refactor(host): merge compose paths and drop single-use seams
MomentDerek Oct 10, 2026
d712d1a
docs(host): drop process narration from the design doc
MomentDerek Oct 10, 2026
91b9acf
refactor(host): fix review findings and slim the entry handshake
MomentDerek Oct 10, 2026
fd20791
Merge remote-tracking branch 'upstream/main' into feat/standalone-host
MomentDerek Oct 10, 2026
a8b257a
fix(host): address CodeRabbit review findings
MomentDerek Oct 10, 2026
6ad2e1f
fix(host): keep dsh host args on restart; assert no early submit afte…
MomentDerek Oct 11, 2026
f44de4f
ci: retrigger after a verify:sync-profile timeout
MomentDerek Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ADAPTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
| UI 层(`screens/`、`components/`、`hooks/`、`ink/`) | 不 import `src/backends/`;从 `src/dsh-adapter/` 只取类型,运行期经 facade(`src/dsh-adapter/types.ts` 的类型 re-export、`channel.ts`/`plugin.ts` 提供的服务)接触上游。存量值 import 登记在 `scripts/adapter-boundary.allowlist.json`,只减不增 |
| `native.dsh` | 只允许 `src/dsh-adapter/` 内访问 |
| `native.codex` | 只允许 `src/backends/codex/` 内访问 |
| `'@deepseek-ai/…'` 字符串字面量 | 只在 `src/dsh-adapter/` 内出现(运行期拼出来交给 `createRequire` / `import()` 的说明符也算) |
| 宿主专属模块(`host-contract.ts` 的 `HOST_MODULES`,cordis 除外,含 `@deepseek-ai/dsh` 本身) | 任何地方不得值 import;只由 `src/dsh-adapter/host-dsh.ts` 按宿主 realpath 动态加载,说明符只写在 `host-contract.ts`;`host-dsh.ts` 对 `@deepseek-ai/*` 只能 `import type`;`host-contract.ts` 只给 `host-dsh.ts` 与 `contract.ts` import |

### 后端 manifest

Expand Down Expand Up @@ -94,6 +96,30 @@ manifest 一致,过期即红)。
- 白名单包:blessed list(harness 包按完整版本号校验,框架包 cordis/schemastery 按 major 校验)
- 启动时:检测到 drift 打 warning;CI 上 `pnpm run verify:contract` 直接失败

## 独立入口的宿主契约

本包入口(`src/dsh-adapter/host-entry.ts`)在进程内加载 PATH 上 `dsh` 的安装
(docs/standalone-host-design.md),契约单一来源是
`src/dsh-adapter/host-contract.ts`:

- **依赖**:`host-dsh.ts` 取类型的 `dsh-app-boot`、`dsh-cmdline`、`dsh-home-paths`、
`dsh-http-proxy`、`dsh-launch-environment`(`HOST_TYPE_PACKAGES`)是 optional peer + dev、
进 blessed 清单。宿主 CLI `@deepseek-ai/dsh` **不是**依赖(它的依赖树是整个 CLI):
`host-dsh.ts` 本地声明入口读取的 `profile-boot` 三个导出。运行期**不**从本包解析
它们:入口按宿主 realpath 加载宿主自己的副本(profile 由宿主 dsh 安装、cordis 必须单实例),
所以它们也在运行期可缺的集合里(缺席不算 drift)。新包范围只写已核对的 `0.2.0-rc.2`
(`dsh-home-paths` 沿用家族宽范围)。`cordis-plugin-loader` 只当 `unknown` 用,不声明 peer。
- **能力探测**:`HOST_MODULES` 列出 8 个宿主模块与入口读取的每个导出。`loadHostDsh` 逐项检查,
缺模块 / 缺导出即抛出点名原因,入口回退(DSH 内核交给 `dsh --profile`,Claude 内核无宿主解析)
并把原因写 stderr 与界面通知。`host-dsh.ts` 用 `Pick<typeof 宿主模块, 契约导出名>` 取类型
(`profile-boot` 除外,本地声明),契约写了 pinned 宿主不存在的导出时编译失败。
- **复刻面**:`HOST_REPLICAS` 列出入口复刻的上游符号,`host-replica.snapshot.json` 记录在
`HOST_REPLICA_VERSION` 上声明各符号的 lib 文件的整文件 sha256(信号偏粗:该文件任何改动都要求复核);
刻意的偏差逐条记在 `HOST_DEVIATIONS`(同文件),审查复刻改动时以它为准。
- **门禁**:`verify:contract` 额外跑 `scripts/verify-host-contract.ts`(覆盖面见其头注释):假宿主回退
在 CI 上总跑;生产探测与复刻指纹只在有已装宿主(PATH 上的 `dsh` 或 `DSH_TUI_CONTRACT_DSH`)时跑。
移动版本线时在装有该版本 `dsh` 的机器上复核、把变化搬进 `host-dsh.ts`,再用 `--snapshot` 重写快照。

## Patch Surface

`cordis.patch.yml` 里对官方行的干预已快照到 `patch-surface.snapshot.json`:
Expand Down Expand Up @@ -128,6 +154,8 @@ web-app patch 按 include 语义合成一遍,直接拦截 loader entry id 复用
- dev 树由 `pnpm-workspace.yaml` 的 overrides 钉在 `0.2.0-rc.2`,
CI `alpha-compat` lane 还对同版上游 tag 的固定 SHA 做源码类型与 patch 合成校验。
旧 SQLite 迁移工具的依赖闭包单独锁在 `vendor/sqlite-island`。
- 主验证线移动时 `verify:contract` 会要求复核独立入口的复刻面(见「独立入口的宿主契约」),
`HOST_REPLICA_VERSION` 与主验证线一起改。
- `contract.ts` 是唯一真源:主验证线原地替换、不累积;`package.json` 的
peer/dev 范围、CI 钉住的上游 SHA、校验脚本里的版本常量都只是它的镜像,
必须同一次改齐(位置见 [docs/contributing.md](docs/contributing.md) 跨文件清单)。
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# AGENTS.md

dsh-TUI 是 DeepSeek Harness 的终端界面插件:零核心改动、纯插件挂载的交互式 TUI(`@deepseek-harness-tui/dsh-tui`)。Agent、会话、模型、工具、持久化与策略域由 DeepSeek Harness 拥有,本包只消费它们。改动前先读 [docs/contributing.md](docs/contributing.md)(本仓库共享开发契约的权威文本)与 [ADAPTER.md](ADAPTER.md)(上游边界与契约);整体结构见 [docs/architecture.md](docs/architecture.md)。
dsh-TUI 是 DeepSeek Harness 的终端界面应用(`@deepseek-harness-tui/dsh-tui`),零核心改动、只消费 DSH 的公开导出。DSH 是首要适配目标与首方后端;本包正从「DSH 的插件」演进为「拥有自身入口与组装根的终端应用」,届时 DSH 与 claude / codex 一样作为后端按需在进程内加载。Agent、会话、模型、工具、持久化与策略域仍然由 DeepSeek Harness 拥有,本包只消费它们。改动前先读 [docs/contributing.md](docs/contributing.md)(本仓库共享开发契约的权威文本)与 [ADAPTER.md](ADAPTER.md)(上游边界与契约);整体结构见 [docs/architecture.md](docs/architecture.md);独立宿主的方案与非目标见 [docs/standalone-host-design.md](docs/standalone-host-design.md)。

## 仓库布局

```
src/index.ts 公共 Cordis 插件入口、配置 Schema、对运行时实现的惰性移交
src/dsh-adapter/plugin.ts 运行时实现:TTY 校验、服务注册、Agent 创建/恢复、React 树挂载与收尾
src/dsh-adapter/host-entry.ts 本包入口:非 DSH 内核不组合完整 DSH profile(改组合轻量 profile:本包的行 + profile 声明的第三方插件)、在裸 Cordis 根上挂运行时;路由判定在 src/hostEntryRoute.ts(docs/standalone-host-design.md)
src/dsh-adapter/channel.ts Channel 入口:后端中立核心(channel/core/)+ 仅 DSH 会话挂载的扩展(channel/extensions.ts)
src/agent/ 后端中立的会话领域:AgentEvent、AgentSession、类型化能力(无 I/O、无厂商依赖)
src/channel/ 共享投影器(AgentEvent → 视图状态)与审批/问卷等中立 store
Expand All @@ -22,7 +23,7 @@ src/ink/ Ink 系渲染器与终端实现——敏感基础设施,
src/native-ts/ 渲染器使用的 Yoga 布局引擎
src/terminal-utils/ 终端格式化与呈现辅助
src/dsh-adapter/ 唯一允许 import 官方 @deepseek-ai/* 的位置;themes.ts 提供 tuiThemes 插件接缝
src/*Prefs.ts 等 ~/.dsh-tui 下的持久化用户偏好与会话元数据
src/*Prefs.ts 等 ~/.dsh-tui 下的持久化用户偏好与会话元数据;src/tuiSettingsFile.ts 是 /settings 的 dsh-tui 分区(settings.json)
.agents/skills/ 仅供仓库维护者使用的项目技能,不随 npm 包分发
presets/ 随包分发的 preset(liangshen)
bin/dsh-tui.js dsh-tui 直达命令入口
Expand Down Expand Up @@ -57,6 +58,7 @@ pnpm smoke # 通用无头屏幕组装冒烟

- 厂商包按目录隔离:`@deepseek-ai/*` 只在 `src/dsh-adapter/`,`@anthropic-ai/*` 只在 `src/backends/claude/`;后端中立层 `src/agent/`、`src/channel/` 不 import 厂商包、`src/dsh-adapter/` 与 `src/backends/`;UI 层不 import `src/backends/`,从 `src/dsh-adapter/` 只取类型(存量值 import 的 allowlist 只减不增)。完整规则表见 [ADAPTER.md](ADAPTER.md);`pnpm run verify:boundary` 扫描全部源码,越界即失败。
- 校验版本线、peer 范围与 blessed 包清单在 `src/dsh-adapter/contract.ts`;本地检测到 drift 打警告,CI 上 `verify:contract` 直接失败。
- 本包入口按宿主 realpath 加载已装 `dsh` 的模块:清单、复刻面与偏差的唯一来源是 `src/dsh-adapter/host-contract.ts`,加载只在 `host-dsh.ts`(对 `@deepseek-ai/*` 只 `import type`);`verify:contract` 跑假宿主回退,有已装宿主时再跑能力探测与复刻指纹(`host-replica.snapshot.json`);宿主 CLI 不是依赖,版本线移动时在装有该版本 `dsh` 的机器上复核。见 [ADAPTER.md](ADAPTER.md)「独立入口的宿主契约」。
- 运行时或发布类型引用的 `@deepseek-ai/*` 框架包必须同时是 peer 与 dev 依赖(`verify:manifest-deps` 门禁);仅测试/脚本使用的框架包只进 dev 依赖。
- `cordis.patch.yml` 对官方行的干预已快照到 `patch-surface.snapshot.json`,改动需保持同步(`verify:patch-surface` 门禁)。

Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@

# dsh-TUI

> An interactive terminal UI plugin for DeepSeek Harness. It ships a
> pixel-whale header, live work status, streaming thinking, double-Esc time
> rewind, a context progress bar, and a TPS gauge. It mounts as a pure plugin,
> with no core changes. Install to enable; uninstall leaves no patches behind.
> An interactive terminal UI for DeepSeek Harness. It ships a pixel-whale
> header, live work status, streaming thinking, double-Esc time rewind, a
> context progress bar, and a TPS gauge. Zero core changes: it consumes only
> DSH's public exports, and is moving from a mounted plugin to an app with its
> own entry and composition root, where DSH loads in process as the first-party
> backend. Install to enable; uninstall leaves no patches behind.

## Highlights

Expand Down Expand Up @@ -104,6 +106,10 @@ IDs. It requires matching profile dependencies with `@deepseek-ai/schemastery`
3.18.3 or newer; an incompatible schema stops TUI startup with repair guidance
instead of showing an uneditable settings page. Older hosts keep their legacy settings scope.

The `dsh-tui` section of `/settings` is saved in `~/.dsh-tui/settings.json`, shared by
both kernels; the first launch imports it once from the profile's `cordis.patch.yml`
(see [configuration](docs/configuration.en.md#tui-configuration)).

```sh
# Install the CLI and this plugin globally (ships the dsh-tui command)
npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui
Expand Down Expand Up @@ -215,6 +221,11 @@ including clicking outside it on the launchpad, cancels a pending installation.
- **Not available**: DSH-only commands such as `/tree`, `/preset`,
`/provider`, `/workspace`, `/agentview` and `/bg`. One process runs one
backend; `/kernel` switches by restarting into a new session.
- **Startup**: the screen appears before the session opens, with only a light
profile composed (this package plus the profile's third-party plugins); see
[Claude backend](docs/claude-backend.en.md#known-limitations). The DSH kernel
starts from the same entry and loads DSH in the same process behind its first
screen; `DSH_TUI_HOST_ENTRY=0` restores the old launch path for every kernel.

Details and known limitations: [Claude backend](docs/claude-backend.en.md).

Expand Down
10 changes: 8 additions & 2 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

# dsh-TUI

> 面向 DeepSeek Harness 的交互式终端界面插件:像素鲸鱼顶栏、实时工作状态、流式思考展示、双击 Esc 时间回溯、上下文进度条与 TPS 仪表。
> 零核心改动,纯插件挂载。安装即启用,卸载不留核心补丁。
> 面向 DeepSeek Harness 的交互式终端界面:像素鲸鱼顶栏、实时工作状态、流式思考展示、双击 Esc 时间回溯、上下文进度条与 TPS 仪表。
> 零核心改动,只消费 DSH 的公开导出;正从「挂载型插件」演进为拥有自身入口与组装根的终端应用,届时 DSH 作为首方后端在进程内加载。安装即启用,卸载不留核心补丁。

## 功能亮点

Expand Down Expand Up @@ -93,6 +93,9 @@ profile 依赖须配套,包含 `@deepseek-ai/schemastery` 3.18.3 或更新版
Schema 不兼容时,TUI 在启动阶段报错并提示修复安装,不再显示不可编辑的设置页。
旧 host 继续使用原有设置 scope。

`/settings` 的 `dsh-tui` 分区保存在 `~/.dsh-tui/settings.json`,两个内核共用;第一次启动时
从 profile 的 `cordis.patch.yml` 一次性导入(见[配置参考](docs/configuration.md#tui-配置))。

```sh
# 安装(全局,自带 dsh-tui 命令)
npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui
Expand Down Expand Up @@ -182,6 +185,9 @@ Enter,按引导一键安装——dsh-TUI 自己定位 profile 目录并装锁
双击 `Esc` 回退、子代理、后台任务、图片、`/btw`,以及 Claude 上报的美元费用。
- **不可用**:DSH 专属命令,如 `/tree`、`/preset`、`/provider`、`/workspace`、
`/agentview`、`/bg`。一个进程只跑一个后端,`/kernel` 切换时会重启并开新会话。
- **启动**:界面先于会话出现,只组合轻量 profile(本包与 profile 声明的第三方插件),
见 [Claude 后端](docs/claude-backend.md#已知限制)。DSH 内核从同一入口启动,界面先出现,
DSH 在同一进程里加载;`DSH_TUI_HOST_ENTRY=0` 让所有内核恢复原来的启动方式。

详细说明与已知限制:[Claude 后端](docs/claude-backend.md)。

Expand Down
84 changes: 74 additions & 10 deletions bin/dsh-tui.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* `DSH_TUI_LANG` 显式指定时从其值,否则默认中文(同 src/i18n.ts 的缺省)。
*/
import { spawn, spawnSync } from 'node:child_process'
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
Expand Down Expand Up @@ -118,6 +118,26 @@ const shellOpt = isWin ? { shell: true } : {}
const cmd = (command, args) =>
isWin ? [`${command} ${shellQuote(args).join(' ')}`, []] : [command, args]

// dsh CLI 预检(`dsh --version`),异步起、不占关键路径:本包入口不需要 dsh CLI,
// 只有 `dsh --profile` 出口(`requireDsh()`)与入口失败后的提示判定才消费结果。
// **不能** unref:`requireDsh()` await 它时若这是唯一活跃句柄,Node 会把顶层 await
// 判成永不结算、以 exit 13 终止启动器。
let dshProbe
const probeDsh = () => {
dshProbe ??= new Promise(resolve => {
const child = spawn(...cmd('dsh', ['--version']), { stdio: 'ignore', ...shellOpt })
child.on('error', () => resolve(false))
child.on('exit', code => resolve(code === 0))
})
return dshProbe
}

const requireDsh = async () => {
if (await probeDsh()) return
console.error(msg('noDsh'))
process.exit(1)
}

// 内联 semver(解析 + 严格大于):启动器可能在依赖不完整的环境里被执行
// (迁移、半损坏安装、测试沙箱),零外部依赖是自保底线。覆盖 semver 的
// 核心-先行版比较规则:先行版标识符逐段比(数字段按数值、小于字母段),
Expand Down Expand Up @@ -787,6 +807,34 @@ const startDshSession = (dshArgs, profile = PROFILE, env = process.env) =>
})
})

// entry 子进程的 Node 编译缓存:纯 env 注入,替身进程继承。用户显式设过
// `NODE_COMPILE_CACHE`(含空串)不覆盖。
// 路径沿用 `join(homedir(), '.dsh-tui', …)` 写法,**不要**抽成共用的模块级常量:
// verify-safe-mode.mjs 把 readLastRunRecord 至「TTY 判定」注释之间的源码切进只注入
// 少数全局的 vm 沙箱,区间内引用的外部常量会是 `undefined`,套件静默转红。
// (别把那两处切片标记原文抄进注释:会让脚本的 indexOf 提前命中。)
const withCompileCache = env => {
if (env.NODE_COMPILE_CACHE !== undefined) return env
const cacheDir = join(homedir(), '.dsh-tui', 'compile-cache')
try {
mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
} catch {
return env
}
return { ...env, NODE_COMPILE_CACHE: cacheDir }
}

// 本包自己的入口:`node <入口> <应用参数>`。结果模型与 startDshSession 相同。
const startEntrySession = (entry, appArgs, env = process.env) =>
new Promise(resolve => {
const child = spawn(process.execPath, [entry, ...appArgs], { stdio: 'inherit', env: withCompileCache(env) })
child.on('error', err => resolve({ kind: 'error', error: err }))
child.on('exit', (code, signal) => {
if (signal) resolve({ kind: 'signal', signal })
else resolve({ kind: 'exit', code: code ?? 0 })
})
})

// 救援子进程的环境:显式构造,而不是把宿主 process.env 原样交给它。救援的
// 语义是「干净冷启动」,而启动器自己写进 process.env 的会话控制变量会把刚
// 崩掉的主 profile 的会话 id / 工作区目标带进救援——救援 profile 里并不存在
Expand Down Expand Up @@ -1449,14 +1497,8 @@ if (!runningInsideProfile && ownVersion !== undefined && process.env.DSH_TUI_NO_
forwardExit(child)
} else {
// ─── profile 副本(或源码运行):完整启动逻辑 ─────────────────────────────
// dsh CLI 预检(缺失时给安装指引,先于一切 profile 逻辑)。
{
const probe = spawnSync(...cmd('dsh', ['--version']), { stdio: 'pipe', ...shellOpt })
if (probe.error || probe.status !== 0) {
console.error(msg('noDsh'))
process.exit(1)
}
}
// dsh CLI 预检:异步起,结果在要起 dsh 的出口才消费(见 probeDsh)。
void probeDsh()

let installedVersion
try {
Expand Down Expand Up @@ -1611,7 +1653,29 @@ if (!runningInsideProfile && ownVersion !== undefined && process.env.DSH_TUI_NO_
// DSH consumes its own --; only the app tail belongs behind it. Preserve
// the app-level separator too, and replay this same argv on a safe retry.
const firstArgs = [...hostArgs, ...(args.length > 0 ? ['--', ...args] : [])]

// 内核分流(docs/standalone-host-design.md):默认所有内核都走本包入口,由入口
// 判定内核(src/hostEntryRoute.ts)。DSH_TUI_HOST_ENTRY=0 让所有内核回到
// `dsh --profile`;dsh 自己的 hostArgs(--version、--dump-config* 等)始终交给 dsh。
const hostEntry = join(ownDir, 'lib', 'types', 'dsh-adapter', 'host-entry.js')
const hostEntryEnabled = process.env.DSH_TUI_HOST_ENTRY !== '0' && existsSync(hostEntry)
if (hostEntryEnabled) {
// 替身进程经它重起(src/update.ts restartArgv)。
process.env.DSH_TUI_HOST_ENTRY_PATH = hostEntry
process.env.DSH_TUI_PROFILE ??= PROFILE
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
// 必须在首次 spawn 之前:本次启动的 TUI 写的记录都晚于这个时刻。
noteLaunchChain()
settleFirstResult(await startDshSession(firstArgs), firstArgs)
if (hostEntryEnabled && hostArgs.length === 0) {
// The in-process DSH kernel reads the bundled guide skills like `dsh` does.
const result = await startEntrySession(hostEntry, args, withGuideSkillDir(process.env))
// 没有 dsh CLI:安全模式与排查提示都指向不存在的 `dsh --profile`(DSH 内核下
// 入口已打印 noDsh),原样退出。
if (result.kind === 'exit' && result.code !== 0 && !(await probeDsh())) process.exit(result.code)
settleFirstResult(result, firstArgs)
} else {
// `dsh --profile` 出口:hostArgs,或入口被关闭的非默认路径。
await requireDsh()
settleFirstResult(await startDshSession(firstArgs), firstArgs)
}
}
Loading
Loading