diff --git a/cli/.gitignore b/cli/.gitignore
new file mode 100644
index 0000000..995f861
--- /dev/null
+++ b/cli/.gitignore
@@ -0,0 +1,3 @@
+dist/
+build/
+__pycache__/
diff --git a/cli/README.md b/cli/README.md
index c31819e..356638a 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -1,57 +1,68 @@
# TJWater CLI
-独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器**直接调用并通过 stdout/stderr 参与管道**。
+独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器使用**编译后的可执行文件**直接调用,并通过 stdout/stderr 参与管道。
-## 直接使用
+## 构建可执行产物
```bash
cd TJWaterServerBinary/cli
-./tjwater help --json
+python -m pip install -r requirements.txt
+python -m pip install -r requirements-build.txt
+chmod +x build.sh
+./build.sh
```
-这个入口文件可以直接参与管道:
+构建完成后,直接使用编译产物:
```bash
-./tjwater help --json | jq
+./dist/tjwater-cli/tjwater-cli help
```
-它会优先使用:
-1. `cli/.venv/bin/python`
-2. 环境变量 `PYTHON`
-3. 当前环境里的 `python`
-4. 最后回退到 `python3`
-
-如果需要,也可以显式走 Python:
+这个可执行文件可以直接参与管道:
```bash
-python -m tjwater_agent_cli help --json
+./dist/tjwater-cli/tjwater-cli help | jq
+```
+
+当前采用 `PyInstaller onedir` 方式输出到 `dist/tjwater-cli/`,避免 onefile 在部分 agent/server 环境下依赖临时目录解包执行的问题。
+
+如果需要在开发时直接走源码入口,也可以显式使用 Python:
+
+```bash
+python -m tjwater_cli help
```
## 部署到 agent 服务器
-最简单的方式是把整个 `TJWaterServerBinary/cli/` 文件夹同步到 agent 服务器,然后直接执行:
+最简单的方式是把 `dist/tjwater-cli/` 整个目录同步到 agent 服务器,然后直接执行:
+
+```bash
+./tjwater-cli/tjwater-cli help
+```
+
+如果希望打包传输:
```bash
cd TJWaterServerBinary/cli
-./tjwater help --json
+tar -C dist -czf tjwater-cli-linux-amd64.tar.gz tjwater-cli
```
如果希望放到 PATH 中:
```bash
-chmod +x tjwater
-ln -s /path/to/TJWaterServerBinary/cli/tjwater /usr/local/bin/tjwater
-tjwater help --json
+ln -s /path/to/TJWaterServerBinary/cli/dist/tjwater-cli/tjwater-cli /usr/local/bin/tjwater-cli
+tjwater-cli help | jq
```
-## Python 依赖
+## 运行与构建依赖
```bash
cd TJWaterServerBinary/cli
python -m pip install -r requirements.txt
+python -m pip install -r requirements-build.txt
```
-只保留运行 CLI 必需依赖,不再包含安装包构建相关内容。
+`requirements.txt` 仅包含运行 CLI 的依赖;`requirements-build.txt` 仅包含生成可执行文件所需的构建依赖。
## 认证上下文
diff --git a/cli/build.sh b/cli/build.sh
new file mode 100755
index 0000000..f6574a6
--- /dev/null
+++ b/cli/build.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+if [ -n "${PYTHON:-}" ]; then
+ PYTHON_BIN="$PYTHON"
+elif command -v python >/dev/null 2>&1; then
+ PYTHON_BIN="python"
+else
+ PYTHON_BIN="python3"
+fi
+
+cd "$ROOT"
+
+"$PYTHON_BIN" -m PyInstaller --noconfirm --clean tjwater.spec
+
+BIN_PATH="$ROOT/dist/"
+if [ ! -x "$BIN_PATH" ]; then
+ echo "build succeeded but executable was not created: $BIN_PATH" >&2
+ exit 1
+fi
+
+"$BIN_PATH" help >/dev/null
+
+echo "built executable: $BIN_PATH"
diff --git a/cli/entrypoint.py b/cli/entrypoint.py
new file mode 100644
index 0000000..46d2a02
--- /dev/null
+++ b/cli/entrypoint.py
@@ -0,0 +1,5 @@
+from tjwater_cli.main import console_entry
+
+
+if __name__ == "__main__":
+ console_entry()
diff --git a/cli/requirements-build.txt b/cli/requirements-build.txt
new file mode 100644
index 0000000..31f7e39
--- /dev/null
+++ b/cli/requirements-build.txt
@@ -0,0 +1 @@
+pyinstaller>=6.11,<7
diff --git a/cli/tests/unit/test_tjwater_cli.py b/cli/tests/unit/test_tjwater_cli.py
index 7709a71..5748cc7 100644
--- a/cli/tests/unit/test_tjwater_cli.py
+++ b/cli/tests/unit/test_tjwater_cli.py
@@ -1,3 +1,4 @@
+import json
from pathlib import Path
from typer.testing import CliRunner
@@ -64,61 +65,60 @@ def test_build_runtime_context_uses_default_server(monkeypatch):
assert runtime.server == core.DEFAULT_SERVER
-def test_help_json_lists_commands():
+def test_help_outputs_json_lists_commands():
+ result = runner.invoke(app, ["help"])
+ payload = json.loads(result.stdout)
+
+ assert result.exit_code == 0
+ assert payload["schema_version"] == "tjwater-cli/v1"
+ assert any(command["command"] == "project" for command in payload["commands"])
+ assert any(command["command"] == "analysis" for command in payload["commands"])
+ assert payload["menu_level"] == 1
+ assert all(command["command"] != "project list" for command in payload["commands"])
+
+
+def test_help_option_json_is_removed():
result = runner.invoke(app, ["help", "--json"])
- assert result.exit_code == 0
- assert '"schema_version": "tjwater-cli/v1"' in result.stdout
- assert '"command": "project"' in result.stdout
- assert '"command": "analysis"' in result.stdout
- assert '"menu_level": 1' in result.stdout
- assert '"command": "project list"' not in result.stdout
-
-
-def test_help_defaults_to_text():
- result = runner.invoke(app, ["help"])
-
- assert result.exit_code == 0
- assert "Commands:" in result.stdout
- assert "project: 项目与项目级元数据相关命令。" in result.stdout
- assert "analysis: 分析计算与诊断相关命令。" in result.stdout
- assert "Use `tjwater