Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e1d46a5
feat(tui): 新增工具参数表单与运行面板,支持 CJK 感知布局与剪贴板复制
kugouming Sep 11, 2026
c8a13e0
fix(tui): 修复列表/表单布局溢出、输入污染与退出失效等审计问题
kugouming Sep 11, 2026
075b455
test(tui): 新增 tmux 驱动的 TUI 端到端回归脚本并同步文档
kugouming Sep 11, 2026
dbfc9c4
feat(scripts): deploy:local 支持由 launchd 守护的 daemon 重启
kugouming Sep 11, 2026
5852615
fix(config): 拒绝归一化后不含 ASCII 字母数字的服务名
kugouming Sep 12, 2026
217a92f
fix(tui): 对齐路由层重试语义,并修复旧 UI 高度/按键与 CJK 开窗
kugouming Sep 12, 2026
de3ecbc
test(tui): 补 T9-T13 场景与三类集成覆盖,合并重复的终端仿真器
kugouming Sep 12, 2026
c699dd6
chore: .tsx 纳入 lint/format、CI 增加 TUI E2E,并清理死代码
kugouming Sep 12, 2026
d06a13b
fix(scripts): deploy:local 加固 launchd 分支(过期 pidfile + 不轻信重启退出码)
kugouming Sep 12, 2026
47718a7
style(tui): 补上 ServiceFormUnified 的 prettier 格式化
kugouming Sep 12, 2026
de8f92c
fix(hooks): pre-commit 守卫 lint:fix 的修复落在工作区未入库
kugouming Sep 12, 2026
89a5dbe
chore(ci): 临时诊断步骤:抓 TUI 在 CI 上的自身输出与退出码
kugouming Sep 12, 2026
d9b34f7
chore(ci): 诊断 v2:最小 Ink 探针 + 面板 TTY 尺寸 + TERM/配置变体
kugouming Sep 12, 2026
03526e6
chore(ci): 诊断 v3:抓原始 PTY 字节(我们的应用 vs 最小 Ink),去掉会中止步骤的 -e 陷阱
kugouming Sep 12, 2026
da7d9f4
ci(tui): TUI E2E 任务改跑 macOS,移除临时诊断步骤
kugouming Sep 12, 2026
a97244f
docs: 说明 TUI E2E 任务跑在 macOS 及其原因
kugouming Sep 12, 2026
530070e
fix(tui-e2e): 剥掉 CI 标记,根因是 Ink 在 CI 模式下只画最后一帧
kugouming Sep 12, 2026
1d340bc
test(tui-e2e): 命令行级剥离 CI 标记,并加一轮对照诊断
kugouming Sep 12, 2026
afc7fba
test(tui-e2e): 慢机器上等焦点就位再输入,剪贴板断言接受无工具环境
kugouming Sep 12, 2026
5225478
test(tui-e2e): 按键之间留稳定间隔,导航后等状态提交
kugouming Sep 12, 2026
e16bdda
fix(tui): 存档提示不再把剪贴板失败藏成「已保存」,并补两种结果的断言
kugouming Sep 12, 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ jobs:
path: dist/
retention-days: 1

# ── TUI end-to-end (needs a real terminal: tmux drives a private pty) ──
tui-e2e:
name: TUI E2E
needs: build
# Runs headless: scripts/tui-e2e.mjs strips CI / CONTINUOUS_INTEGRATION / CI_*
# from the tmux session env, because Ink (is-in-ci) paints only the FINAL
# frame in CI mode — without that, every scenario would watch a blank pane.
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: ./.github/actions/setup

- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux

- name: Build
run: npm run build

- name: Run TUI end-to-end scenarios
run: npm run verify:tui
env:
# CI runners are slower to re-render between keystrokes than a local
# machine; scripts/tui-e2e.mjs reads this settle delay.
TUI_E2E_KEY_DELAY: '250'

