diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index e130af77..ac4d35d3 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -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=() diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 4d4c844f..234fd46f 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -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 diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 4e113e88..9a26da1a 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -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: | diff --git a/.github/workflows/build-mcp.yml b/.github/workflows/build-mcp.yml index 79e9d1a2..12bb2e5b 100644 --- a/.github/workflows/build-mcp.yml +++ b/.github/workflows/build-mcp.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ef126d1..bdfe3708 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/check-versions.yml b/.github/workflows/check-versions.yml new file mode 100644 index 00000000..35a9ec8b --- /dev/null +++ b/.github/workflows/check-versions.yml @@ -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 diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index cf417edd..65cb38ac 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -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 diff --git a/README.md b/README.md index 74ed8ed5..8a5ae5ab 100644 --- a/README.md +++ b/README.md @@ -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):连接失败、黑屏、系统权限、文件位置与构建问题。 diff --git a/README_EN.md b/README_EN.md index 0e675414..31a11365 100644 --- a/README_EN.md +++ b/README_EN.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. diff --git a/apps/android/README.md b/apps/android/README.md index a1577cee..5f7531a1 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -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-YYYYMMDD` 标签,例如: +单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先在仓库根目录运行 `python3 scripts/version.py set client <版本>` 同步全部客户端版本及各移动端构建号,检查并提交改动,再为该提交创建 `android-v-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 提交。 diff --git a/apps/android/gradle.properties b/apps/android/gradle.properties index 06741f7c..08242b93 100644 --- a/apps/android/gradle.properties +++ b/apps/android/gradle.properties @@ -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 diff --git a/apps/cli/docs/npm-distribution.md b/apps/cli/docs/npm-distribution.md index 89c79cca..90944271 100644 --- a/apps/cli/docs/npm-distribution.md +++ b/apps/cli/docs/npm-distribution.md @@ -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 的手动运行入口需要该工作流先进入默认分支。 diff --git a/apps/desktop/xmake/options.lua b/apps/desktop/xmake/options.lua index 28dc05ec..bf7c987f 100644 --- a/apps/desktop/xmake/options.lua +++ b/apps/desktop/xmake/options.lua @@ -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() diff --git a/apps/harmony/AppScope/app.json5 b/apps/harmony/AppScope/app.json5 index 367349bf..908de3d5 100644 --- a/apps/harmony/AppScope/app.json5 +++ b/apps/harmony/AppScope/app.json5 @@ -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" } diff --git a/apps/ios/README.md b/apps/ios/README.md index 78da4a3c..d3e737aa 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -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 ` to update all clients, +then commit the changes and tag that commit with `ios-v-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 diff --git a/apps/mcp/DEVELOPMENT.md b/apps/mcp/DEVELOPMENT.md index 949364f2..f4c13fb4 100644 --- a/apps/mcp/DEVELOPMENT.md +++ b/apps/mcp/DEVELOPMENT.md @@ -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;内含原生程序的平台包不受此顺序限制。构建通过不等于各宿主和设备上的联调完成。 diff --git a/apps/mcp/README.md b/apps/mcp/README.md index a2931323..2927fc40 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -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/...` 都需要换成自己电脑上的完整路径**,版本号也应与实际下载的文件一致。路径中有空格时保留引号。 @@ -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" ``` 安装完成后,在同一目录运行: diff --git a/apps/mcp/integrations/deepseek-harness/README.md b/apps/mcp/integrations/deepseek-harness/README.md index d2c98ada..fc976fcc 100644 --- a/apps/mcp/integrations/deepseek-harness/README.md +++ b/apps/mcp/integrations/deepseek-harness/README.md @@ -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 插件已启用。 diff --git a/apps/mcp/integrations/deepseek-harness/package.json b/apps/mcp/integrations/deepseek-harness/package.json index 149da6b2..f53f4674 100644 --- a/apps/mcp/integrations/deepseek-harness/package.json +++ b/apps/mcp/integrations/deepseek-harness/package.json @@ -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", diff --git a/apps/mcp/package-lock.json b/apps/mcp/package-lock.json index e188439e..64c64016 100644 --- a/apps/mcp/package-lock.json +++ b/apps/mcp/package-lock.json @@ -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", diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 6d90add6..244fa7db 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -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", diff --git a/apps/mcp/scripts/pack.mjs b/apps/mcp/scripts/pack.mjs index fc901822..79068182 100644 --- a/apps/mcp/scripts/pack.mjs +++ b/apps/mcp/scripts/pack.mjs @@ -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" }, }; diff --git a/apps/mcp/src/main.ts b/apps/mcp/src/main.ts index 5a2160c6..7844ea4c 100644 --- a/apps/mcp/src/main.ts +++ b/apps/mcp/src/main.ts @@ -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) { diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index 4b2f8a43..79b0e383 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -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: diff --git a/apps/mcp/src/version.ts b/apps/mcp/src/version.ts new file mode 100644 index 00000000..e28e31e2 --- /dev/null +++ b/apps/mcp/src/version.ts @@ -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; diff --git a/docs/BUILD.md b/docs/BUILD.md index 1265da66..d5b1f2a3 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -2,6 +2,8 @@ [返回 README](../README.md) · [English](BUILD_EN.md) +各组件版本可通过 `python3 scripts/version.py show` 查看,使用 `set` 或 `bump` 修改;同步范围和发布衔接见[组件版本管理](VERSIONING.md)。 + 安装版的升级器随主程序构建。发布公钥、清单签名和验证方法见 [桌面端应用内升级](UPDATER.md);便携版不包含升级器。 diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md new file mode 100644 index 00000000..9a3aa066 --- /dev/null +++ b/docs/VERSIONING.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#打包)。 diff --git a/scripts/version.py b/scripts/version.py new file mode 100644 index 00000000..734540bf --- /dev/null +++ b/scripts/version.py @@ -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[^"\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[^"\n]+)";')] + return result + if component == "desktop": + return {"version": [Field("apps/desktop/xmake/options.lua", r'option\("CROSSDESK_VERSION"\)\s*set_default\("(?P[^"\n]+)"\)')]} + if component == "android": + return {key: [Field("apps/android/gradle.properties", rf'^{prop}=(?P[^\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[^;\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[^"\n]+)"')], + "build": [Field("apps/harmony/AppScope/app.json5", r'"versionCode"\s*:\s*(?P[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)