From 903a02962286dbf5c20b678d72978a4222bfeb5c Mon Sep 17 00:00:00 2001 From: haonan Date: Thu, 12 Nov 2026 09:17:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=9A=E8=BF=87=20GitHub=20Release?= =?UTF-8?q?=20=E5=88=86=E5=8F=91=20CLI=20=E4=B8=8E=20MCP=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Release 完成后自动构建 Linux/macOS/Windows 的 shield + shellsmith-mcp 包 - 复用发布中的 android-runtime 资源,附带 apktool、apksigner 与校验和 - shellsmith-mcp 默认查找同目录 shield,解压即可接入 MCP 客户端 - 文档补充 GitHub Release 安装与客户端配置 --- .github/workflows/cli-release.yml | 252 ++++++++++++++++++++++++++++++ apps/shellsmith-mcp/src/main.rs | 33 ++-- docs/mcp.md | 25 ++- 3 files changed, 300 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/cli-release.yml diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml new file mode 100644 index 0000000..c7f7eb4 --- /dev/null +++ b/.github/workflows/cli-release.yml @@ -0,0 +1,252 @@ +name: CLI and MCP Release Bundles + +on: + workflow_run: + workflows: ["Release"] + types: [completed] + workflow_dispatch: + inputs: + tag: + description: "Release tag, for example v1.10.2" + required: true + type: string + +permissions: + contents: write + +concurrency: + group: cli-release-${{ github.event.workflow_run.head_sha || inputs.tag }} + cancel-in-progress: false + +jobs: + bundle: + name: CLI + MCP ${{ matrix.platform }} + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + strategy: + fail-fast: false + matrix: + include: + - platform: linux-x86_64 + os: ubuntu-22.04 + target: x86_64-unknown-linux-musl + - platform: macos-universal + os: macos-15 + target: universal + - platform: windows-x86_64 + os: windows-2022 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + env: + GH_TOKEN: ${{ github.token }} + + steps: + - name: Checkout release commit + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || inputs.tag }} + fetch-depth: 0 + + - name: Resolve version + shell: bash + run: | + set -euo pipefail + PYTHON=python3 + command -v python3 >/dev/null 2>&1 || PYTHON=python + version="$("$PYTHON" -c 'import json; print(json.load(open("apps/shield-gui/package.json", encoding="utf-8"))["version"])')" + tag="v${version}" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + test "${{ inputs.tag }}" = "$tag" + fi + if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' > draft.txt; then + echo "release $tag not found" >&2 + exit 1 + fi + test "$(cat draft.txt)" = "false" + echo "TAG=$tag" >> "$GITHUB_ENV" + echo "VERSION=$version" >> "$GITHUB_ENV" + echo "PLATFORM=${{ matrix.platform }}" >> "$GITHUB_ENV" + + - name: Install Rust targets + shell: bash + run: | + set -euo pipefail + case "${{ matrix.platform }}" in + linux-x86_64) + rustup target add x86_64-unknown-linux-musl + ;; + macos-universal) + rustup target add aarch64-apple-darwin x86_64-apple-darwin + ;; + esac + + - name: Install musl tools + if: matrix.platform == 'linux-x86_64' + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + - name: Build shield and shellsmith-mcp + shell: bash + run: | + set -euo pipefail + mkdir -p bundle/bin + case "${{ matrix.platform }}" in + linux-x86_64) + cargo build --release -p shield-cli -p shellsmith-mcp \ + --target x86_64-unknown-linux-musl + cp target/x86_64-unknown-linux-musl/release/shield bundle/bin/shield + cp target/x86_64-unknown-linux-musl/release/shellsmith-mcp bundle/bin/shellsmith-mcp + ;; + macos-universal) + cargo build --release -p shield-cli -p shellsmith-mcp --target aarch64-apple-darwin + cargo build --release -p shield-cli -p shellsmith-mcp --target x86_64-apple-darwin + for binary in shield shellsmith-mcp; do + lipo -create \ + "target/aarch64-apple-darwin/release/$binary" \ + "target/x86_64-apple-darwin/release/$binary" \ + -output "bundle/bin/$binary" + done + ;; + windows-x86_64) + cargo build --release -p shield-cli -p shellsmith-mcp \ + --target x86_64-pc-windows-msvc + cp target/x86_64-pc-windows-msvc/release/shield.exe bundle/bin/shield.exe + cp target/x86_64-pc-windows-msvc/release/shellsmith-mcp.exe bundle/bin/shellsmith-mcp.exe + ;; + *) + echo "unknown platform: ${{ matrix.platform }}" >&2 + exit 1 + ;; + esac + + - name: Download Android runtime asset + shell: bash + run: | + set -euo pipefail + mkdir -p runtime + gh release download "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern "*android-runtime.zip" \ + --dir runtime + archive="$(find runtime -maxdepth 1 -type f -name '*android-runtime.zip' -print -quit)" + test -n "$archive" + echo "RUNTIME_ARCHIVE=$archive" >> "$GITHUB_ENV" + + - name: Extract Android runtime asset + shell: bash + run: | + set -euo pipefail + PYTHON=python3 + command -v python3 >/dev/null 2>&1 || PYTHON=python + "$PYTHON" - "$RUNTIME_ARCHIVE" <<'PY' + import sys, zipfile + with zipfile.ZipFile(sys.argv[1]) as archive: + archive.extractall("runtime/extracted") + PY + + - name: Assemble CLI + MCP bundle + shell: bash + run: | + set -euo pipefail + mkdir -p bundle/lib bundle/resources bundle/licenses + runtime_root="$(dirname "$(find runtime/extracted -type f -name resources.zip -print -quit)")" + test -n "$runtime_root" + cp "$runtime_root"/resources*.zip bundle/resources/ + cp "$runtime_root"/mocika-play-delivery-api.jar bundle/resources/ + cp "$runtime_root"/xop-pvm2-packer.jar bundle/lib/ + cp "$runtime_root"/licenses/* bundle/licenses/ 2>/dev/null || true + cp tools/apktool_3.0.3.jar bundle/lib/apktool.jar + cp tools/apksigner.jar bundle/lib/apksigner.jar + cp tools/licenses/XopProtector-LICENSE.txt bundle/licenses/ + cp tools/licenses/XopProtector-NOTICE.txt bundle/licenses/ + + cat > bundle/README.md </dev/null 2>&1 || PYTHON=python + "$PYTHON" - "$archive" > "$archive.sha256" <<'PY' + import hashlib, pathlib, sys + path = pathlib.Path(sys.argv[1]) + print(f"{hashlib.sha256(path.read_bytes()).hexdigest()} {path.name}") + PY + mv "$archive" "$archive.sha256" dist/ + echo "RELEASE_ARCHIVE=$archive" >> "$GITHUB_ENV" + echo "RELEASE_CHECKSUM=$archive.sha256" >> "$GITHUB_ENV" + + - name: Upload release assets + shell: bash + run: | + set -euo pipefail + gh release upload "$TAG" \ + "dist/$RELEASE_ARCHIVE" \ + "dist/$RELEASE_CHECKSUM" \ + --repo "$GITHUB_REPOSITORY" \ + --clobber diff --git a/apps/shellsmith-mcp/src/main.rs b/apps/shellsmith-mcp/src/main.rs index c97c041..0d07f5c 100644 --- a/apps/shellsmith-mcp/src/main.rs +++ b/apps/shellsmith-mcp/src/main.rs @@ -14,14 +14,9 @@ use std::sync::Arc; #[derive(Debug, Parser)] #[command(name = "shellsmith-mcp", version, about)] struct Args { - /// shellsmith/shield CLI 可执行文件。 - #[arg( - long, - env = "SHELLSMITH_MCP_CLI", - value_name = "PATH", - default_value = "shield" - )] - cli: PathBuf, + /// shellsmith/shield CLI 可执行文件;默认先找同目录的 shield,再找 PATH。 + #[arg(long, env = "SHELLSMITH_MCP_CLI", value_name = "PATH")] + cli: Option, /// 允许工具读写的工作目录;可重复。未配置时不限制路径。 #[arg(long = "allow-root", value_name = "DIR")] @@ -45,6 +40,26 @@ fn run() -> Result<()> { .into_iter() .map(|root| root.canonicalize().unwrap_or(root)) .collect(); - let server = Arc::new(jobs::Server::new(args.cli, roots)); + let server = Arc::new(jobs::Server::new( + args.cli.unwrap_or_else(default_cli), + roots, + )); protocol::serve(server) } + +fn default_cli() -> PathBuf { + let name = if cfg!(windows) { + "shield.exe" + } else { + "shield" + }; + if let Ok(executable) = std::env::current_exe() { + if let Some(directory) = executable.parent() { + let sibling = directory.join(name); + if sibling.is_file() { + return sibling; + } + } + } + PathBuf::from(name) +} diff --git a/docs/mcp.md b/docs/mcp.md index a755a9c..9d662cf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -8,6 +8,29 @@ - 长任务:立即返回 `job_id`,通过 `shellsmith_job_status` / `shellsmith_job_result` / `shellsmith_job_cancel` 管理 - 破坏性操作:必须显式传 `confirm: true` +## 从 GitHub Releases 安装 + +发布页的 `Shellsmith-cli--` 包同时包含 `shield` 和 `shellsmith-mcp`。Release 发布后由 `CLI and MCP Release Bundles` 工作流自动构建并上传。 + +1. 下载对应平台的 `.tar.gz`(Linux/macOS)或 `.zip`(Windows)并解压; +2. 直接运行 `bin/shellsmith-mcp --allow-root /path/to/projects`; +3. 不需要额外指定 `--cli`:MCP 会优先查找同目录的 `shield`,找不到时再查 `PATH`。 + +MCP 客户端配置示例: + +```json +{ + "mcpServers": { + "shellsmith": { + "command": "/absolute/path/to/bin/shellsmith-mcp", + "args": ["--allow-root", "/absolute/path/to/projects"] + } + } +} +``` + +发布包中的 `README.md` 也包含同样的配置。密码仍然只从启动环境读取,不要把明文密码写进客户端 JSON。 + ## 构建与启动 ```bash @@ -21,7 +44,7 @@ cargo build --release -p shellsmith-mcp | 参数 | 环境变量 | 说明 | |---|---|---| -| `--cli PATH` | `SHELLSMITH_MCP_CLI` | `shield` 可执行文件;默认从 `PATH` 查找 `shield` | +| `--cli PATH` | `SHELLSMITH_MCP_CLI` | `shield` 可执行文件;默认先找同目录,再查 `PATH` | | `--allow-root DIR` | `SHELLSMITH_MCP_ALLOWED_ROOTS` | 允许读写的目录,可重复;未配置时不限制路径。环境变量使用平台路径分隔符 | 服务启动后从 stdin 读取 MCP 消息,向 stdout 写响应;诊断信息只写 stderr。