# ── Cross-platform unit & property tests ─────────────────────────────
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
Expand Down
27 changes: 27 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,35 @@ export NVM_DIR="$HOME/.nvm"

echo "Running pre-commit checks..."

# `lint:fix` rewrites files in the WORKING TREE, but a commit records the INDEX.
# A fix that lands after `git add` is therefore left out of the commit while
# `format:check` (which inspects the working tree) still passes — CI then fails
# on the very file you thought you fixed. Stop and ask for a re-stage instead.
staged=$(git diff --cached --name-only --diff-filter=ACM)
if [ -n "$staged" ]; then
before=$(git diff --name-only -- $staged | sort)
else
before=""
fi

npm run lint:fix || { echo "❌ Lint failed. Fix the issues and try again."; exit 1; }

if [ -n "$staged" ]; then
after=$(git diff --name-only -- $staged | sort)
if [ "$before" != "$after" ]; then
echo "❌ lint:fix 修改了已暂存文件,但改动只落在工作区(提交会漏掉它)。"
echo " 受影响文件:"
for f in $after; do
case " $before " in
*" $f "*) ;;
*) echo " $f" ;;
esac
done
echo " 请重新 git add 后再提交。"
exit 1
fi
fi

npm run format:check || { echo "❌ Format check failed. Run 'npm run format' and try again."; exit 1; }

npm run typecheck || { echo "❌ Type check failed. Fix the type errors and try again."; exit 1; }
Expand Down
63 changes: 58 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@
| `npm run test:watch` | Watch mode tests |
| `npm run test:coverage` | Coverage report (thresholds: 80% lines/fn/stmt, 75% branches) |
| `npm run test:property` | Property-based tests (fast-check) |
| `npm run deploy:local` | 编译 → npm 打包真实 tarball → 全局安装(完整替代旧 onemcp 命令)→ 重启 ~/.onemcp daemon → 冒烟 |
| `npm run verify:local` | 端到端回归:重新编译安装后,以独立实例(随机端口)跑全部场景 case |
| `npm run deploy:local` | 编译 → npm 打包真实 tarball → 全局安装(完整替代旧 onemcp 命令)→ 重启 daemon(launchd 守护时自动走 `launchctl kickstart`)→ 就绪冒烟 |
| `npm run verify:local` | 端到端回归:重新编译安装后,以独立实例(随机端口)跑全部场景 case(N*/F*) |
| `npm run verify:tui` | TUI 端到端回归:tmux 驱动真实终端跑交互场景(T*),不动 daemon/用户配置 |
| `npm run lint` / `lint:fix` | ESLint |
| `npm run format` / `format:check` | Prettier |
| `npm run typecheck` | TypeScript check only |
| `npx vitest run <file>` | Single test file |
| `npx vitest run -t "<name>"` | Single test by name |

> **本机注记(daemon 守护方式)**::5625 由 launchd 守护 `site.iskill.onemcp`
> (plist `~/Library/LaunchAgents/site.iskill.onemcp.plist`,`RunAtLoad` + `KeepAlive`)直接运行**全局安装的 `onemcp`**,
> 因此 `deploy:local` 装的本地构建只要重启守护即可生效(脚本会自动检测并用 `launchctl kickstart -k` 重启)。
> 这类守护进程**不写** `~/.onemcp/server.pid`,手动切版用
> `launchctl kickstart -k gui/$(id -u)/site.iskill.onemcp`;plist 备份见同目录 `*.bak-*`。

---

