@crazyhappyone/dsh-tui 0.1.0-alpha.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/LICENSE +21 -0
- package/README.i18n.yaml +5 -0
- package/README.md +169 -0
- package/README.zh.md +169 -0
- package/bin/dsh-tui.js +72 -0
- package/package.json +38 -0
- package/src/launcher.js +253 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhangyangrui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Bilingual-pair consistency record: both languages carry equal authority.
|
|
2
|
+
# This source-only mirror has no DSH pairing runner. Recompute with:
|
|
3
|
+
# git hash-object README.md README.zh.md
|
|
4
|
+
README.md: 87fc088f1ef0e8f09d582ca4075d7be650ed5312
|
|
5
|
+
README.zh.md: 0f23e7e5d1f1a5f9b2fdf41f8bf4e593114b1111
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Installable source-runtime launcher and private source mirror for the DeepSeek Harness terminal interface."
|
|
3
|
+
kind: "package-group"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dsh-tui — DeepSeek Harness terminal interface
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`@crazyhappyone/dsh-tui` provides the `dsh-tui` command for running and safely updating the DeepSeek Harness terminal interface. The launcher manages a dedicated DSH source checkout, so it never pulls, resets, stashes, or cleans a contributor's development checkout. This repository also mirrors the `packages/tui/` bundle and renderer sources for review. DSH continues to own agents, sessions, tools, persistence, providers, permissions, and profile assembly.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Install the launcher](#install-the-launcher)
|
|
17
|
+
- [Run and update](#run-and-update)
|
|
18
|
+
- [Configure the source runtime](#configure-the-source-runtime)
|
|
19
|
+
- [Relationship to DSH](#relationship-to-dsh)
|
|
20
|
+
- [Packages](#packages)
|
|
21
|
+
- [Development workflow](#development-workflow)
|
|
22
|
+
- [Publish the npm package](#publish-the-npm-package)
|
|
23
|
+
- [Known limitations](#known-limitations)
|
|
24
|
+
- [Dev Note](#dev-note)
|
|
25
|
+
|
|
26
|
+
-----
|
|
27
|
+
|
|
28
|
+
<a id="install-the-launcher"></a>
|
|
29
|
+
## Install the launcher
|
|
30
|
+
|
|
31
|
+
The unscoped `dsh-tui` package belongs to another maintainer. This project publishes only under the authenticated `crazyhappyone` npm scope.
|
|
32
|
+
|
|
33
|
+
After a prerelease is published:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
npm install --global @crazyhappyone/dsh-tui@next
|
|
37
|
+
dsh-tui version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Installation creates only the launcher. It does not clone DSH, run `pnpm install`, prompt for Git credentials, or execute a postinstall script. Run `dsh-tui update` explicitly to create the source runtime.
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="run-and-update"></a>
|
|
45
|
+
## Run and update
|
|
46
|
+
|
|
47
|
+
Initialize the dedicated runtime, then start the TUI:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
dsh-tui update
|
|
51
|
+
dsh-tui
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Ordinary arguments pass to the `deepseek-tui` profile unchanged:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
dsh-tui "review this repository"
|
|
58
|
+
dsh-tui --resume <session-id>
|
|
59
|
+
dsh-tui --cwd <directory>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`dsh-tui update` clones the configured source when the runtime is absent. For an existing runtime it requires a clean worktree, fetches the configured ref, proves the current HEAD is its ancestor, applies only `git merge --ff-only`, and runs `pnpm install --frozen-lockfile`. Dirty or divergent history stops before HEAD changes.
|
|
63
|
+
|
|
64
|
+
`dsh-tui version` reports the launcher version, runtime directory, runtime Git SHA, and DSH version without network access. The exact words `update` and `version` are launcher commands; use `dsh-tui -- update` or `dsh-tui -- version` to send either word as a TUI task.
|
|
65
|
+
|
|
66
|
+
Updating the launcher and updating the runtime are separate operations:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
npm install --global @crazyhappyone/dsh-tui@next
|
|
70
|
+
dsh-tui update
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
-----
|
|
74
|
+
|
|
75
|
+
<a id="configure-the-source-runtime"></a>
|
|
76
|
+
## Configure the source runtime
|
|
77
|
+
|
|
78
|
+
The defaults track the owner's private `feat/deepseek-tui` branch. Git authorization for that repository is required. Other users can select a compatible accessible repository and ref.
|
|
79
|
+
|
|
80
|
+
| Environment variable | Default | Purpose |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `DSH_TUI_RUNTIME_DIR` | `~/.local/share/dsh-tui/runtime` | Dedicated clone used only by the launcher |
|
|
83
|
+
| `DSH_TUI_SOURCE_URL` | `https://github.com/zhangyang-crazy-one/deepseek-harness.git` | Git remote cloned and fetched by `update` |
|
|
84
|
+
| `DSH_TUI_SOURCE_REF` | `feat/deepseek-tui` | Branch or ref fetched by `update` |
|
|
85
|
+
| `DSH_TUI_PNPM` | `pnpm` | pnpm executable used for install and launch |
|
|
86
|
+
|
|
87
|
+
Values must be non-empty, and the runtime directory must be absolute. The launcher refuses a symbolic-link runtime path and an existing directory that is not a Git checkout. Child commands receive argument arrays without shell interpolation.
|
|
88
|
+
|
|
89
|
+
If dependency refresh fails after a fast-forward, the runtime stays at the reported new SHA but is not ready. Run the exact recovery command printed by the launcher. For dirty or divergent state, inspect only the dedicated runtime directory or select a new empty `DSH_TUI_RUNTIME_DIR`; the launcher never discards it automatically.
|
|
90
|
+
|
|
91
|
+
-----
|
|
92
|
+
|
|
93
|
+
<a id="relationship-to-dsh"></a>
|
|
94
|
+
## Relationship to DSH
|
|
95
|
+
|
|
96
|
+
The TUI is a DSH profile layer, not a separate agent runtime:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
@deepseek-ai/dsh-base
|
|
100
|
+
└─ @deepseek-ai/dsh-tui profile patch, terminal lifecycle, controller
|
|
101
|
+
└─ @deepseek-ai/dsh-tui-render Ink projection and terminal I/O
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The integration source lives on the private [`feat/deepseek-tui` DSH branch](https://github.com/zhangyang-crazy-one/deepseek-harness/tree/feat/deepseek-tui/packages/tui). Changes that affect DSH services, profile composition, assembled CLI snapshots, or Agent Notes belong in that monorepo and follow its architecture, testing, and documentation rules.
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="packages"></a>
|
|
109
|
+
## Packages
|
|
110
|
+
|
|
111
|
+
| Package | DSH shape | Responsibility |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| [`tui/`](tui/README.md) | Profile bundle plus runtime plugin | Composes the terminal layer over `dsh-base`, owns the live terminal session, and maps user actions to DSH services |
|
|
114
|
+
| [`tui-render/`](tui-render/README.md) | Library | Projects controller state through Ink without owning agents, persistence, or model requests |
|
|
115
|
+
|
|
116
|
+
-----
|
|
117
|
+
|
|
118
|
+
<a id="development-workflow"></a>
|
|
119
|
+
## Development workflow
|
|
120
|
+
|
|
121
|
+
Implement TUI behavior in the complete DSH checkout, run the checks selected by the changed behavior, and export the confirmed `packages/tui/` tree here. Develop the launcher in this repository with `npm test`; test installation through a packed tarball rather than a workspace link.
|
|
122
|
+
|
|
123
|
+
The launcher runtime must remain separate from the development checkout. To test against another source safely, point `DSH_TUI_RUNTIME_DIR` at a temporary absolute directory and configure `DSH_TUI_SOURCE_URL` and `DSH_TUI_SOURCE_REF`.
|
|
124
|
+
|
|
125
|
+
-----
|
|
126
|
+
|
|
127
|
+
<a id="publish-the-npm-package"></a>
|
|
128
|
+
## Publish the npm package
|
|
129
|
+
|
|
130
|
+
The operator authenticates through npm's browser flow and verifies the selected account:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
npm config delete //registry.npmjs.org/:_authToken
|
|
134
|
+
npm config set registry https://registry.npmjs.org/
|
|
135
|
+
npm login --registry=https://registry.npmjs.org/ --auth-type=web
|
|
136
|
+
npm whoami
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`npm whoami` must print `crazyhappyone`. Never place the password, OTP, or npm token in this repository, an issue, command output captured for review, or an AI conversation.
|
|
140
|
+
|
|
141
|
+
After tests and packed-install verification pass, publish a prerelease only with explicit operator approval:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
npm publish --access public --tag next
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When npm requires two-factor authentication, the operator supplies the current code locally:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
npm publish --access public --tag next --otp=<six-digit-code>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Promotion to `latest` is a separate release decision. Local implementation and verification never create or change a registry package automatically.
|
|
154
|
+
|
|
155
|
+
-----
|
|
156
|
+
|
|
157
|
+
<a id="known-limitations"></a>
|
|
158
|
+
## Known limitations
|
|
159
|
+
|
|
160
|
+
- **Private default source** — npm installation is public, but the default DSH runtime remote requires repository authorization; anonymous users must configure an accessible compatible source.
|
|
161
|
+
- **Source toolchain required** — runtime initialization requires Git, pnpm, and a DSH-supported Node version.
|
|
162
|
+
- **No automatic conflict resolution** — dirty or non-fast-forward runtime history requires explicit human action or a new runtime directory.
|
|
163
|
+
- **Runtime update is not launcher update** — `dsh-tui update` refreshes DSH source and dependencies; npm updates the launcher package.
|
|
164
|
+
- **Integration owns release readiness** — launcher success does not replace the DSH branch's behavior, snapshot, build, or hygiene checks.
|
|
165
|
+
|
|
166
|
+
<a id="dev-note"></a>
|
|
167
|
+
## Dev Note
|
|
168
|
+
|
|
169
|
+
None.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "DeepSeek Harness 终端界面的可安装源码运行时 launcher 与 private 源码镜像。"
|
|
3
|
+
kind: "package-group"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dsh-tui — DeepSeek Harness 终端界面
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`@crazyhappyone/dsh-tui` 提供 `dsh-tui` 命令,用于运行和安全更新 DeepSeek Harness 终端界面。Launcher 管理专用 DSH 源码 checkout,因此不会 pull、reset、stash 或 clean 贡献者的开发 checkout。本仓库还镜像 `packages/tui/` bundle 与 renderer 源码以供审查。Agent、会话、工具、持久化、provider、权限与 profile 组装仍由 DSH 拥有。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [安装 launcher](#install-the-launcher)
|
|
17
|
+
- [运行与更新](#run-and-update)
|
|
18
|
+
- [配置源码运行时](#configure-the-source-runtime)
|
|
19
|
+
- [与 DSH 的关系](#relationship-to-dsh)
|
|
20
|
+
- [包](#packages)
|
|
21
|
+
- [开发工作流](#development-workflow)
|
|
22
|
+
- [发布 npm 包](#publish-the-npm-package)
|
|
23
|
+
- [已知限制](#known-limitations)
|
|
24
|
+
- [开发备注](#dev-note)
|
|
25
|
+
|
|
26
|
+
-----
|
|
27
|
+
|
|
28
|
+
<a id="install-the-launcher"></a>
|
|
29
|
+
## 安装 launcher
|
|
30
|
+
|
|
31
|
+
无 scope 的 `dsh-tui` 包属于另一位 maintainer。本项目只在已认证的 `crazyhappyone` npm scope 下发布。
|
|
32
|
+
|
|
33
|
+
发布 prerelease 后运行:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
npm install --global @crazyhappyone/dsh-tui@next
|
|
37
|
+
dsh-tui version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
安装只创建 launcher。它不会 clone DSH、运行 `pnpm install`、请求 Git 凭据或执行 postinstall script。请显式运行 `dsh-tui update` 创建源码运行时。
|
|
41
|
+
|
|
42
|
+
-----
|
|
43
|
+
|
|
44
|
+
<a id="run-and-update"></a>
|
|
45
|
+
## 运行与更新
|
|
46
|
+
|
|
47
|
+
初始化专用运行时,然后启动 TUI:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
dsh-tui update
|
|
51
|
+
dsh-tui
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
普通参数会原样传递给 `deepseek-tui` profile:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
dsh-tui "review this repository"
|
|
58
|
+
dsh-tui --resume <session-id>
|
|
59
|
+
dsh-tui --cwd <directory>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
运行时不存在时,`dsh-tui update` 会 clone 配置的源码。对于现有运行时,它要求工作树干净、fetch 配置的 ref、证明当前 HEAD 是该 ref 的祖先、只应用 `git merge --ff-only`,再运行 `pnpm install --frozen-lockfile`。Dirty 或 divergent history 会在 HEAD 改变前停止。
|
|
63
|
+
|
|
64
|
+
`dsh-tui version` 会报告 launcher 版本、运行时目录、运行时 Git SHA 与 DSH 版本,并且不访问网络。精确单词 `update` 与 `version` 是 launcher 命令;使用 `dsh-tui -- update` 或 `dsh-tui -- version` 将其作为 TUI task 发送。
|
|
65
|
+
|
|
66
|
+
更新 launcher 与更新运行时是不同操作:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
npm install --global @crazyhappyone/dsh-tui@next
|
|
70
|
+
dsh-tui update
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
-----
|
|
74
|
+
|
|
75
|
+
<a id="configure-the-source-runtime"></a>
|
|
76
|
+
## 配置源码运行时
|
|
77
|
+
|
|
78
|
+
默认值跟踪所有者的 private `feat/deepseek-tui` 分支。该仓库需要 Git authorization。其他用户可以选择一个兼容且可访问的仓库与 ref。
|
|
79
|
+
|
|
80
|
+
| 环境变量 | 默认值 | 用途 |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `DSH_TUI_RUNTIME_DIR` | `~/.local/share/dsh-tui/runtime` | 仅供 launcher 使用的专用 clone |
|
|
83
|
+
| `DSH_TUI_SOURCE_URL` | `https://github.com/zhangyang-crazy-one/deepseek-harness.git` | `update` clone 和 fetch 的 Git remote |
|
|
84
|
+
| `DSH_TUI_SOURCE_REF` | `feat/deepseek-tui` | `update` fetch 的 branch 或 ref |
|
|
85
|
+
| `DSH_TUI_PNPM` | `pnpm` | 安装和启动所用的 pnpm executable |
|
|
86
|
+
|
|
87
|
+
所有值都不得为空,运行时目录必须是绝对路径。Launcher 会拒绝 symbolic-link runtime path,也会拒绝不是 Git checkout 的现有目录。Child command 接收参数数组,不使用 shell interpolation。
|
|
88
|
+
|
|
89
|
+
如果 fast-forward 后的依赖刷新失败,运行时会停留在报告的新 SHA,但不会被声明为 ready。请运行 launcher 打印的精确恢复命令。对于 dirty 或 divergent 状态,只检查专用运行时目录,或选择新的空 `DSH_TUI_RUNTIME_DIR`;launcher 不会自动丢弃它。
|
|
90
|
+
|
|
91
|
+
-----
|
|
92
|
+
|
|
93
|
+
<a id="relationship-to-dsh"></a>
|
|
94
|
+
## 与 DSH 的关系
|
|
95
|
+
|
|
96
|
+
TUI 是 DSH profile 层,而不是独立的 agent 运行时:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
@deepseek-ai/dsh-base
|
|
100
|
+
└─ @deepseek-ai/dsh-tui profile patch, terminal lifecycle, controller
|
|
101
|
+
└─ @deepseek-ai/dsh-tui-render Ink projection and terminal I/O
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
集成源码位于 private [`feat/deepseek-tui` DSH 分支](https://github.com/zhangyang-crazy-one/deepseek-harness/tree/feat/deepseek-tui/packages/tui)。影响 DSH 服务、profile 组合、组装后 CLI snapshot 或 Agent Note 的变更归该 monorepo 所有,并遵循其中的架构、测试与文档规则。
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="packages"></a>
|
|
109
|
+
## 包
|
|
110
|
+
|
|
111
|
+
| 包 | DSH 形态 | 职责 |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| [`tui/`](tui/README.zh.md) | Profile bundle 与运行时插件 | 在 `dsh-base` 上组合终端层、拥有 live 终端会话,并将用户动作映射到 DSH 服务 |
|
|
114
|
+
| [`tui-render/`](tui-render/README.zh.md) | Library | 通过 Ink 投影控制器状态,但不拥有 agent、持久化或模型请求 |
|
|
115
|
+
|
|
116
|
+
-----
|
|
117
|
+
|
|
118
|
+
<a id="development-workflow"></a>
|
|
119
|
+
## 开发工作流
|
|
120
|
+
|
|
121
|
+
在完整 DSH checkout 中实现 TUI 行为,运行受变更行为影响的检查,并将确认后的 `packages/tui/` tree 导出到这里。在本仓库中使用 `npm test` 开发 launcher;通过 packed tarball 测试安装,不使用 workspace link。
|
|
122
|
+
|
|
123
|
+
Launcher runtime 必须与开发 checkout 分离。要安全测试其他源码,请将 `DSH_TUI_RUNTIME_DIR` 指向临时绝对目录,并配置 `DSH_TUI_SOURCE_URL` 与 `DSH_TUI_SOURCE_REF`。
|
|
124
|
+
|
|
125
|
+
-----
|
|
126
|
+
|
|
127
|
+
<a id="publish-the-npm-package"></a>
|
|
128
|
+
## 发布 npm 包
|
|
129
|
+
|
|
130
|
+
Operator 通过 npm browser flow 认证,并验证所选账号:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
npm config delete //registry.npmjs.org/:_authToken
|
|
134
|
+
npm config set registry https://registry.npmjs.org/
|
|
135
|
+
npm login --registry=https://registry.npmjs.org/ --auth-type=web
|
|
136
|
+
npm whoami
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`npm whoami` 必须打印 `crazyhappyone`。绝不要把密码、OTP 或 npm token 放入本仓库、issue、供审查捕获的命令输出或 AI conversation。
|
|
140
|
+
|
|
141
|
+
测试与 packed-install 验证通过后,仅在 operator 显式批准时发布 prerelease:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
npm publish --access public --tag next
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
npm 要求双因素认证时,operator 在本地提供当前验证码:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
npm publish --access public --tag next --otp=<six-digit-code>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
提升到 `latest` 是单独的 release 决策。本地实现与验证绝不会自动创建或更改 registry package。
|
|
154
|
+
|
|
155
|
+
-----
|
|
156
|
+
|
|
157
|
+
<a id="known-limitations"></a>
|
|
158
|
+
## 已知限制
|
|
159
|
+
|
|
160
|
+
- **Private 默认源码** — npm 安装是 public,但默认 DSH runtime remote 需要仓库授权;匿名用户必须配置一个可访问的兼容源码。
|
|
161
|
+
- **需要源码工具链** — 初始化运行时需要 Git、pnpm 与 DSH 支持的 Node 版本。
|
|
162
|
+
- **不自动解决冲突** — dirty 或 non-fast-forward runtime history 需要显式人工操作或新的运行时目录。
|
|
163
|
+
- **运行时更新不是 launcher 更新** — `dsh-tui update` 刷新 DSH 源码与依赖;npm 更新 launcher package。
|
|
164
|
+
- **集成分支拥有发布就绪状态** — launcher 成功不能替代 DSH 分支的行为、snapshot、build 或 hygiene 检查。
|
|
165
|
+
|
|
166
|
+
<a id="dev-note"></a>
|
|
167
|
+
## 开发备注
|
|
168
|
+
|
|
169
|
+
无。
|
package/bin/dsh-tui.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Command-line entry for the dsh-tui source-runtime launcher. */
|
|
3
|
+
|
|
4
|
+
import { lstatSync, mkdirSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { constants, homedir } from 'node:os'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { parseLauncherInvocation, resolveLauncherSettings, runLauncher } from '../src/launcher.js'
|
|
9
|
+
|
|
10
|
+
function inspectPath(path) {
|
|
11
|
+
try {
|
|
12
|
+
const stat = lstatSync(path)
|
|
13
|
+
if (stat.isSymbolicLink()) return 'symlink'
|
|
14
|
+
if (stat.isDirectory()) return 'directory'
|
|
15
|
+
if (stat.isFile()) return 'file'
|
|
16
|
+
return 'other'
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return 'missing'
|
|
19
|
+
throw error
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function run(command, args, options = {}) {
|
|
24
|
+
const capture = options.capture === true
|
|
25
|
+
const result = spawnSync(command, args, {
|
|
26
|
+
cwd: options.cwd,
|
|
27
|
+
encoding: capture ? 'utf8' : undefined,
|
|
28
|
+
env: process.env,
|
|
29
|
+
maxBuffer: capture ? 16 * 1024 * 1024 : undefined,
|
|
30
|
+
shell: false,
|
|
31
|
+
stdio: options.stdio === 'inherit' ? 'inherit' : capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
32
|
+
})
|
|
33
|
+
if (result.error !== undefined) {
|
|
34
|
+
return { status: 1, stdout: '', stderr: result.error.message }
|
|
35
|
+
}
|
|
36
|
+
const status = result.status ?? signalOutcome(result.signal)
|
|
37
|
+
return {
|
|
38
|
+
status,
|
|
39
|
+
stdout: typeof result.stdout === 'string' ? result.stdout : '',
|
|
40
|
+
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
41
|
+
...(result.signal === null ? {} : { signal: result.signal }),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function signalOutcome(signal) {
|
|
46
|
+
if (signal === null) return 1
|
|
47
|
+
const number = constants.signals[signal]
|
|
48
|
+
return number === undefined ? 1 : 128 + number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const manifest = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'))
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const invocation = parseLauncherInvocation(process.argv.slice(2))
|
|
55
|
+
const settings = resolveLauncherSettings({ env: process.env, homeDirectory: homedir() })
|
|
56
|
+
process.exitCode = runLauncher({
|
|
57
|
+
invocation,
|
|
58
|
+
settings,
|
|
59
|
+
packageVersion: manifest.version,
|
|
60
|
+
adapters: {
|
|
61
|
+
inspectPath,
|
|
62
|
+
makeDirectory: path => mkdirSync(path, { recursive: true, mode: 0o700 }),
|
|
63
|
+
readText: path => readFileSync(path, 'utf8'),
|
|
64
|
+
run,
|
|
65
|
+
writeOut: text => process.stdout.write(text),
|
|
66
|
+
writeError: text => process.stderr.write(text),
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
} catch (error) {
|
|
70
|
+
process.stderr.write(`dsh-tui: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
71
|
+
process.exitCode = 1
|
|
72
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crazyhappyone/dsh-tui",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Source-runtime launcher for the DeepSeek Harness terminal interface",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dsh-tui": "bin/dsh-tui.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/dsh-tui.js",
|
|
12
|
+
"src/**/*.js",
|
|
13
|
+
"README.md",
|
|
14
|
+
"README.zh.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"pack:check": "npm pack --dry-run --json"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"tag": "next"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/zhangyang-crazy-one/dsh-tui.git"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"deepseek",
|
|
34
|
+
"dsh",
|
|
35
|
+
"tui",
|
|
36
|
+
"terminal"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-runtime launcher for the DeepSeek Harness TUI.
|
|
3
|
+
* @module @crazyhappyone/dsh-tui/launcher
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SOURCE_URL = 'https://github.com/zhangyang-crazy-one/deepseek-harness.git'
|
|
9
|
+
const DEFAULT_SOURCE_REF = 'feat/deepseek-tui'
|
|
10
|
+
|
|
11
|
+
/** @typedef {'missing' | 'directory' | 'file' | 'symlink' | 'other'} PathKind */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} LauncherSettings
|
|
15
|
+
* @property {string} runtimeDirectory
|
|
16
|
+
* @property {string} sourceUrl
|
|
17
|
+
* @property {string} sourceRef
|
|
18
|
+
* @property {string} packageManager
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} CommandResult
|
|
23
|
+
* @property {number} status
|
|
24
|
+
* @property {string} stdout
|
|
25
|
+
* @property {string} stderr
|
|
26
|
+
* @property {string | undefined} [signal]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} LauncherAdapters
|
|
31
|
+
* @property {(path: string) => PathKind} inspectPath
|
|
32
|
+
* @property {(path: string) => void} makeDirectory
|
|
33
|
+
* @property {(path: string) => string} readText
|
|
34
|
+
* @property {(command: string, args: string[], options?: {cwd?: string, stdio?: 'inherit', capture?: boolean}) => CommandResult} run
|
|
35
|
+
* @property {(text: string) => void} writeOut
|
|
36
|
+
* @property {(text: string) => void} writeError
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse the launcher's reserved management commands.
|
|
41
|
+
* @param {readonly string[]} argv - arguments after the executable.
|
|
42
|
+
* @returns {{kind: 'version'} | {kind: 'update'} | {kind: 'launch', args: string[]}}
|
|
43
|
+
*/
|
|
44
|
+
export function parseLauncherInvocation(argv) {
|
|
45
|
+
if (argv[0] === '--') return { kind: 'launch', args: [...argv.slice(1)] }
|
|
46
|
+
if (argv[0] === 'version') {
|
|
47
|
+
if (argv.length !== 1) throw new Error('version takes no arguments; use `dsh-tui -- version ...` to send it as a task')
|
|
48
|
+
return { kind: 'version' }
|
|
49
|
+
}
|
|
50
|
+
if (argv[0] === 'update') {
|
|
51
|
+
if (argv.length !== 1) throw new Error('update takes no arguments; use `dsh-tui -- update ...` to send it as a task')
|
|
52
|
+
return { kind: 'update' }
|
|
53
|
+
}
|
|
54
|
+
return { kind: 'launch', args: [...argv] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve deployment settings without touching the filesystem.
|
|
59
|
+
* @param {{env: Readonly<Record<string, string | undefined>>, homeDirectory: string}} input
|
|
60
|
+
* @returns {LauncherSettings}
|
|
61
|
+
*/
|
|
62
|
+
export function resolveLauncherSettings({ env, homeDirectory }) {
|
|
63
|
+
const runtimeDirectory = environmentValue(
|
|
64
|
+
env,
|
|
65
|
+
'DSH_TUI_RUNTIME_DIR',
|
|
66
|
+
join(homeDirectory, '.local', 'share', 'dsh-tui', 'runtime'),
|
|
67
|
+
)
|
|
68
|
+
if (!isAbsolute(runtimeDirectory)) throw new Error('DSH_TUI_RUNTIME_DIR must be an absolute path')
|
|
69
|
+
return {
|
|
70
|
+
runtimeDirectory,
|
|
71
|
+
sourceUrl: environmentValue(env, 'DSH_TUI_SOURCE_URL', DEFAULT_SOURCE_URL),
|
|
72
|
+
sourceRef: environmentValue(env, 'DSH_TUI_SOURCE_REF', DEFAULT_SOURCE_REF),
|
|
73
|
+
packageManager: environmentValue(env, 'DSH_TUI_PNPM', 'pnpm'),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run one parsed launcher invocation.
|
|
79
|
+
* @param {{invocation: ReturnType<typeof parseLauncherInvocation>, settings: LauncherSettings, packageVersion: string, adapters: LauncherAdapters}} input
|
|
80
|
+
* @returns {number} process exit status.
|
|
81
|
+
*/
|
|
82
|
+
export function runLauncher({ invocation, settings, packageVersion, adapters }) {
|
|
83
|
+
switch (invocation.kind) {
|
|
84
|
+
case 'version':
|
|
85
|
+
return showVersion({ settings, packageVersion, adapters })
|
|
86
|
+
case 'update':
|
|
87
|
+
return updateRuntime({ settings, adapters })
|
|
88
|
+
case 'launch':
|
|
89
|
+
return launchTui({ args: invocation.args, settings, adapters })
|
|
90
|
+
default:
|
|
91
|
+
return assertNever(invocation)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function environmentValue(env, name, fallback) {
|
|
96
|
+
const value = env[name]
|
|
97
|
+
if (value === undefined) return fallback
|
|
98
|
+
if (value.trim() === '') throw new Error(`${name} must not be empty`)
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runtimeState(settings, adapters) {
|
|
103
|
+
const kind = adapters.inspectPath(settings.runtimeDirectory)
|
|
104
|
+
if (kind === 'symlink') return { ok: false, message: `runtime path is a symbolic link: ${settings.runtimeDirectory}` }
|
|
105
|
+
if (kind === 'missing') return { ok: true, initialized: false }
|
|
106
|
+
if (kind !== 'directory') return { ok: false, message: `runtime path is not a directory: ${settings.runtimeDirectory}` }
|
|
107
|
+
const gitKind = adapters.inspectPath(join(settings.runtimeDirectory, '.git'))
|
|
108
|
+
if (gitKind !== 'directory' && gitKind !== 'file') {
|
|
109
|
+
return { ok: false, message: `runtime directory is not a Git checkout: ${settings.runtimeDirectory}` }
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, initialized: true }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function launchTui({ args, settings, adapters }) {
|
|
115
|
+
const state = runtimeState(settings, adapters)
|
|
116
|
+
if (!state.ok) return reportError(adapters, state.message)
|
|
117
|
+
if (!state.initialized) {
|
|
118
|
+
return reportError(adapters, `runtime is not initialized at ${settings.runtimeDirectory}; run \`dsh-tui update\` first`)
|
|
119
|
+
}
|
|
120
|
+
const result = adapters.run(
|
|
121
|
+
settings.packageManager,
|
|
122
|
+
['dsh', '--profile', 'deepseek-tui', ...args],
|
|
123
|
+
{ cwd: settings.runtimeDirectory, stdio: 'inherit' },
|
|
124
|
+
)
|
|
125
|
+
return result.status
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function showVersion({ settings, packageVersion, adapters }) {
|
|
129
|
+
adapters.writeOut(`dsh-tui ${packageVersion}\n`)
|
|
130
|
+
adapters.writeOut(`runtime directory: ${settings.runtimeDirectory}\n`)
|
|
131
|
+
const state = runtimeState(settings, adapters)
|
|
132
|
+
if (!state.ok) return reportError(adapters, state.message)
|
|
133
|
+
if (!state.initialized) {
|
|
134
|
+
adapters.writeOut('runtime: uninitialized\n')
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
const revision = captureGit(adapters, settings.runtimeDirectory, ['rev-parse', 'HEAD'], 'read runtime HEAD')
|
|
138
|
+
if (!revision.ok) return revision.status
|
|
139
|
+
adapters.writeOut(`runtime SHA: ${revision.stdout}\n`)
|
|
140
|
+
adapters.writeOut(`DSH version: ${readDshVersion(settings.runtimeDirectory, adapters)}\n`)
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readDshVersion(runtimeDirectory, adapters) {
|
|
145
|
+
try {
|
|
146
|
+
const manifest = JSON.parse(adapters.readText(join(runtimeDirectory, 'package.json')))
|
|
147
|
+
return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown'
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return `unreadable (${errorMessage(error)})`
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updateRuntime({ settings, adapters }) {
|
|
154
|
+
const state = runtimeState(settings, adapters)
|
|
155
|
+
if (!state.ok) return reportError(adapters, state.message)
|
|
156
|
+
if (!state.initialized) return cloneRuntime({ settings, adapters })
|
|
157
|
+
|
|
158
|
+
const clean = captureGit(adapters, settings.runtimeDirectory, ['status', '--porcelain'], 'inspect runtime worktree')
|
|
159
|
+
if (!clean.ok) return clean.status
|
|
160
|
+
if (clean.stdout !== '') {
|
|
161
|
+
return reportError(adapters, `runtime worktree is not clean: ${settings.runtimeDirectory}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const before = captureGit(adapters, settings.runtimeDirectory, ['rev-parse', 'HEAD'], 'read current runtime HEAD')
|
|
165
|
+
if (!before.ok) return before.status
|
|
166
|
+
const fetched = adapters.run(
|
|
167
|
+
'git',
|
|
168
|
+
['fetch', '--no-tags', '--', settings.sourceUrl, settings.sourceRef],
|
|
169
|
+
{ cwd: settings.runtimeDirectory, capture: true },
|
|
170
|
+
)
|
|
171
|
+
if (fetched.status !== 0) {
|
|
172
|
+
return reportCommandFailure(adapters, 'fetch configured runtime source', fetched, [settings.sourceUrl])
|
|
173
|
+
}
|
|
174
|
+
const after = captureGit(adapters, settings.runtimeDirectory, ['rev-parse', 'FETCH_HEAD'], 'read fetched runtime HEAD')
|
|
175
|
+
if (!after.ok) return after.status
|
|
176
|
+
|
|
177
|
+
const ancestry = adapters.run(
|
|
178
|
+
'git',
|
|
179
|
+
['merge-base', '--is-ancestor', before.stdout, after.stdout],
|
|
180
|
+
{ cwd: settings.runtimeDirectory, capture: true },
|
|
181
|
+
)
|
|
182
|
+
if (ancestry.status === 1) {
|
|
183
|
+
return reportError(
|
|
184
|
+
adapters,
|
|
185
|
+
`runtime update is not a fast-forward (${before.stdout} -> ${after.stdout}); replace the dedicated runtime only after reviewing its history`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
if (ancestry.status !== 0) return reportCommandFailure(adapters, 'verify runtime ancestry', ancestry)
|
|
189
|
+
|
|
190
|
+
const merge = adapters.run('git', ['merge', '--ff-only', after.stdout], {
|
|
191
|
+
cwd: settings.runtimeDirectory,
|
|
192
|
+
capture: true,
|
|
193
|
+
})
|
|
194
|
+
if (merge.status !== 0) return reportCommandFailure(adapters, 'fast-forward runtime', merge)
|
|
195
|
+
const installStatus = installDependencies({ settings, adapters, currentSha: after.stdout })
|
|
196
|
+
if (installStatus !== 0) return installStatus
|
|
197
|
+
adapters.writeOut(`runtime updated: ${before.stdout} -> ${after.stdout}\n`)
|
|
198
|
+
return 0
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cloneRuntime({ settings, adapters }) {
|
|
202
|
+
adapters.makeDirectory(dirname(settings.runtimeDirectory))
|
|
203
|
+
const clone = adapters.run(
|
|
204
|
+
'git',
|
|
205
|
+
['clone', '--branch', settings.sourceRef, '--single-branch', '--', settings.sourceUrl, settings.runtimeDirectory],
|
|
206
|
+
{ capture: true },
|
|
207
|
+
)
|
|
208
|
+
if (clone.status !== 0) return reportCommandFailure(adapters, 'clone configured runtime source', clone, [settings.sourceUrl])
|
|
209
|
+
const installStatus = installDependencies({ settings, adapters, currentSha: 'new checkout' })
|
|
210
|
+
if (installStatus !== 0) return installStatus
|
|
211
|
+
const revision = captureGit(adapters, settings.runtimeDirectory, ['rev-parse', 'HEAD'], 'read cloned runtime HEAD')
|
|
212
|
+
if (!revision.ok) return revision.status
|
|
213
|
+
adapters.writeOut(`runtime initialized: ${revision.stdout}\n`)
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function installDependencies({ settings, adapters, currentSha }) {
|
|
218
|
+
const install = adapters.run(settings.packageManager, ['install', '--frozen-lockfile'], {
|
|
219
|
+
cwd: settings.runtimeDirectory,
|
|
220
|
+
stdio: 'inherit',
|
|
221
|
+
})
|
|
222
|
+
if (install.status === 0) return 0
|
|
223
|
+
adapters.writeError(`dsh-tui: dependency refresh failed at ${currentSha}; runtime is not ready\n`)
|
|
224
|
+
adapters.writeError(`dsh-tui: recover with: cd ${settings.runtimeDirectory} && ${settings.packageManager} install --frozen-lockfile\n`)
|
|
225
|
+
return install.status
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function captureGit(adapters, cwd, args, label) {
|
|
229
|
+
const result = adapters.run('git', args, { cwd, capture: true })
|
|
230
|
+
if (result.status !== 0) return { ok: false, status: reportCommandFailure(adapters, label, result) }
|
|
231
|
+
return { ok: true, stdout: result.stdout.trim() }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reportCommandFailure(adapters, label, result, redactions = []) {
|
|
235
|
+
let detail = result.stderr.trim()
|
|
236
|
+
for (const value of redactions) detail = detail.replaceAll(value, '<source>')
|
|
237
|
+
detail = detail.replaceAll(/https?:\/\/[^/@\s]+@/g, 'https://<credentials>@')
|
|
238
|
+
adapters.writeError(`dsh-tui: ${label} failed${detail === '' ? '' : `: ${detail}`}\n`)
|
|
239
|
+
return result.status === 0 ? 1 : result.status
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function reportError(adapters, message) {
|
|
243
|
+
adapters.writeError(`dsh-tui: ${message}\n`)
|
|
244
|
+
return 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function errorMessage(error) {
|
|
248
|
+
return error instanceof Error ? error.message : String(error)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function assertNever(value) {
|
|
252
|
+
throw new Error(`unhandled launcher invocation: ${JSON.stringify(value)}`)
|
|
253
|
+
}
|