mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-13 04:05:43 +08:00
[feat] synchronize client and agent component versions
This commit is contained in:
1 parent
2a9faf1eb1
commit
b1236534ac
28 files changed
+548
-37
No files matched your search
@@ -79,6 +79,10 @@ jobs:
|
||||
# also exists when source_tag checks out an older Android release.
|
||||
cache-dependency-path: .github/workflows/build-android.yml
|
||||
|
||||
- name: Check shared client version
|
||||
if: hashFiles('scripts/version.py') != ''
|
||||
run: python3 scripts/version.py check client
|
||||
|
||||
- name: Install native build tools
|
||||
run: |
|
||||
crossdesk_missing_packages=()
|
||||
|
||||
@@ -49,6 +49,11 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Check shared CLI and MCP version
|
||||
run: python scripts/version.py check cli
|
||||
# A runner label can receive compiler/SDK updates without changing its name.
|
||||
- name: Set cache environment
|
||||
id: runner-image
|
||||
|
||||
@@ -163,6 +163,10 @@ jobs:
|
||||
cache: pip
|
||||
cache-dependency-path: .github/workflows/build-ios.yml
|
||||
|
||||
- name: Check shared client version
|
||||
if: hashFiles('scripts/version.py') != ''
|
||||
run: python3 scripts/version.py check client
|
||||
|
||||
- name: Install Python build tools
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -14,6 +14,8 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check shared CLI and MCP version
|
||||
run: python3 scripts/version.py check mcp
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
desktop_version:
|
||||
description: "Desktop version for branch builds, e.g. 1.7.2. Leave blank for 0.0.0; tags use their own version."
|
||||
description: "Desktop version for branch builds, e.g. 1.7.2. Leave blank for the source version; tags use their own version."
|
||||
required: false
|
||||
type: string
|
||||
patch:
|
||||
@@ -49,6 +49,9 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
# Resolve the date once for every desktop build and the release job.
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check shared client version
|
||||
run: python3 scripts/version.py check client
|
||||
- name: Resolve desktop version
|
||||
id: version
|
||||
shell: bash
|
||||
@@ -57,7 +60,7 @@ jobs:
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
if [[ "${GITHUB_REF}" == refs/heads/* ]]; then
|
||||
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-0.0.0}"
|
||||
VERSION_REF="${CROSSDESK_DESKTOP_VERSION:-$(python3 scripts/version.py show desktop --field version)}"
|
||||
if [[ ! "${VERSION_REF}" =~ ^v?[0-9]+(\.[0-9]+){1,3}$ ]]; then
|
||||
echo "::error::desktop_version must be a numeric version such as 1.7.2"
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Check component versions
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- run: python3 scripts/version.py check
|
||||
@@ -31,7 +31,9 @@ jobs:
|
||||
id: release
|
||||
env:
|
||||
RELEASE_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
run: node apps/cli/scripts/release-npm.cjs plan
|
||||
run: |
|
||||
python3 scripts/version.py check cli
|
||||
node apps/cli/scripts/release-npm.cjs plan
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
|
||||
@@ -347,6 +347,7 @@ PC 端可控制和被控制,复用桌面界面与桌面墙;手机和平板
|
||||
- [Agent / MCP 用户指南](apps/mcp/README.md):下载安装、Codex / DSH 配置、连接授权、日常使用与常见问题。
|
||||
- [MCP 开发参考](apps/mcp/DEVELOPMENT.md):源码构建、宿主集成、工具接口、打包与验证范围。
|
||||
- [源码构建](docs/BUILD.md):桌面端依赖、编译参数与打包;移动端请使用上方各平台的构建指南。
|
||||
- [组件版本管理](docs/VERSIONING.md):通过统一脚本查看、设置、递增各组件版本,以及同步构建号和锁文件。
|
||||
- [GUI 架构](docs/gui-architecture.md):桌面界面、平台实现与工程结构。
|
||||
- [连接模式架构](docs/connection-architecture.md):ID 连接与 IP 直连的寻址、认证、协商、数据路径及生命周期。
|
||||
- [常见问题](docs/FAQ.md):连接失败、黑屏、系统权限、文件位置与构建问题。
|
||||
|
||||
@@ -347,6 +347,7 @@ Host your own signaling and TURN services, then enable them under **Settings →
|
||||
- [MCP developer reference](apps/mcp/DEVELOPMENT.md) (Chinese): source builds, host integration, tool interfaces, packaging, and validation scope.
|
||||
- [Terminal client CLI](apps/cli/README.md) (Chinese): independent builds, terminal usage, Pi extension, bridge protocol, and npm packaging.
|
||||
- [Build from source](docs/BUILD_EN.md): desktop dependencies, build options, and packaging. Use the platform guides above for mobile builds.
|
||||
- [Component versions](docs/VERSIONING.md) (Chinese): inspect, set, and increment component versions, with synchronized build numbers and lockfiles.
|
||||
- [GUI architecture](docs/gui-architecture.md) (Chinese): desktop UI, platform implementation, and project structure.
|
||||
- [Connection architecture](docs/connection-architecture.md) (Chinese): discovery, authentication, negotiation, data paths, and lifecycle for ID and direct IP modes.
|
||||
- [FAQ](docs/FAQ.md#english): connections, blank video, permissions, received files, and build issues.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CrossDesk Android
|
||||
|
||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。当前版本为 1.6.1,支持 Android 8.0(API 26)及以上的 arm64 手机和平板。
|
||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。支持 Android 8.0(API 26)及以上的 arm64 手机和平板。桌面、Android、iOS 和 HarmonyOS 共用客户端发布版本,各移动平台构建号独立;可在仓库根目录执行 `python3 scripts/version.py show client` 查看,使用 `set client <版本>` 同步修改,详见[组件版本管理](../../docs/VERSIONING.md)。
|
||||
|
||||
## 当前功能
|
||||
|
||||
@@ -85,11 +85,11 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
|
||||
|
||||
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建及签名成功,并将已签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
||||
|
||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先更新并提交 `gradle.properties` 中的 `crossdeskVersionName` 和递增的 `crossdeskVersionCode`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先在仓库根目录运行 `python3 scripts/version.py set client <版本>` 同步全部客户端版本及各移动端构建号,检查并提交改动,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||
|
||||
```sh
|
||||
git tag android-v1.6.1-20261010
|
||||
git push origin android-v1.6.1-20261010
|
||||
git tag android-v1.7.6-20261112
|
||||
git push origin android-v1.7.6-20261112
|
||||
```
|
||||
|
||||
该标签只触发安卓构建,使用固定发布密钥签名并验证 Release APK 后,再发布到独立的 GitHub Release 和现有下载服务器。签名失败或缺少签名 Secrets 时停止发布,不回退到未签名 APK。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
crossdeskVersionName=1.6.1
|
||||
crossdeskVersionCode=3
|
||||
crossdeskVersionName=1.7.6
|
||||
crossdeskVersionCode=4
|
||||
@@ -100,11 +100,11 @@ Pi 等宿主应直接启动原生程序以可靠控制进程和专用密码描
|
||||
| 推送 `cli-v0.2.0-rc.1` | 校验源码版本为 `0.2.0-rc.1`,构建并发布到 `next` |
|
||||
| 在对应 `cli-v*` 标签上手动运行并取消 `dry_run` | 执行真实发布;分支上不允许关闭预演 |
|
||||
|
||||
准备下一版时同步修改 `apps/cli/package.json` 的 `version` 与 `apps/cli/xmake.lua` 的 `set_version`,提交并推送代码后创建同版本标签。例如源码已更新为 `0.1.1` 时:
|
||||
准备下一版时,在仓库根目录运行 `python3 scripts/version.py set cli <版本>`,同步 CLI、MCP、DSH 插件及 MCP 的原生依赖版本,再执行 `python3 scripts/version.py check cli`。`set mcp <版本>` 的修改范围相同,详见[组件版本管理](../../../docs/VERSIONING.md)。先发布 CLI 的平台包和入口包,再发布同版本 MCP。提交并推送代码后创建同版本标签。例如源码已更新为 `0.2.1` 时:
|
||||
|
||||
```sh
|
||||
git tag cli-v0.1.1
|
||||
git push origin cli-v0.1.1
|
||||
git tag cli-v0.2.1
|
||||
git push origin cli-v0.2.1
|
||||
```
|
||||
|
||||
已经上传过 `0.1.0` 的包不能被新构建覆盖;下一次原生程序发布应使用新版本。手动预演使用 `npm pack --dry-run` 加上内容校验,不调用 `npm publish` 或交换 OIDC 凭据;可对分支运行,也允许检查已发布的版本,但不验证 npm 发布权限。标签运行始终校验版本一致性。GitHub 的手动运行入口需要该工作流先进入默认分支。
|
||||
|
||||
@@ -5,7 +5,7 @@ function setup_options_and_dependencies(enable_dependencies)
|
||||
option_end()
|
||||
|
||||
option("CROSSDESK_VERSION")
|
||||
set_default("0.0.0")
|
||||
set_default("1.7.6")
|
||||
set_showmenu(true)
|
||||
set_description("Set CROSSDESK_VERSION for build")
|
||||
option_end()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"app": {
|
||||
"bundleName": "com.chunkdash.crossdesk",
|
||||
"vendor": "CrossDesk",
|
||||
"versionCode": 10000,
|
||||
"versionName": "0.1.0",
|
||||
"versionCode": 10001,
|
||||
"versionName": "1.7.6",
|
||||
"icon": "$media:app_icon",
|
||||
"label": "$string:app_name"
|
||||
}
|
||||
|
||||
+7
-4
@@ -204,13 +204,16 @@ The main workflow collects this same ZIP for GitHub Releases and the download
|
||||
server. Standalone `Build iOS` runs only upload the build artifact.
|
||||
|
||||
For an independent release, use the
|
||||
[`Release Mobile`](../../.github/workflows/release-mobile.yml) workflow. Commit
|
||||
the intended `MARKETING_VERSION` in the iOS target, then tag that commit with
|
||||
[`Release Mobile`](../../.github/workflows/release-mobile.yml) workflow. Desktop,
|
||||
Android, iOS and HarmonyOS share the same client release version while mobile
|
||||
build numbers remain independent. From the repository root, run
|
||||
`python3 scripts/version.py set client <version>` to update all clients,
|
||||
then commit the changes and tag that commit with
|
||||
`ios-v<marketing-version>-YYYYMMDD`, for example:
|
||||
|
||||
```sh
|
||||
git tag ios-v0.0.1-20261002
|
||||
git push origin ios-v0.0.1-20261002
|
||||
git tag ios-v1.7.6-20261112
|
||||
git push origin ios-v1.7.6-20261112
|
||||
```
|
||||
|
||||
The tag starts only the iOS build and publishes its unsigned ZIP to a dedicated
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 版本与依赖
|
||||
|
||||
MCP 包 `@chunkdash/crossdesk-mcp` 0.1.0 绑定原生 CLI 0.2.0,内置 DSH 插件随 MCP 同包分发。源码版本和本地打包不代表版本已经发布到 npm;内含原生程序的平台安装包可独立分发。
|
||||
CLI、MCP 包 `@chunkdash/crossdesk-mcp` 和内置 DSH 插件共用版本,当前均为 0.2.0,MCP 原生依赖也固定为 0.2.0。使用[版本脚本](../../docs/VERSIONING.md)修改任一工具版本会同步整个分组。源码版本和本地打包不代表版本已经发布到 npm;内含原生程序的平台安装包可独立分发。
|
||||
|
||||
当前提供 31 个模型工具和 11 个 MCP Apps 面板专用接口。本机 MCP 接入使用 stdio,不提供 MCP Streamable HTTP 或通用远程 shell 工具。控制端原生 CLI、被控端和信令服务器需要支持对应自动化协议。
|
||||
|
||||
@@ -497,7 +497,7 @@ npm run pack:npm -- --native-package-dir "$native_package_dir/package" --out-dir
|
||||
rm -rf "$native_package_dir"
|
||||
```
|
||||
|
||||
平台包保留原生包的程序、源码、许可和构建信息,设置对应 OS / CPU / libc 限制,不再下载原生平台依赖;其他 JavaScript 依赖仍由包管理器安装。CLI 构建工作流为每个平台生成 `crossdesk-mcp-<平台>` 产物;平台安装包要求实际传入的原生包与 MCP 的依赖版本一致。通用包和平台包名称、版本相同,应放在不同输出目录,不要将平台包当成通用版本发布到 npm。
|
||||
平台包保留原生包的程序、源码、许可和构建信息,设置对应 OS / CPU / libc 限制,不再下载原生平台依赖;其他 JavaScript 依赖仍由包管理器安装。CLI 构建工作流为每个平台生成 `crossdesk-mcp-<平台>` 产物,CLI、MCP、DSH 和原生依赖版本不一致时直接停止构建或打包。通用包和平台包名称、版本相同,应放在不同输出目录,不要将平台包当成通用版本发布到 npm。
|
||||
|
||||
原生 `doctor` 必须同时声明自动化协议和 `automation_video_protocols: ["h264-annexb/1", "nv12/1"]`、`automation_video_decoders: ["auto", "software"]`,以及 `automation_video_transport: "loopback-websocket/1"`、`automation_terminal_protocol: "ansi/1"`。通用 npm 分发需要先发布对应 0.2.0 平台包,再发布 MCP;内含原生程序的平台包不受此顺序限制。构建通过不等于各宿主和设备上的联调完成。
|
||||
|
||||
|
||||
+6
-6
@@ -31,7 +31,7 @@ DSH 和 Codex 都使用下表中的**平台安装包**,其中已包含所需
|
||||
|
||||
1. 登录 GitHub,打开 [CrossDesk 构建页面](https://github.com/kunkundi/crossdesk/actions/workflows/build-cli.yml)。
|
||||
2. 选择一次成功的运行,在页面的 **Artifacts** 区域下载与你的本机对应的文件。
|
||||
3. 解压下载的 `.zip`,取出里面的 `.tgz` 安装包,例如 `chunkdash-crossdesk-mcp-0.1.0.tgz`。保留 `.tgz`,安装时直接使用它。
|
||||
3. 解压下载的 `.zip`,取出里面的 `.tgz` 安装包,例如 `chunkdash-crossdesk-mcp-0.2.0.tgz`。保留 `.tgz`,安装时直接使用它。
|
||||
|
||||
GitHub 要求登录后才能下载构建产物,操作说明见 [下载构建产物](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/download-workflow-artifacts)。
|
||||
|
||||
@@ -47,7 +47,7 @@ Mac 可在“关于本机”中查看芯片;Windows 可在“设置 → 系统
|
||||
|
||||
不同平台解压后的 `.tgz` 文件名相同,请以下载时的产物名称区分。如果某次运行没有表中对应的产物,选择另一次提供该产物的运行;也可以按 [源码构建说明](DEVELOPMENT.md#从源码运行) 自行构建。
|
||||
|
||||
下文以 `0.1.0` 为例。**所有 `/absolute/path/...` 都需要换成自己电脑上的完整路径**,版本号也应与实际下载的文件一致。路径中有空格时保留引号。
|
||||
下文以 `0.2.0` 为例。**所有 `/absolute/path/...` 都需要换成自己电脑上的完整路径**,版本号也应与实际下载的文件一致。路径中有空格时保留引号。
|
||||
|
||||
<a id="install-dsh"></a>
|
||||
|
||||
@@ -58,7 +58,7 @@ Mac 可在“关于本机”中查看芯片;Windows 可在“设置 → 系统
|
||||
在 Harness 的插件页面选择本地 `.tgz` 安装包,或在终端执行:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.1.0.tgz"
|
||||
dsh plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.2.0.tgz"
|
||||
```
|
||||
|
||||
这个插件包会自动提供 MCP 工具和侧栏画面,无需另外安装 Node.js、原生 CLI 或填写 MCP 启动配置。
|
||||
@@ -66,7 +66,7 @@ dsh plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.1.0.t
|
||||
如果 macOS 终端提示找不到 `dsh`,可使用应用自带的命令:
|
||||
|
||||
```sh
|
||||
"/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.1.0.tgz"
|
||||
"/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.2.0.tgz"
|
||||
```
|
||||
|
||||
上述命令对应默认安装位置和 `desktop` 配置。使用自定义安装位置或 profile 时替换为自己的值。
|
||||
@@ -93,7 +93,7 @@ macOS / Linux:
|
||||
mkdir -p "$HOME/crossdesk-mcp"
|
||||
cd "$HOME/crossdesk-mcp"
|
||||
npm init -y
|
||||
npm install "/absolute/path/chunkdash-crossdesk-mcp-0.1.0.tgz"
|
||||
npm install "/absolute/path/chunkdash-crossdesk-mcp-0.2.0.tgz"
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
@@ -102,7 +102,7 @@ Windows PowerShell:
|
||||
New-Item -ItemType Directory -Force "$env:USERPROFILE/crossdesk-mcp"
|
||||
Set-Location "$env:USERPROFILE/crossdesk-mcp"
|
||||
npm init -y
|
||||
npm install "C:/Downloads/chunkdash-crossdesk-mcp-0.1.0.tgz"
|
||||
npm install "C:/Downloads/chunkdash-crossdesk-mcp-0.2.0.tgz"
|
||||
```
|
||||
|
||||
安装完成后,在同一目录运行:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
2. 在 Harness 桌面版的插件页面安装该文件,或执行以下命令。将示例中的路径和版本号替换为实际文件:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.1.0.tgz"
|
||||
dsh plugin --profile desktop add "/absolute/path/chunkdash-crossdesk-mcp-0.2.0.tgz"
|
||||
```
|
||||
|
||||
3. 重启 Harness,确认 CrossDesk 插件已启用。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chunkdash/dsh-crossdesk-sidebar",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Open CrossDesk remote desktop panels in DeepSeek Harness Desktop's sidebar",
|
||||
"type": "module",
|
||||
"main": "dist/host.js",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@chunkdash/crossdesk-mcp",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@chunkdash/crossdesk-mcp",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/client": "2.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chunkdash/crossdesk-mcp",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Local CrossDesk MCP tools and remote desktop panels",
|
||||
|
||||
@@ -23,12 +23,17 @@ const { values } = parseArgs({ options: {
|
||||
const output = resolve(values["out-dir"] ?? join(repository, "build/npm-mcp"));
|
||||
const project = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
||||
const cli = JSON.parse(await readFile(join(root, "../cli/package.json"), "utf8"));
|
||||
const harness = JSON.parse(await readFile(join(root, "integrations/deepseek-harness/package.json"), "utf8"));
|
||||
const platforms = JSON.parse(
|
||||
await readFile(join(root, "../cli/npm/platforms.json"), "utf8"),
|
||||
);
|
||||
const native = await import(new URL("../dist/native.js", import.meta.url));
|
||||
if (native.NATIVE_VERSION !== cli.version)
|
||||
throw new Error("MCP and CLI native versions must match.");
|
||||
const nativeVersion = (await readFile(join(root, "src/native.ts"), "utf8"))
|
||||
.match(/export const NATIVE_VERSION = "([^"]+)";/)?.[1];
|
||||
if (project.version !== cli.version || project.version !== harness.version || project.version !== nativeVersion)
|
||||
throw new Error("CLI, MCP, DSH and NATIVE_VERSION must match. Run python3 scripts/version.py set mcp VERSION from the repository root.");
|
||||
if (!nativeVersion || native.NATIVE_VERSION !== nativeVersion)
|
||||
throw new Error("Built MCP native version differs from source. Run npm run build first.");
|
||||
for (const file of ["main.js", "viewer-browser.html", "viewer-apps.html", "harness.js", "harness-client.js"])
|
||||
await access(join(root, "dist", file));
|
||||
const staging = await mkdtemp(join(tmpdir(), "crossdesk-mcp-pack-"));
|
||||
@@ -72,7 +77,7 @@ try {
|
||||
"LICENSE",
|
||||
],
|
||||
optionalDependencies: Object.fromEntries(
|
||||
Object.values(platforms).map((p) => [p.packageName, cli.version]),
|
||||
Object.values(platforms).map((p) => [p.packageName, nativeVersion]),
|
||||
),
|
||||
publishConfig: { access: "public" },
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { parseArgs } from "node:util";
|
||||
import { VERSION } from "./version.js";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { Writable } from "node:stream";
|
||||
import {
|
||||
@@ -149,7 +150,7 @@ async function main() {
|
||||
},
|
||||
});
|
||||
if (values.version) {
|
||||
process.stdout.write("0.1.0\n");
|
||||
process.stdout.write(`${VERSION}\n`);
|
||||
return;
|
||||
}
|
||||
if (values.help || !positionals.length) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { VERSION } from "./version.js";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
@@ -80,7 +81,7 @@ export function createServer(
|
||||
terminal: TerminalPanel,
|
||||
): McpServer {
|
||||
const server = new McpServer(
|
||||
{ name: "crossdesk-mcp", version: "0.1.0" },
|
||||
{ name: "crossdesk-mcp", version: VERSION },
|
||||
{
|
||||
capabilities: { tools: {}, resources: {} },
|
||||
instructions:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export const VERSION: string = JSON.parse(
|
||||
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
).version;
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
[返回 README](../README.md) · [English](BUILD_EN.md)
|
||||
|
||||
各组件版本可通过 `python3 scripts/version.py show` 查看,使用 `set` 或 `bump` 修改;同步范围和发布衔接见[组件版本管理](VERSIONING.md)。
|
||||
|
||||
安装版的升级器随主程序构建。发布公钥、清单签名和验证方法见
|
||||
[桌面端应用内升级](UPDATER.md);便携版不包含升级器。
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# 组件版本管理
|
||||
|
||||
在仓库根目录使用 `python3 scripts/version.py` 查看、修改和检查版本,需要 Python 3.9 或更新版本,无需安装第三方依赖。版本分为三组:桌面、Android、iOS 和 HarmonyOS 共用客户端版本;CLI、MCP 和 DSH 插件共用工具版本;Pi 扩展独立编号。脚本只修改源码配置,不会创建 Git 标签、提交代码、构建或发布安装包。
|
||||
|
||||
## 查看和检查
|
||||
|
||||
```sh
|
||||
python3 scripts/version.py show
|
||||
python3 scripts/version.py show client
|
||||
python3 scripts/version.py show mcp
|
||||
python3 scripts/version.py show --json
|
||||
python3 scripts/version.py show desktop --field version
|
||||
python3 scripts/version.py check
|
||||
```
|
||||
|
||||
`check` 校验版本格式、移动端构建号、重复字段和分组版本一致性。发现不一致时返回非零退出码并指出组件;用 `set client <版本>` 统一客户端,用 `set cli <版本>` 或 `set mcp <版本>` 统一 CLI、MCP、DSH 及原生依赖。单独检查任一客户端都会检查全部客户端;`check cli` 和 `check mcp` 也都会检查整个工具分组。
|
||||
|
||||
`Check component versions` GitHub Action 在 push、PR 和手动运行时执行全部检查。各构建及 npm 发布工作流也会检查对应分组,不一致时停止。版本脚本不查询 npm 或应用商店;正式发布仍使用对应工作流的发布前检查。
|
||||
|
||||
## 可管理的组件
|
||||
|
||||
| 组件参数 | 版本来源及同步范围 |
|
||||
| --- | --- |
|
||||
| `client` | 统一管理所有客户端版本,分别保留 Android、iOS、HarmonyOS 的构建号;`mobile` 是同一分组的别名,也会同步桌面版本 |
|
||||
| `desktop` | 修改版本时同步全部客户端;版本写入 `apps/desktop/xmake/options.lua` 的 `CROSSDESK_VERSION` 默认值,供 Windows、macOS、Linux 共用 |
|
||||
| `android` | 修改版本时同步全部客户端;`--build` 和 `bump android build` 只指定 Gradle 的 `crossdeskVersionCode` |
|
||||
| `ios` | 修改版本时同步全部客户端;`--build` 和 `bump ios build` 只指定 `CURRENT_PROJECT_VERSION`,同步 Debug 和 Release |
|
||||
| `harmony` | 修改版本时同步全部客户端;`--build` 和 `bump harmony build` 只指定 `AppScope/app.json5` 的 `versionCode` |
|
||||
| `cli` | 同步 CLI、MCP、DSH 和 MCP 的 CLI 依赖;CLI 写入 `apps/cli/package.json` 与 `apps/cli/xmake.lua`,五个平台原生 npm 包及入口包在打包时继承此版本 |
|
||||
| `mcp` | 与 `cli` 修改范围相同;MCP 写入 `apps/mcp/package.json`、锁文件中的根版本与根包版本、DSH 集成目录的包版本,以及 `src/native.ts` 的 `NATIVE_VERSION` |
|
||||
| `pi` | `apps/cli/plugins/pi/package.json` 的扩展版本 |
|
||||
|
||||
npm 组件若有 `package-lock.json`,脚本会同步其中的根版本和 `packages[""].version`,不会改动依赖包版本。DSH 与 MCP 一起分发,不需要独立设置 DSH 发布版本。HarmonyOS 的模块描述、SDK `modelVersion` 和本地类型库版本不属于应用发布版本,保持各自的用途。
|
||||
|
||||
## 设置版本
|
||||
|
||||
以下均为命令示例,替换为准备发布的版本:
|
||||
|
||||
```sh
|
||||
python3 scripts/version.py set client 1.7.7 --android-build 5 --ios-build 7 --harmony-build 10002
|
||||
python3 scripts/version.py set cli 0.3.0
|
||||
python3 scripts/version.py set mcp 0.3.0
|
||||
python3 scripts/version.py set pi 0.1.1
|
||||
```
|
||||
|
||||
所有修改命令都支持 `--dry-run`,先显示将要修改的文件差异:
|
||||
|
||||
```sh
|
||||
python3 scripts/version.py set mcp 0.3.0 --dry-run
|
||||
```
|
||||
|
||||
版本参数填写 `X.Y.Z`,不包含标签前缀 `v`、日期或构建元数据。CLI、MCP、Pi 另支持 `X.Y.Z-rc.N` 等预发布版本。客户端共用版本各段限制在 `0–65535`,以适配 Windows 版本资源。
|
||||
|
||||
`set client` 可分别传入 `--android-build`、`--ios-build`、`--harmony-build`,不接受含义不明确的 `--build`。`set android 1.7.7 --build 5` 同样会将全部客户端发布版本设为 `1.7.7`,但 `--build 5` 只指定 Android 构建号,其余移动端按各自的构建号递增。`set ios`、`set harmony` 对称处理;`set desktop` 同样同步全部客户端,但不接受 `--build`。
|
||||
|
||||
各端发布版本变化而未显式指定构建号时,该端构建号自动加一;相同版本且未指定构建号时不改动文件。例如从所有客户端 `1.7.6`、Android / iOS / HarmonyOS 构建号分别为 `4` / `6` / `10001` 执行 `set client 1.7.7`,结果为所有客户端 `1.7.7`、构建号 `5` / `7` / `10002`。修复版本差异时,仅版本实际变化的端自动增加构建号。显式构建号不得降低,修改应用版本时构建号必须增加。脚本统一使用正整数构建号:Android 最大 `2100000000`,iOS 最大 `9999`,HarmonyOS 最大 `2147483647`。
|
||||
|
||||
修改前会先验证全部目标字段,验证失败不写文件;写入保留文件格式和权限,并使用逐文件原子替换。发生写入错误时会尝试还原已经写入的文件,修改后仍应检查 `git diff`。
|
||||
|
||||
## 递增版本
|
||||
|
||||
```sh
|
||||
python3 scripts/version.py bump cli patch
|
||||
python3 scripts/version.py bump mcp minor
|
||||
python3 scripts/version.py bump client patch
|
||||
python3 scripts/version.py bump client major --dry-run
|
||||
python3 scripts/version.py bump android build
|
||||
```
|
||||
|
||||
`major`、`minor`、`patch` 递增对应数字,并清零较低位;例如 `1.2.3` 递增 `minor` 得到 `1.3.0`。`bump client patch` 和对任一客户端执行 `bump ... patch` 都会同步提升全部客户端发布版本,并分别增加移动端构建号。`bump cli` 和 `bump mcp` 都会同步 CLI、MCP、DSH 和 MCP 的原生依赖。分组内原本不一致时,先使用 `set` 明确统一到哪个版本。
|
||||
|
||||
`bump android build`、`bump ios build`、`bump harmony build` 只增加指定端的构建号,其余端保持不变;不提供 `bump client build` 或 `bump mobile build`。当前是预发布版本时使用 `set` 明确指定下一个版本,例如 `set mcp 0.2.0-rc.2` 或 `set mcp 0.2.0`。
|
||||
|
||||
## CLI、MCP 与 DSH 的共同版本
|
||||
|
||||
CLI、MCP、DSH 集成元数据和 MCP 的原生依赖版本必须完全相同,当前均为 `0.2.0`。执行 `set cli 0.3.0` 或 `set mcp 0.3.0` 会将它们一起设为 `0.3.0`,只需执行其中一条命令。预发布后缀也必须一致。不提供单独设置原生依赖版本的参数。
|
||||
|
||||
MCP 通用 npm 包从 `NATIVE_VERSION` 生成五个平台包的精确依赖;MCP 的 `--version` 和协议握手读取包版本。DSH 随 MCP 同包分发,不另发插件版本。
|
||||
|
||||
先通过 CLI 发布工作流发布同版本的五个平台原生包和 CLI 入口,再发布 MCP。即使只修改 MCP,也要为两者准备同一个新版本并按此顺序发布。MCP 平台安装包由 CLI 构建工作流一并生成,通用包使用 `npm run pack:npm` 打包。
|
||||
|
||||
CLI 构建工作流为每个平台附带生成 `crossdesk-mcp-<平台>` 安装包。源码版本不一致时,构建、MCP 打包和发布检查会报错;平台安装包还要求传入的原生包版本一致。工具版本不影响客户端版本和 Pi 扩展版本。
|
||||
|
||||
## 与构建和发布配合
|
||||
|
||||
客户端版本同时控制本地 Xmake 默认值,以及 GitHub Actions 分支构建未填写 `desktop_version` 时的默认版本。已有 Xmake 配置可能保留缓存,应重新配置;显式 `--CROSSDESK_VERSION`、Actions 的 `desktop_version` 和发布标签继续按原流程覆盖默认值。正式发布时应使标签的基础版本与客户端源码版本一致。日期和热修订后缀仍由发布流程处理,本脚本不修改既有标签格式。各端可分开构建和发布,但共用同一个源码发布版本。
|
||||
|
||||
修改并检查后,审阅、提交和推送源码,再按实际版本创建对应标签:CLI 使用 `cli-v<版本>`。移动端沿用 `android-v<版本>-YYYYMMDD`、`ios-v<版本>-YYYYMMDD`。已发布版本不能用不同内容覆盖;版本修改脚本不会替你判断某个版本是否已在注册表中使用。
|
||||
|
||||
相关说明:[桌面构建](BUILD.md) · [CLI 发布](../apps/cli/docs/npm-distribution.md#ci-与发布) · [MCP 打包](../apps/mcp/DEVELOPMENT.md#打包)。
|
||||
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inspect and update CrossDesk component versions without building or publishing."""
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SEMVER = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?")
|
||||
COMPONENTS = ("desktop", "android", "ios", "harmony", "cli", "mcp", "pi")
|
||||
MOBILE = ("android", "ios", "harmony")
|
||||
CLIENTS = ("desktop", *MOBILE)
|
||||
CLIENT_GROUPS = ("client", "mobile")
|
||||
TOOLS = ("cli", "mcp")
|
||||
COMPONENT_CHOICES = (*CLIENT_GROUPS, *COMPONENTS)
|
||||
|
||||
|
||||
def validate_version(value, prerelease=False):
|
||||
match = SEMVER.fullmatch(value)
|
||||
if not match or (match[4] and not prerelease):
|
||||
raise ValueError(f"Invalid version: {value!r}; use X.Y.Z" + (" or X.Y.Z-rc.N" if prerelease else ""))
|
||||
if match[4] and any(part.isdigit() and len(part) > 1 and part[0] == "0" for part in match[4].split(".")):
|
||||
raise ValueError(f"Invalid prerelease version: {value!r}; numeric identifiers cannot have leading zeros")
|
||||
return match
|
||||
|
||||
|
||||
def json_span(text, keys, start=0):
|
||||
"""Find a JSON scalar by object path, preserving all unrelated formatting."""
|
||||
decoder = json.JSONDecoder()
|
||||
|
||||
def skip(index):
|
||||
while index < len(text) and text[index].isspace():
|
||||
index += 1
|
||||
return index
|
||||
|
||||
index = skip(start)
|
||||
if text[index:index + 1] != "{":
|
||||
raise ValueError(f"Expected a JSON object at {keys!r}")
|
||||
index = skip(index + 1)
|
||||
matches = []
|
||||
while text[index:index + 1] != "}":
|
||||
key, index = decoder.raw_decode(text, index)
|
||||
index = skip(index)
|
||||
if text[index:index + 1] != ":":
|
||||
raise ValueError("Expected ':' in JSON object")
|
||||
begin = skip(index + 1)
|
||||
value, end = decoder.raw_decode(text, begin)
|
||||
if key == keys[0]:
|
||||
matches.append(json_span(text, keys[1:], begin) if len(keys) > 1 else (begin, end, value))
|
||||
index = skip(end)
|
||||
if text[index:index + 1] == "}":
|
||||
break
|
||||
if text[index:index + 1] != ",":
|
||||
raise ValueError("Expected ',' in JSON object")
|
||||
index = skip(index + 1)
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"Expected one JSON field at {'.'.join(keys)!r}, found {len(matches)}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
class Field:
|
||||
def __init__(self, file, pattern=None, keys=None, multiple=False):
|
||||
self.file, self.pattern, self.keys, self.multiple = file, pattern, keys, multiple
|
||||
|
||||
def spans(self, text):
|
||||
if self.keys is not None:
|
||||
json.loads(text)
|
||||
return [json_span(text, self.keys)]
|
||||
matches = list(re.finditer(self.pattern, text, re.MULTILINE))
|
||||
if not matches or (not self.multiple and len(matches) != 1):
|
||||
raise ValueError(f"{self.file}: expected {'at least ' if self.multiple else ''}one version field, found {len(matches)}")
|
||||
return [(m.start('value'), m.end('value'), m['value']) for m in matches]
|
||||
|
||||
def values(self, snapshot):
|
||||
values = [value for _, _, value in self.spans(snapshot.read(self.file))]
|
||||
if any(not isinstance(value, str) for value in values):
|
||||
raise ValueError(f"{self.file}: expected a string version")
|
||||
return values
|
||||
|
||||
def update(self, snapshot, value):
|
||||
text = snapshot.read(self.file)
|
||||
for start, end, _ in reversed(self.spans(text)):
|
||||
text = text[:start] + (json.dumps(value) if self.keys is not None else value) + text[end:]
|
||||
snapshot.set(self.file, text)
|
||||
|
||||
|
||||
def package_fields(snapshot, directory):
|
||||
fields = [Field(f"{directory}/package.json", keys=("version",))]
|
||||
if (snapshot.root / directory / "package-lock.json").exists():
|
||||
fields.extend([Field(f"{directory}/package-lock.json", keys=("version",)),
|
||||
Field(f"{directory}/package-lock.json", keys=("packages", "", "version"))])
|
||||
return fields
|
||||
|
||||
|
||||
def fields(snapshot, component):
|
||||
if component in ("cli", "mcp", "pi"):
|
||||
directory = "apps/cli/plugins/pi" if component == "pi" else f"apps/{component}"
|
||||
result = {"version": package_fields(snapshot, directory)}
|
||||
if component == "cli":
|
||||
result["version"].append(Field("apps/cli/xmake.lua", r'set_version\("(?P<value>[^"\n]+)"\)'))
|
||||
elif component == "mcp":
|
||||
result["version"].append(Field("apps/mcp/integrations/deepseek-harness/package.json", keys=("version",)))
|
||||
result["native_version"] = [Field("apps/mcp/src/native.ts", r'^export const NATIVE_VERSION = "(?P<value>[^"\n]+)";')]
|
||||
return result
|
||||
if component == "desktop":
|
||||
return {"version": [Field("apps/desktop/xmake/options.lua", r'option\("CROSSDESK_VERSION"\)\s*set_default\("(?P<value>[^"\n]+)"\)')]}
|
||||
if component == "android":
|
||||
return {key: [Field("apps/android/gradle.properties", rf'^{prop}=(?P<value>[^\r\n]+)\r?$')]
|
||||
for key, prop in (("version", "crossdeskVersionName"), ("build", "crossdeskVersionCode"))}
|
||||
if component == "ios":
|
||||
return {key: [Field("apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj",
|
||||
rf'^\s*{prop} = (?P<value>[^;\r\n]+);', multiple=True)]
|
||||
for key, prop in (("version", "MARKETING_VERSION"), ("build", "CURRENT_PROJECT_VERSION"))}
|
||||
if component == "harmony":
|
||||
return {"version": [Field("apps/harmony/AppScope/app.json5", r'"versionName"\s*:\s*"(?P<value>[^"\n]+)"')],
|
||||
"build": [Field("apps/harmony/AppScope/app.json5", r'"versionCode"\s*:\s*(?P<value>[0-9]+)(?=\s*[,}])')]}
|
||||
raise ValueError(f"Unknown component: {component}")
|
||||
|
||||
|
||||
def validate_build(component, value):
|
||||
# Keep an integer build number for all three mobile applications.
|
||||
maximum = {"android": 2100000000, "ios": 9999, "harmony": 2147483647}[component]
|
||||
if not re.fullmatch(r"[1-9][0-9]*", value) or int(value) > maximum:
|
||||
raise ValueError(f"{component}: build must be an integer from 1 to {maximum}")
|
||||
|
||||
|
||||
class Snapshot:
|
||||
def __init__(self, root=ROOT):
|
||||
self.root = Path(root)
|
||||
self.original = {}
|
||||
self.current = {}
|
||||
|
||||
def read(self, file):
|
||||
if file not in self.original:
|
||||
text = (self.root / file).read_bytes().decode("utf-8")
|
||||
self.original[file] = self.current[file] = text
|
||||
return self.current[file]
|
||||
|
||||
def set(self, file, value):
|
||||
self.read(file)
|
||||
self.current[file] = value
|
||||
|
||||
def state(self, component, check=True):
|
||||
if component in CLIENT_GROUPS:
|
||||
clients = {platform: self.state(platform, check=check) for platform in CLIENTS}
|
||||
if len({data["version"] for data in clients.values()}) != 1:
|
||||
versions = ", ".join(f"{platform}={data['version']}" for platform, data in clients.items())
|
||||
raise ValueError(f"Client versions differ: {versions}; use set client X.Y.Z to synchronize them")
|
||||
return {"version": clients["desktop"]["version"],
|
||||
**{f"{platform}_build": clients[platform]["build"] for platform in MOBILE}}
|
||||
result = {}
|
||||
for key, group in fields(self, component).items():
|
||||
values = [value for field in group for value in field.values(self)]
|
||||
if check and len(set(values)) != 1:
|
||||
raise ValueError(f"{component}.{key} is inconsistent: {', '.join(values)}; use set to synchronize it")
|
||||
value = values[0]
|
||||
if key == "build":
|
||||
validate_build(component, value)
|
||||
else:
|
||||
validate_version(value, component in ("cli", "mcp", "pi"))
|
||||
if component == "desktop" and any(int(part) > 65535 for part in value.split(".")):
|
||||
raise ValueError("Desktop version components must fit Windows version resources (0–65535)")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def changes(self):
|
||||
return {file: text for file, text in self.current.items() if text != self.original[file]}
|
||||
|
||||
def save(self, dry_run=False):
|
||||
changes = self.changes()
|
||||
for file, text in changes.items():
|
||||
if dry_run:
|
||||
print("".join(difflib.unified_diff(self.original[file].splitlines(keepends=True),
|
||||
text.splitlines(keepends=True), fromfile=file, tofile=file)), end="")
|
||||
if dry_run or not changes:
|
||||
return
|
||||
for file in changes:
|
||||
if (self.root / file).read_bytes().decode("utf-8") != self.original[file]:
|
||||
raise ValueError(f"{file} changed while preparing the update; retry")
|
||||
written = []
|
||||
try:
|
||||
for file, text in changes.items():
|
||||
atomic_write(self.root / file, text)
|
||||
written.append(file)
|
||||
except OSError:
|
||||
for file in reversed(written):
|
||||
if (self.root / file).read_bytes().decode("utf-8") == changes[file]:
|
||||
atomic_write(self.root / file, self.original[file])
|
||||
raise
|
||||
for file in changes:
|
||||
print(f"Updated {file}")
|
||||
|
||||
|
||||
def atomic_write(file, text):
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(dir=file.parent, prefix=f".{file.name}.", delete=False) as stream:
|
||||
temporary = Path(stream.name)
|
||||
stream.write(text.encode("utf-8"))
|
||||
os.chmod(temporary, stat.S_IMODE(file.stat().st_mode))
|
||||
os.replace(temporary, file)
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def check_tools(snapshot):
|
||||
cli, mcp = (snapshot.state(component) for component in TOOLS)
|
||||
if not (cli["version"] == mcp["version"] == mcp["native_version"]):
|
||||
raise ValueError(f"CLI/MCP versions differ: cli={cli['version']}, mcp={mcp['version']}, "
|
||||
f"mcp.native_version={mcp['native_version']}; use set cli X.Y.Z or set mcp X.Y.Z to synchronize them")
|
||||
|
||||
|
||||
def update_component(snapshot, component, version, build=None):
|
||||
previous = snapshot.state(component, check=False)
|
||||
validate_version(version, component in ("cli", "mcp", "pi"))
|
||||
if build is not None and component not in MOBILE:
|
||||
raise ValueError("--build is only available for android, ios and harmony")
|
||||
values = {"version": version}
|
||||
if component in MOBILE:
|
||||
build = build if build is not None else str(int(previous["build"]) + (version != previous["version"]))
|
||||
validate_build(component, build)
|
||||
if int(build) < int(previous["build"]) or (version != previous["version"] and build == previous["build"]):
|
||||
raise ValueError(f"{component}: a changed app version requires a higher build number; build numbers cannot decrease")
|
||||
values["build"] = build
|
||||
if component == "mcp":
|
||||
values["native_version"] = version
|
||||
for key, value in values.items():
|
||||
for field in fields(snapshot, component)[key]:
|
||||
field.update(snapshot, value)
|
||||
snapshot.state(component)
|
||||
|
||||
|
||||
def update(snapshot, component, version, build=None,
|
||||
android_build=None, ios_build=None, harmony_build=None):
|
||||
if component in CLIENT_GROUPS or component in CLIENTS:
|
||||
if build is not None and component not in MOBILE:
|
||||
raise ValueError("--build requires android, ios or harmony; use platform-specific build options with client")
|
||||
if component not in CLIENT_GROUPS and any(value is not None for value in (android_build, ios_build, harmony_build)):
|
||||
raise ValueError("Use client with --android-build/--ios-build/--harmony-build, or --build for the selected platform")
|
||||
builds = {"android": android_build, "ios": ios_build, "harmony": harmony_build}
|
||||
if component in MOBILE:
|
||||
builds[component] = build
|
||||
for platform in CLIENTS:
|
||||
update_component(snapshot, platform, version, builds.get(platform))
|
||||
snapshot.state("client")
|
||||
else:
|
||||
if any(value is not None for value in (android_build, ios_build, harmony_build)):
|
||||
raise ValueError("Platform-specific build options are only available for client")
|
||||
if component in TOOLS:
|
||||
for tool in TOOLS:
|
||||
update_component(snapshot, tool, version, build)
|
||||
check_tools(snapshot)
|
||||
else:
|
||||
update_component(snapshot, component, version, build)
|
||||
|
||||
|
||||
def parser():
|
||||
result = argparse.ArgumentParser(description=__doc__,
|
||||
epilog="Desktop, Android, iOS and HarmonyOS share one client version; build numbers remain independent. mobile is an alias for client. CLI, MCP, DSH and MCP's native CLI dependency share another version; set/bump cli or mcp updates all of them.")
|
||||
sub = result.add_subparsers(dest="command", required=True)
|
||||
show = sub.add_parser("show", help="Show current versions")
|
||||
show.add_argument("component", nargs="?", choices=COMPONENT_CHOICES)
|
||||
show.add_argument("--json", action="store_true", help="Print machine-readable values")
|
||||
show.add_argument("--field", choices=("version", "build", "native_version", "android_build", "ios_build", "harmony_build"), help="Print one field of one component")
|
||||
check = sub.add_parser("check", help="Validate version formats and synchronized fields")
|
||||
check.add_argument("component", nargs="?", choices=COMPONENT_CHOICES)
|
||||
for name, description in (("set", "Set an exact version"), ("bump", "Increment a version or mobile build number")):
|
||||
command = sub.add_parser(name, help=description)
|
||||
command.add_argument("component", choices=COMPONENT_CHOICES)
|
||||
if name == "set":
|
||||
command.add_argument("version")
|
||||
else:
|
||||
command.add_argument("part", choices=("major", "minor", "patch", "build"))
|
||||
command.add_argument("--build", help="Explicit mobile build number; app version changes otherwise increment it")
|
||||
command.add_argument("--android-build", help="Android build number when setting/bumping client")
|
||||
command.add_argument("--ios-build", help="iOS build number when setting/bumping client")
|
||||
command.add_argument("--harmony-build", help="HarmonyOS build number when setting/bumping client")
|
||||
command.add_argument("--dry-run", action="store_true", help="Preview the diff without writing files")
|
||||
return result
|
||||
|
||||
|
||||
def main(argv=None, root=ROOT):
|
||||
args = parser().parse_args(argv)
|
||||
snapshot = Snapshot(root)
|
||||
if args.command in ("show", "check"):
|
||||
selected = (args.component,) if args.component else COMPONENTS
|
||||
values, errors = {}, []
|
||||
for component in selected:
|
||||
try:
|
||||
values[component] = snapshot.state(component)
|
||||
except (ValueError, OSError) as error:
|
||||
errors.append(f"{component}: {error}")
|
||||
if any(component in CLIENTS for component in selected) and not errors:
|
||||
try:
|
||||
snapshot.state("client")
|
||||
except (ValueError, OSError) as error:
|
||||
errors.append(str(error))
|
||||
if any(component in TOOLS for component in selected) and not errors:
|
||||
try:
|
||||
check_tools(snapshot)
|
||||
except (ValueError, OSError) as error:
|
||||
errors.append(str(error))
|
||||
if errors:
|
||||
raise ValueError("\n".join(errors))
|
||||
if args.command == "check":
|
||||
print(f"Version checks passed: {', '.join(selected)}")
|
||||
elif args.field:
|
||||
if not args.component or args.json or args.field not in values[args.component]:
|
||||
raise ValueError("--field requires one component with that field and cannot be combined with --json")
|
||||
print(values[args.component][args.field])
|
||||
elif args.json:
|
||||
print(json.dumps(values[args.component] if args.component else values, indent=2))
|
||||
else:
|
||||
for component, data in values.items():
|
||||
print(f"{component:8} " + " ".join(f"{key}={value}" for key, value in data.items()))
|
||||
return
|
||||
version, build = getattr(args, "version", None), args.build
|
||||
if args.command == "bump":
|
||||
if args.component in CLIENTS:
|
||||
snapshot.state("client")
|
||||
if args.component in TOOLS:
|
||||
check_tools(snapshot)
|
||||
current = snapshot.state(args.component)
|
||||
version = current["version"]
|
||||
if args.part == "build":
|
||||
if args.component not in MOBILE or any(value is not None for value in (build, args.android_build, args.ios_build, args.harmony_build)):
|
||||
raise ValueError("bump build requires android, ios or harmony and cannot be combined with explicit build numbers")
|
||||
build = str(int(current["build"]) + 1)
|
||||
else:
|
||||
match = validate_version(version, prerelease=True)
|
||||
if match[4]:
|
||||
raise ValueError("Use set to choose the next version explicitly when the current version is a prerelease")
|
||||
numbers = [int(match[index]) for index in range(1, 4)]
|
||||
index = ("major", "minor", "patch").index(args.part)
|
||||
numbers[index] += 1
|
||||
numbers[index + 1:] = [0] * (2 - index)
|
||||
version = ".".join(map(str, numbers))
|
||||
build_only = args.command == "bump" and args.part == "build"
|
||||
if build_only:
|
||||
update_component(snapshot, args.component, version, build)
|
||||
else:
|
||||
update(snapshot, args.component, version, build,
|
||||
args.android_build, args.ios_build, args.harmony_build)
|
||||
snapshot.save(args.dry_run)
|
||||
reported = "client" if (args.component in CLIENTS or args.component in CLIENT_GROUPS) and not build_only else args.component
|
||||
label = "cli/mcp/dsh" if reported in TOOLS else reported
|
||||
print(f"{'Preview' if args.dry_run else 'Current'} {label}: " + json.dumps(snapshot.state(reported)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (ValueError, OSError) as error:
|
||||
print(f"version: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user