## E2E 场景回归规则(必须遵守)
Expand All @@ -37,7 +44,14 @@
修复类问题放入 `F*`(故障恢复),新功能/正常操作放入 `N*`(正常场景),编号顺延。
确保每个问题都能在端到端层面复现与验证,迭代过程始终可用全局 case 回归。
2. **每次修改代码后的标准验证链**(全绿才算完成):
`npm test` → `npm run deploy:local` → `npm run verify:local`
`npm test` → `npm run build && npm run verify:tui`(改动涉及 TUI 时)→
`npm run deploy:local` → `npm run verify:local`
- CI(`.github/workflows/ci.yml`)跑 `lint` / `typecheck` / `build` / 单测 / 集成测试 / coverage
以及 **TUI E2E**(tui-e2e 任务,`verify:tui`);
`verify:local` 需要全局安装,仍是本地门禁,CI 不跑。
- 驱动脚本会为 tmux 会话**剥掉 `CI` / `CONTINUOUS_INTEGRATION` / `CI_*`**(`withoutCiMarkers`):
Ink 用 `is-in-ci` 判断 CI 环境,命中时只在**退出时**绘制最后一帧,驱动会全程看到空屏
(这是排查「CI 里 TUI 未就绪」类问题的第一嫌疑人,而不是应用代码)。
3. **场景编写约定**:
- 随机空闲端口 + `mkdtemp` 独立临时配置,绝不触碰 :5625 运行实例
- mock 后端自带请求级日志与 `/__stats`、`/__expire`(HTTP 过期触发)控制端点;
Expand All @@ -60,8 +74,46 @@
- **F2** HTTP 后端规范型会话过期(HTTP 404)→ 透明重建
- **F3** stdio 后端进程崩溃 → 自动 respawn 重放
- **F4** 前端客户端会话句柄失效 → 重启实例后旧 Mcp-Session-Id 透明重建
- TUI:交互式界面需 PTY,不纳入脚本;其恢复逻辑由
`tests/integration/discovery-worker-session-expiry.test.ts` 覆盖
- TUI:交互式界面需 PTY,由 **`scripts/tui-e2e.mjs`**(`npm run verify:tui`)覆盖,
见下「TUI 场景回归规则」;组件级行为另有 `tests/integration/tui-*.test.ts`

---

## TUI 场景回归规则(必须遵守)

TUI 交互场景统一维护在 **`scripts/tui-e2e.mjs`**(`npm run verify:tui`),用 tmux 充当真实终端:
私有 socket 建会话、`send-keys` 驱动按键、`capture-pane` 读屏。**必须用真实终端驱动**——
自研 ANSI 仿真在帧高溢出等场景下的滚动语义与真实终端不一致,会产生假象。

### 规则

1. **改动 TUI 行为(按键、布局、渲染)时,必须同步在 `scripts/tui-e2e.mjs` 增加/更新 T* 场景**,
编号顺延;纯组件逻辑另加 `tests/integration/tui-*.test.ts`。
2. **隔离是硬性要求**:
- tmux 私有 socket(`-L onemcp-tui-e2e`),不触碰用户默认 tmux server
- 每场景 `mkdtemp` 独立配置目录,绝不读写 `~/.onemcp`;配置由 `onemcp --init` 生成后打补丁
(不要手写配置模板:schema 新增必填字段时会静默失效,应用会因校验失败直接退出)
- 不绑定端口,与运行中的 daemon 无关
3. **断言要锁行为而不是锁像素**:相对式判断(如「每个服务恰好占一行」「无 `-http://` 游离字符」
「确认前服务数不变」),失败时转储当前画面。退出码:0 通过 / 1 断言失败 / 2 环境不具备(未装 tmux,
需显式 `--allow-skip` 才会按通过处理)——「跳过」不允许伪装成「通过」。
4. **文档同步**:新增/调整场景后同步更新 README「本地部署与端到端验证」的 TUI 场景清单。

### 当前 TUI 场景清单(以 scripts/tui-e2e.mjs 为准)

- **T1** 列表一屏渲染(16 服务 @34 行 / @50 行):每服务恰好一行、无续行、无游离字符、不超帧高
- **T2** 窄终端降级(60 列):丢弃 tags 列、端点省略号截断、仍每服务一行
- **T3** 删除二次确认:`d` 弹确认 → `n` 取消(配置未变)→ `d`+`y` 才删除并落盘
- **T4** 重名覆盖确认:同名保存弹确认,`n` 取消且原 tags 完好(不静默覆盖)
- **T5** Ctrl+S 不污染:空 Command 连按 3 次 → 字段无 `s`、有错误提示、零落库
- **T6** Ctrl+C 退出:有服务配置时进程也能真正结束
- **T7** 参数粘贴并运行:整块粘贴(一次多字符输入)生效并运行成功
- **T8** 工具视图:工具清单(精确计数)+ 搜索框粘贴过滤
- **T9** 结果区操作:Ctrl+P 原始输出 / `v` 选行 + Ctrl+Y 复制选区 / `f` 全宽
- **T10** 结果分页:大输出下 PageDown/PageUp 改变可见行区间
- **T11** CJK:中文标签在列表里每服务一行;中文参数值运行后原样回显
- **T12** 配置路径与自身写盘提示:footer 显示真实 configDir;保存后提示是成功而非「外部变更」
- **T13** 结果存档:Ctrl+O 生成临时文件、给出路径并复制完整路径

---

Expand Down Expand Up @@ -270,6 +322,7 @@ src/
scripts/
├── deploy-local.mjs # npm run deploy:local
├── e2e-local.mjs # npm run verify:local(E2E 场景 case 维护在此)
├── tui-e2e.mjs # npm run verify:tui(TUI 场景 case 维护在此,tmux 驱动)
└── lib/install-local.mjs # 共享"编译→打包→安装"管道
```

Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,10 @@ npm run clean
npm run deploy:local [-- --port 5625 --log-level INFO]
```

重启步骤会自动识别 daemon 的托管方式:若端口由 **launchd 等外部守护**托管(这类 daemon 不写 pidfile),
脚本改用守护自身的重启命令(macOS 上为 `launchctl kickstart -k gui/<uid>/<label>`)并等待 initialize 就绪;
否则按 `~/.onemcp/server.pid` 停旧实例、再以 `-d` 启动新实例。两种方式最后都会做一次 initialize 冒烟。

针对**已安装产物**的端到端回归。脚本自身完成"编译 → npm pack → 全局真实安装(tarball + 安装形态校验)",然后以随机端口 + 独立临时配置的独立实例(不影响正在运行的 daemon,注册 HTTP×2 + SSE + stdio×2 共 5 个 mock 后端)覆盖正常与故障恢复两类共 10 个场景,每条断言独立报告、退出码可供 CI 使用:

```bash
Expand All @@ -641,7 +645,37 @@ npm run verify:local
- **F2 后端规范型会话过期(HTTP 404)**:同上,验证 MCP 规范的过期信号
- **F3 stdio 后端进程崩溃**:进程运行中退出,调用路径自动 respawn 并重放请求
- **F4 前端会话句柄失效**:重启 onemcp 实例后客户端携带旧 `Mcp-Session-Id` 重放,会话句柄透明重建
- TUI:交互式界面(需 PTY)不在本脚本内;其恢复逻辑由 `tests/integration/discovery-worker-session-expiry.test.ts` 覆盖

### TUI 端到端验证(需 tmux)

TUI 交互界面需要真实 PTY,因此单独用 tmux 驱动:私有 socket 建会话、`send-keys` 按键、`capture-pane`
读屏(tmux 即终端模拟器,读到的画面是 ground truth)。每场景独立临时配置目录、不绑定端口,
**不触碰 `~/.onemcp` 与运行中的 daemon**:

```bash
npm run build && npm run verify:tui
```

CI 的 `tui-e2e` 任务也在 CI 上运行同一套场景。注意脚本会为 tmux 会话**剥掉
`CI` / `CONTINUOUS_INTEGRATION` / `CI_*`** 环境变量:Ink 通过 `is-in-ci` 检测到它们时
只在**退出时**绘制最后一帧(CI 日志友好),否则驱动全程只能看到空屏。未安装 tmux 的环境下
脚本返回退出码 2(不是 0),避免「跳过」被当成「通过」。

- **T1 列表一屏渲染**:16 服务 @34 行 —— 每服务恰好一行、无续行、无游离字符、不超过帧高(@50 行渲染一致)
- **T2 窄终端降级**:60 列 —— 丢弃 tags 列、端点省略号截断、仍每服务一行
- **T3 删除二次确认**:`d` 弹确认 → `n` 取消(配置未变)→ `d`+`y` 才删除
- **T4 重名覆盖确认**:同名保存弹确认,`n` 取消且原 tags 完好(不静默覆盖)
- **T5 Ctrl+S 不污染**:空 Command 连按 3 次 —— 字段无 `s`、有错误提示、零落库
- **T6 Ctrl+C 退出**:有服务配置时进程也能真正结束
- **T7 参数粘贴并运行**:整块粘贴(一次多字符输入)生效并运行成功
- **T8 工具视图**:工具清单(精确计数)+ 搜索框粘贴过滤
- **T9 结果区操作**:Ctrl+P 原始输出、`v` 选行 + Ctrl+Y 复制选区、`f` 全宽
- **T10 结果分页**:大输出下 PageDown/PageUp 改变可见行区间
- **T11 CJK**:中文标签在列表里每服务一行;中文参数值运行后原样回显
- **T12 配置路径与自身写盘提示**:footer 显示真实配置目录;保存后提示是成功而非「外部变更」
- **T13 结果存档**:Ctrl+O 生成临时文件、给出路径并复制完整路径

组件级 TUI 行为(渲染细节、按键分发)另有 `tests/integration/tui-*.test.ts`(伪终端 harness)覆盖。

### 运行测试

Expand Down
7 changes: 7 additions & 0 deletions docs/TUI_JSON_MODE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# TUI JSON Mode

> **状态(2026-09-12)**:本文档描述的「独立 JSON 配置视图」(`ServiceJsonEditor`,批量导入服务)
> **已不再挂载** —— TUI 的视图只有 list / add / edit / tools / help,没有任何入口渲染该组件,
> 因此该组件与其只被自身引用的测试已删除。当前可用的 JSON 编辑是**工具详情页的 Ctrl+J**
> (表单参数 ↔ 原始 JSON 互转,见 `ServiceTools` + `JsonTextArea`)。
> 下文保留为历史设计记录。


## Overview

The TUI JSON mode provides an alternative way to configure services using JSON instead of the step-by-step form interface. This mode is designed for advanced users who prefer to work with JSON directly and supports bulk import of multiple services.
Expand Down
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
"test:watch": "vitest",
"test:coverage": "vitest --run --coverage",
"test:property": "vitest --run property",
"lint": "eslint src tests --ext .ts",
"lint:fix": "eslint src tests --ext .ts --fix",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
"lint": "eslint src tests --ext .ts,.tsx",
"lint:fix": "eslint src tests --ext .ts,.tsx --fix",
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"tests/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\" \"src/**/*.tsx\" \"tests/**/*.ts\"",
"typecheck": "tsc --noEmit",
"deploy:local": "npm run build && node scripts/deploy-local.mjs",
"verify:local": "node scripts/e2e-local.mjs",
"verify:tui": "node scripts/tui-e2e.mjs",
"clean": "node -e \"require('fs').rmSync('dist', {recursive:true, force:true})\"",
"prepublishOnly": "npm run clean && npm run build",
"prepare": "husky"
Expand Down Expand Up @@ -61,7 +62,9 @@
"ink-text-input": "^6.0.0",
"pino": "^8.17.2",
"pino-pretty": "^10.3.1",
"react": "^18.2.0"
"react": "^18.2.0",
"string-width": "^7.2.0",
"wrap-ansi": "^9.0.2"
},
"devDependencies": {
"@types/eventsource": "^1.1.15",
Expand Down
Loading