diff --git a/.github/workflows/update-app-upgrade-config.yml b/.github/workflows/update-app-upgrade-config.yml new file mode 100644 index 000000000..acb838117 --- /dev/null +++ b/.github/workflows/update-app-upgrade-config.yml @@ -0,0 +1,212 @@ +name: Update App Upgrade Config + +on: + release: + types: + - released + - prereleased + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g., v1.2.3)" + required: true + type: string + is_prerelease: + description: "Mark the tag as a prerelease when running manually" + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + propose-update: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.draft == false) + + steps: + - name: Check if should proceed + id: check + run: | + EVENT="${{ github.event_name }}" + + if [ "$EVENT" = "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.tag }}" + else + TAG="${{ github.event.release.tag_name }}" + fi + + latest_tag=$( + curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ github.token }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/${{ github.repository }}/releases/latest \ + | jq -r '.tag_name' + ) + + if [ "$EVENT" = "workflow_dispatch" ]; then + MANUAL_IS_PRERELEASE="${{ github.event.inputs.is_prerelease }}" + if [ -z "$MANUAL_IS_PRERELEASE" ]; then + MANUAL_IS_PRERELEASE="false" + fi + if [ "$MANUAL_IS_PRERELEASE" = "true" ]; then + if ! echo "$TAG" | grep -E '(-beta([.-][0-9]+)?|-rc([.-][0-9]+)?)' >/dev/null; then + echo "Manual prerelease flag set but tag $TAG lacks beta/rc suffix. Skipping." >&2 + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=$MANUAL_IS_PRERELEASE" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + + IS_PRERELEASE="${{ github.event.release.prerelease }}" + + if [ "$IS_PRERELEASE" = "true" ]; then + if ! echo "$TAG" | grep -E '(-beta([.-][0-9]+)?|-rc([.-][0-9]+)?)' >/dev/null; then + echo "Release marked as prerelease but tag $TAG lacks beta/rc suffix. Skipping." >&2 + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is prerelease, proceeding" + exit 0 + fi + + if [[ "${latest_tag}" == "$TAG" ]]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is latest, proceeding" + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "Release is neither prerelease nor latest, skipping" + fi + + - name: Prepare metadata + id: meta + if: steps.check.outputs.should_run == 'true' + run: | + EVENT="${{ github.event_name }}" + LATEST_TAG="${{ steps.check.outputs.latest_tag }}" + if [ "$EVENT" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + PRE="${{ github.event.release.prerelease }}" + + if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" = "$TAG" ]; then + LATEST="true" + else + LATEST="false" + fi + TRIGGER="release" + else + TAG="${{ github.event.inputs.tag }}" + PRE="${{ github.event.inputs.is_prerelease }}" + if [ -z "$PRE" ]; then + PRE="false" + fi + if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" = "$TAG" ] && [ "$PRE" != "true" ]; then + LATEST="true" + else + LATEST="false" + fi + TRIGGER="manual" + fi + + SAFE_TAG=$(echo "$TAG" | sed 's/[^A-Za-z0-9._-]/-/g') + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "safe_tag=$SAFE_TAG" >> "$GITHUB_OUTPUT" + echo "prerelease=$PRE" >> "$GITHUB_OUTPUT" + echo "latest=$LATEST" >> "$GITHUB_OUTPUT" + echo "trigger=$TRIGGER" >> "$GITHUB_OUTPUT" + + - name: Checkout default branch + if: steps.check.outputs.should_run == 'true' + uses: actions/checkout@v5 + with: + ref: ${{ github.event.repository.default_branch }} + path: main + fetch-depth: 0 + + - name: Checkout cs-releases branch + if: steps.check.outputs.should_run == 'true' + uses: actions/checkout@v5 + with: + ref: cs-releases + path: cs + fetch-depth: 0 + + - name: Setup Node.js + if: steps.check.outputs.should_run == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Enable Corepack + if: steps.check.outputs.should_run == 'true' + run: corepack enable && corepack prepare yarn@4.9.1 --activate + + - name: Install dependencies + if: steps.check.outputs.should_run == 'true' + working-directory: main + run: yarn install --immutable + + - name: Update upgrade config + if: steps.check.outputs.should_run == 'true' + working-directory: main + env: + RELEASE_TAG: ${{ steps.meta.outputs.tag }} + IS_PRERELEASE: ${{ steps.check.outputs.is_prerelease }} + run: | + yarn tsx scripts/update-app-upgrade-config.ts \ + --tag "$RELEASE_TAG" \ + --config ../cs/app-upgrade-config.json \ + --is-prerelease "$IS_PRERELEASE" + + - name: Detect changes + if: steps.check.outputs.should_run == 'true' + id: diff + working-directory: cs + run: | + if git diff --quiet -- app-upgrade-config.json; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create pull request + if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + path: cs + base: cs-releases + branch: chore/update-app-upgrade-config/${{ steps.meta.outputs.safe_tag }} + commit-message: "🤖 chore: sync app-upgrade-config for ${{ steps.meta.outputs.tag }}" + title: "chore: update app-upgrade-config for ${{ steps.meta.outputs.tag }}" + body: | + Automated update triggered by `${{ steps.meta.outputs.trigger }}`. + + - Source tag: `${{ steps.meta.outputs.tag }}` + - Pre-release: `${{ steps.meta.outputs.prerelease }}` + - Latest: `${{ steps.meta.outputs.latest }}` + - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + labels: | + automation + app-upgrade + + - name: No changes detected + if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed != 'true' + run: echo "No updates required for cs-releases/app-upgrade-config.json" diff --git a/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch b/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch deleted file mode 100644 index e9ca84e6c..000000000 --- a/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch +++ /dev/null @@ -1,276 +0,0 @@ -diff --git a/out/macPackager.js b/out/macPackager.js -index 852f6c4d16f86a7bb8a78bf1ed5a14647a279aa1..60e7f5f16a844541eb1909b215fcda1811e924b8 100644 ---- a/out/macPackager.js -+++ b/out/macPackager.js -@@ -423,7 +423,7 @@ class MacPackager extends platformPackager_1.PlatformPackager { - } - appPlist.CFBundleName = appInfo.productName; - appPlist.CFBundleDisplayName = appInfo.productName; -- const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion; -+ const minimumSystemVersion = this.platformSpecificBuildOptions.LSMinimumSystemVersion; - if (minimumSystemVersion != null) { - appPlist.LSMinimumSystemVersion = minimumSystemVersion; - } -diff --git a/out/publish/updateInfoBuilder.js b/out/publish/updateInfoBuilder.js -index 7924c5b47d01f8dfccccb8f46658015fa66da1f7..1a1588923c3939ae1297b87931ba83f0ebc052d8 100644 ---- a/out/publish/updateInfoBuilder.js -+++ b/out/publish/updateInfoBuilder.js -@@ -133,6 +133,7 @@ async function createUpdateInfo(version, event, releaseInfo) { - const customUpdateInfo = event.updateInfo; - const url = path.basename(event.file); - const sha512 = (customUpdateInfo == null ? null : customUpdateInfo.sha512) || (await (0, hash_1.hashFile)(event.file)); -+ const minimumSystemVersion = customUpdateInfo == null ? null : customUpdateInfo.minimumSystemVersion; - const files = [{ url, sha512 }]; - const result = { - // @ts-ignore -@@ -143,9 +144,13 @@ async function createUpdateInfo(version, event, releaseInfo) { - path: url /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */, - // @ts-ignore - sha512 /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */, -+ minimumSystemVersion, - ...releaseInfo, - }; - if (customUpdateInfo != null) { -+ if (customUpdateInfo.minimumSystemVersion) { -+ delete customUpdateInfo.minimumSystemVersion; -+ } - // file info or nsis web installer packages info - Object.assign("sha512" in customUpdateInfo ? files[0] : result, customUpdateInfo); - } -diff --git a/out/targets/ArchiveTarget.js b/out/targets/ArchiveTarget.js -index e1f52a5fa86fff6643b2e57eaf2af318d541f865..47cc347f154a24b365e70ae5e1f6d309f3582ed0 100644 ---- a/out/targets/ArchiveTarget.js -+++ b/out/targets/ArchiveTarget.js -@@ -69,6 +69,9 @@ class ArchiveTarget extends core_1.Target { - } - } - } -+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) { -+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion; -+ } - await packager.info.emitArtifactBuildCompleted({ - updateInfo, - file: artifactPath, -diff --git a/out/targets/nsis/NsisTarget.js b/out/targets/nsis/NsisTarget.js -index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01de4b89b1 100644 ---- a/out/targets/nsis/NsisTarget.js -+++ b/out/targets/nsis/NsisTarget.js -@@ -305,6 +305,9 @@ class NsisTarget extends core_1.Target { - if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) { - updateInfo.isAdminRightsRequired = true; - } -+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) { -+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion; -+ } - await packager.info.emitArtifactBuildCompleted({ - file: installerPath, - updateInfo, -diff --git a/out/util/yarn.js b/out/util/yarn.js -index 1ee20f8b252a8f28d0c7b103789cf0a9a427aec1..c2878ec54d57da50bf14225e0c70c9c88664eb8a 100644 ---- a/out/util/yarn.js -+++ b/out/util/yarn.js -@@ -140,6 +140,7 @@ async function rebuild(config, { appDir, projectDir }, options) { - arch, - platform, - buildFromSource, -+ ignoreModules: config.excludeReBuildModules || undefined, - projectRootPath: projectDir, - mode: config.nativeRebuilder || "sequential", - disablePreGypCopy: true, -diff --git a/scheme.json b/scheme.json -index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..0167441bf928a92f59b5dbe70b2317a74dda74c9 100644 ---- a/scheme.json -+++ b/scheme.json -@@ -1825,6 +1825,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableArgs": { - "anyOf": [ - { -@@ -1975,6 +1989,13 @@ - ], - "description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing." - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "packageCategory": { - "description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place", - "type": [ -@@ -2327,6 +2348,13 @@ - "MacConfiguration": { - "additionalProperties": false, - "properties": { -+ "LSMinimumSystemVersion": { -+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "additionalArguments": { - "anyOf": [ - { -@@ -2527,6 +2555,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -2737,7 +2779,7 @@ - "type": "boolean" - }, - "minimumSystemVersion": { -- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "description": "The minimum os kernel version required to install the application.", - "type": [ - "null", - "string" -@@ -2959,6 +3001,13 @@ - "MasConfiguration": { - "additionalProperties": false, - "properties": { -+ "LSMinimumSystemVersion": { -+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "additionalArguments": { - "anyOf": [ - { -@@ -3159,6 +3208,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -3369,7 +3432,7 @@ - "type": "boolean" - }, - "minimumSystemVersion": { -- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.", -+ "description": "The minimum os kernel version required to install the application.", - "type": [ - "null", - "string" -@@ -6381,6 +6444,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -6507,6 +6584,13 @@ - "string" - ] - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "protocols": { - "anyOf": [ - { -@@ -7153,6 +7237,20 @@ - "string" - ] - }, -+ "excludeReBuildModules": { -+ "anyOf": [ -+ { -+ "items": { -+ "type": "string" -+ }, -+ "type": "array" -+ }, -+ { -+ "type": "null" -+ } -+ ], -+ "description": "The modules to exclude from the rebuild." -+ }, - "executableName": { - "description": "The executable name. Defaults to `productName`.", - "type": [ -@@ -7376,6 +7474,13 @@ - ], - "description": "MAS (Mac Application Store) development options (`mas-dev` target)." - }, -+ "minimumSystemVersion": { -+ "description": "The minimum os kernel version required to install the application.", -+ "type": [ -+ "null", -+ "string" -+ ] -+ }, - "msi": { - "anyOf": [ - { diff --git a/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch b/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch new file mode 100644 index 000000000..f9e54ac94 --- /dev/null +++ b/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch @@ -0,0 +1,14 @@ +diff --git a/out/util.js b/out/util.js +index 9294ffd6ca8f02c2e0f90c663e7e9cdc02c1ac37..f52107493e2995320ee4efd0eb2a8c9bf03291a2 100644 +--- a/out/util.js ++++ b/out/util.js +@@ -23,7 +23,8 @@ function newUrlFromBase(pathname, baseUrl, addRandomQueryToAvoidCaching = false) + result.search = search; + } + else if (addRandomQueryToAvoidCaching) { +- result.search = `noCache=${Date.now().toString(32)}`; ++ // use no cache header instead ++ // result.search = `noCache=${Date.now().toString(32)}`; + } + return result; + } diff --git a/app-upgrade-config.json b/app-upgrade-config.json new file mode 100644 index 000000000..84e381c86 --- /dev/null +++ b/app-upgrade-config.json @@ -0,0 +1,49 @@ +{ + "lastUpdated": "2025-11-10T08:14:28Z", + "versions": { + "1.6.7": { + "metadata": { + "segmentId": "legacy-v1", + "segmentType": "legacy" + }, + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://releases.cherry-ai.com" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.7.0-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "metadata": { + "segmentId": "gateway-v2", + "segmentType": "breaking" + }, + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json new file mode 100644 index 000000000..70c8ac25f --- /dev/null +++ b/config/app-upgrade-segments.json @@ -0,0 +1,81 @@ +{ + "segments": [ + { + "id": "legacy-v1", + "type": "legacy", + "match": { + "range": ">=1.0.0 <2.0.0" + }, + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://releases.cherry-ai.com" + } + }, + "rc": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "beta": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + }, + { + "id": "gateway-v2", + "type": "breaking", + "match": { + "exact": ["2.0.0"] + }, + "lockedVersion": "2.0.0", + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + }, + { + "id": "current-v2", + "type": "latest", + "match": { + "range": ">=2.0.0 <3.0.0", + "excludeExact": ["2.0.0"] + }, + "minCompatibleVersion": "2.0.0", + "description": "Current latest v2.x release", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "rc": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + }, + "beta": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" + } + } + } + } + ] +} diff --git a/docs/technical/app-upgrade-config-en.md b/docs/technical/app-upgrade-config-en.md new file mode 100644 index 000000000..993c130d7 --- /dev/null +++ b/docs/technical/app-upgrade-config-en.md @@ -0,0 +1,430 @@ +# Update Configuration System Design Document + +## Background + +Currently, AppUpdater directly queries the GitHub API to retrieve beta and rc update information. To support users in China, we need to fetch a static JSON configuration file from GitHub/GitCode based on IP geolocation, which contains update URLs for all channels. + +## Design Goals + +1. Support different configuration sources based on IP geolocation (GitHub/GitCode) +2. Support version compatibility control (e.g., users below v1.x must upgrade to v1.7.0 before upgrading to v2.0) +3. Easy to extend, supporting future multi-major-version upgrade paths (v1.6 → v1.7 → v2.0 → v2.8 → v3.0) +4. Maintain compatibility with existing electron-updater mechanism + +## Current Version Strategy + +- **v1.7.x** is the last version of the 1.x series +- Users **below v1.7.0** must first upgrade to v1.7.0 (or higher 1.7.x version) +- Users **v1.7.0 and above** can directly upgrade to v2.x.x + +## Automation Workflow + +The `cs-releases/app-upgrade-config.json` file is synchronized by the [`Update App Upgrade Config`](../../.github/workflows/update-app-upgrade-config.yml) workflow. The workflow runs the [`scripts/update-app-upgrade-config.ts`](../../scripts/update-app-upgrade-config.ts) helper so that every release tag automatically updates the JSON in `cs-releases`. + +### Trigger Conditions + +- **Release events (`release: released/prereleased`)** + - Draft releases are ignored. + - When GitHub marks the release as _prerelease_, the tag must include `-beta`/`-rc` (with optional numeric suffix). Otherwise the workflow exits early. + - When GitHub marks the release as stable, the tag must match the latest release returned by the GitHub API. This prevents out-of-order updates when publishing historical tags. + - If the guard clauses pass, the version is tagged as `latest` or `beta/rc` based on its semantic suffix and propagated to the script through the `IS_PRERELEASE` flag. +- **Manual dispatch (`workflow_dispatch`)** + - Required input: `tag` (e.g., `v2.0.1`). Optional input: `is_prerelease` (defaults to `false`). + - When `is_prerelease=true`, the tag must carry a beta/rc suffix, mirroring the automatic validation. + - Manual runs still download the latest release metadata so that the workflow knows whether the tag represents the newest stable version (for documentation inside the PR body). + +### Workflow Steps + +1. **Guard + metadata preparation** – the `Check if should proceed` and `Prepare metadata` steps compute the target tag, prerelease flag, whether the tag is the newest release, and a `safe_tag` slug used for branch names. When any rule fails, the workflow stops without touching the config. +2. **Checkout source branches** – the default branch is checked out into `main/`, while the long-lived `cs-releases` branch lives in `cs/`. All modifications happen in the latter directory. +3. **Install toolchain** – Node.js 22, Corepack, and frozen Yarn dependencies are installed inside `main/`. +4. **Run the update script** – `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json --is-prerelease ` updates the JSON in-place. + - The script normalizes the tag (e.g., strips `v` prefix), detects the release channel (`latest`, `rc`, `beta`), and loads segment rules from `config/app-upgrade-segments.json`. + - It validates that prerelease flags and semantic suffixes agree, enforces locked segments, builds mirror feed URLs, and performs release-availability checks (GitHub HEAD request for every channel; GitCode GET for latest channels, falling back to `https://releases.cherry-ai.com` when gitcode is delayed). + - After updating the relevant channel entry, the script rewrites the config with semver-sort order and a new `lastUpdated` timestamp. +5. **Detect changes + create PR** – if `cs/app-upgrade-config.json` changed, the workflow opens a PR `chore/update-app-upgrade-config/` against `cs-releases` with a commit message `🤖 chore: sync app-upgrade-config for `. Otherwise it logs that no update is required. + +### Manual Trigger Guide + +1. Open the Cherry Studio repository on GitHub → **Actions** tab → select **Update App Upgrade Config**. +2. Click **Run workflow**, choose the default branch (usually `main`), and fill in the `tag` input (e.g., `v2.1.0`). +3. Toggle `is_prerelease` only when the tag carries a prerelease suffix (`-beta`, `-rc`). Leave it unchecked for stable releases. +4. Start the run and wait for it to finish. Check the generated PR in the `cs-releases` branch, verify the diff in `app-upgrade-config.json`, and merge once validated. + +## JSON Configuration File Format + +### File Location + +- **GitHub**: `https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/cs-releases/app-upgrade-config.json` +- **GitCode**: `https://gitcode.com/CherryHQ/cherry-studio/raw/cs-releases/app-upgrade-config.json` + +**Note**: Both mirrors provide the same configuration file hosted on the `cs-releases` branch. The client automatically selects the optimal mirror based on IP geolocation. + +### Configuration Structure (Current Implementation) + +```json +{ + "lastUpdated": "2025-01-05T00:00:00Z", + "versions": { + "1.6.7": { + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v1.6.7" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.6.7-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} +``` + +### Future Extension Example + +When releasing v3.0, if users need to first upgrade to v2.8, you can add: + +```json +{ + "2.8.0": { + "minCompatibleVersion": "2.0.0", + "description": "Stable v2.8 - required for v3 upgrade", + "channels": { + "latest": { + "version": "2.8.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v2.8.0", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v2.8.0" + } + }, + "rc": null, + "beta": null + } + }, + "3.0.0": { + "minCompatibleVersion": "2.8.0", + "description": "Major release v3.0", + "channels": { + "latest": { + "version": "3.0.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/latest", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/latest" + } + }, + "rc": { + "version": "3.0.0-rc.1", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1" + } + }, + "beta": null + } + } +} +``` + +### Field Descriptions + +- `lastUpdated`: Last update time of the configuration file (ISO 8601 format) +- `versions`: Version configuration object, key is the version number, sorted by semantic versioning + - `minCompatibleVersion`: Minimum compatible version that can upgrade to this version + - `description`: Version description + - `channels`: Update channel configuration + - `latest`: Stable release channel + - `rc`: Release Candidate channel + - `beta`: Beta testing channel + - Each channel contains: + - `version`: Version number for this channel + - `feedUrls`: Multi-mirror URL configuration + - `github`: electron-updater feed URL for GitHub mirror + - `gitcode`: electron-updater feed URL for GitCode mirror + - `metadata`: Stable mapping info for automation + - `segmentId`: ID from `config/app-upgrade-segments.json` + - `segmentType`: Optional flag (`legacy` | `breaking` | `latest`) for documentation/debugging + +## TypeScript Type Definitions + +```typescript +// Mirror enum +enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } + metadata?: { + segmentId: string + segmentType?: 'legacy' | 'breaking' | 'latest' + } +} + +interface ChannelConfig { + version: string + feedUrls: Record + // Equivalent to: + // feedUrls: { + // github: string + // gitcode: string + // } +} +``` + +## Segment Metadata & Breaking Markers + +- **Segment definitions** now live in `config/app-upgrade-segments.json`. Each segment describes a semantic-version range (or exact matches) plus metadata such as `segmentId`, `segmentType`, `minCompatibleVersion`, and per-channel feed URL templates. +- Each entry under `versions` carries a `metadata.segmentId`. This acts as the stable key that scripts use to decide which slot to update, even if the actual semantic version string changes. +- Mark major upgrade gateways (e.g., `2.0.0`) by giving the related segment a `segmentType: "breaking"` and (optionally) `lockedVersion`. This prevents automation from accidentally moving that entry when other 2.x builds ship. +- Adding another breaking hop (e.g., `3.0.0`) only requires defining a new segment in the JSON file; the automation will pick it up on the next run. + +## Automation Workflow + +Starting from this change, `.github/workflows/update-app-upgrade-config.yml` listens to GitHub release events (published + prerelease). The workflow: + +1. Checks out the default branch (for scripts) and the `cs-releases` branch (where the config is hosted). +2. Runs `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json` to regenerate the config directly inside the `cs-releases` working tree. +3. If the file changed, it opens a PR against `cs-releases` via `peter-evans/create-pull-request`, with the generated diff limited to `app-upgrade-config.json`. + +You can run the same script locally via `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json` (add `--dry-run` to preview) to reproduce or debug whatever the workflow does. Passing `--skip-release-checks` along with `--dry-run` lets you bypass the release-page existence check (useful when the GitHub/GitCode pages aren’t published yet). Running without `--config` continues to update the copy in your current working directory (main branch) for documentation purposes. + +## Version Matching Logic + +### Algorithm Flow + +1. Get user's current version (`currentVersion`) and requested channel (`requestedChannel`) +2. Get all version numbers from configuration file, sort in descending order by semantic versioning +3. Iterate through the sorted version list: + - Check if `currentVersion >= minCompatibleVersion` + - Check if the requested `channel` exists and is not `null` + - If conditions are met, return the channel configuration +4. If no matching version is found, return `null` + +### Pseudocode Implementation + +```typescript +function findCompatibleVersion( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig +): ChannelConfig | null { + // Get all version numbers and sort in descending order + const versions = Object.keys(config.versions).sort(semver.rcompare) + + for (const versionKey of versions) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + + // Check version compatibility and channel availability + if ( + semver.gte(currentVersion, versionConfig.minCompatibleVersion) && + channelConfig !== null + ) { + return channelConfig + } + } + + return null // No compatible version found +} +``` + +## Upgrade Path Examples + +### Scenario 1: v1.6.5 User Upgrade (Below 1.7) + +- **Current Version**: 1.6.5 +- **Requested Channel**: latest +- **Match Result**: 1.7.0 +- **Reason**: 1.6.5 >= 0.0.0 (satisfies 1.7.0's minCompatibleVersion), but doesn't satisfy 2.0.0's minCompatibleVersion (1.7.0) +- **Action**: Prompt user to upgrade to 1.7.0, which is the required intermediate version for v2.x upgrade + +### Scenario 2: v1.6.5 User Requests rc/beta + +- **Current Version**: 1.6.5 +- **Requested Channel**: rc or beta +- **Match Result**: 1.7.0 (latest) +- **Reason**: 1.7.0 version doesn't provide rc/beta channels (values are null) +- **Action**: Upgrade to 1.7.0 stable version + +### Scenario 3: v1.7.0 User Upgrades to Latest + +- **Current Version**: 1.7.0 +- **Requested Channel**: latest +- **Match Result**: 2.0.0 +- **Reason**: 1.7.0 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion) +- **Action**: Directly upgrade to 2.0.0 (current latest stable version) + +### Scenario 4: v1.7.2 User Upgrades to RC Version + +- **Current Version**: 1.7.2 +- **Requested Channel**: rc +- **Match Result**: 2.0.0-rc.1 +- **Reason**: 1.7.2 >= 1.7.0 (satisfies 2.0.0's minCompatibleVersion), and rc channel exists +- **Action**: Upgrade to 2.0.0-rc.1 + +### Scenario 5: v1.7.0 User Upgrades to Beta Version + +- **Current Version**: 1.7.0 +- **Requested Channel**: beta +- **Match Result**: 2.0.0-beta.1 +- **Reason**: 1.7.0 >= 1.7.0, and beta channel exists +- **Action**: Upgrade to 2.0.0-beta.1 + +### Scenario 6: v2.5.0 User Upgrade (Future) + +Assuming v2.8.0 and v3.0.0 configurations have been added: +- **Current Version**: 2.5.0 +- **Requested Channel**: latest +- **Match Result**: 2.8.0 +- **Reason**: 2.5.0 >= 2.0.0 (satisfies 2.8.0's minCompatibleVersion), but doesn't satisfy 3.0.0's requirement +- **Action**: Prompt user to upgrade to 2.8.0, which is the required intermediate version for v3.x upgrade + +## Code Changes + +### Main Modifications + +1. **New Methods** + - `_fetchUpdateConfig(ipCountry: string): Promise` - Fetch configuration file based on IP + - `_findCompatibleChannel(currentVersion: string, channel: UpgradeChannel, config: UpdateConfig): ChannelConfig | null` - Find compatible channel configuration + +2. **Modified Methods** + - `_getReleaseVersionFromGithub()` → Remove or refactor to `_getChannelFeedUrl()` + - `_setFeedUrl()` - Use new configuration system to replace existing logic + +3. **New Type Definitions** + - `UpdateConfig` + - `VersionConfig` + - `ChannelConfig` + +### Mirror Selection Logic + +The client automatically selects the optimal mirror based on IP geolocation: + +```typescript +private async _setFeedUrl() { + const currentVersion = app.getVersion() + const testPlan = configManager.getTestPlan() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST + + // Determine mirror based on IP country + const ipCountry = await getIpCountry() + const mirror = ipCountry.toLowerCase() === 'cn' ? 'gitcode' : 'github' + + // Fetch update config + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + const channelConfig = this._findCompatibleChannel(currentVersion, requestedChannel, config) + if (channelConfig) { + // Select feed URL from the corresponding mirror + const feedUrl = channelConfig.feedUrls[mirror] + this._setChannel(requestedChannel, feedUrl) + return + } + } + + // Fallback logic + const defaultFeedUrl = mirror === 'gitcode' + ? FeedUrl.PRODUCTION + : FeedUrl.GITHUB_LATEST + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) +} + +private async _fetchUpdateConfig(mirror: 'github' | 'gitcode'): Promise { + const configUrl = mirror === 'gitcode' + ? UpdateConfigUrl.GITCODE + : UpdateConfigUrl.GITHUB + + try { + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + 'Accept': 'application/json', + 'X-Client-Id': configManager.getClientId() + } + }) + return await response.json() as UpdateConfig + } catch (error) { + logger.error('Failed to fetch update config:', error) + return null + } +} +``` + +## Fallback and Error Handling Strategy + +1. **Configuration file fetch failure**: Log error, return current version, don't offer updates +2. **No matching version**: Notify user that current version doesn't support automatic upgrade +3. **Network exception**: Cache last successfully fetched configuration (optional) + +## GitHub Release Requirements + +To support intermediate version upgrades, the following files need to be retained: + +- **v1.7.0 release** and its latest*.yml files (as upgrade target for users below v1.7) +- Future intermediate versions (e.g., v2.8.0) need to retain corresponding release and latest*.yml files +- Complete installation packages for each version + +### Currently Required Releases + +| Version | Purpose | Must Retain | +|---------|---------|-------------| +| v1.7.0 | Upgrade target for users below 1.7 | ✅ Yes | +| v2.0.0-rc.1 | RC testing channel | ❌ Optional | +| v2.0.0-beta.1 | Beta testing channel | ❌ Optional | +| latest | Latest stable version (automatic) | ✅ Yes | + +## Advantages + +1. **Flexibility**: Supports arbitrarily complex upgrade paths +2. **Extensibility**: Adding new versions only requires adding new entries to the configuration file +3. **Maintainability**: Configuration is separated from code, allowing upgrade strategy adjustments without releasing new versions +4. **Multi-source support**: Automatically selects optimal configuration source based on geolocation +5. **Version control**: Enforces intermediate version upgrades, ensuring data migration and compatibility + +## Future Extensions + +- Support more granular version range control (e.g., `>=1.5.0 <1.8.0`) +- Support multi-step upgrade path hints (e.g., notify user needs 1.5 → 1.8 → 2.0) +- Support A/B testing and gradual rollout +- Support local caching and expiration strategy for configuration files diff --git a/docs/technical/app-upgrade-config-zh.md b/docs/technical/app-upgrade-config-zh.md new file mode 100644 index 000000000..d8812d494 --- /dev/null +++ b/docs/technical/app-upgrade-config-zh.md @@ -0,0 +1,430 @@ +# 更新配置系统设计文档 + +## 背景 + +当前 AppUpdater 直接请求 GitHub API 获取 beta 和 rc 的更新信息。为了支持国内用户,需要根据 IP 地理位置,分别从 GitHub/GitCode 获取一个固定的 JSON 配置文件,该文件包含所有渠道的更新地址。 + +## 设计目标 + +1. 支持根据 IP 地理位置选择不同的配置源(GitHub/GitCode) +2. 支持版本兼容性控制(如 v1.x 以下必须先升级到 v1.7.0 才能升级到 v2.0) +3. 易于扩展,支持未来多个主版本的升级路径(v1.6 → v1.7 → v2.0 → v2.8 → v3.0) +4. 保持与现有 electron-updater 机制的兼容性 + +## 当前版本策略 + +- **v1.7.x** 是 1.x 系列的最后版本 +- **v1.7.0 以下**的用户必须先升级到 v1.7.0(或更高的 1.7.x 版本) +- **v1.7.0 及以上**的用户可以直接升级到 v2.x.x + +## 自动化工作流 + +`cs-releases/app-upgrade-config.json` 由 [`Update App Upgrade Config`](../../.github/workflows/update-app-upgrade-config.yml) workflow 自动同步。工作流会调用 [`scripts/update-app-upgrade-config.ts`](../../scripts/update-app-upgrade-config.ts) 脚本,根据指定 tag 更新 `cs-releases` 分支上的配置文件。 + +### 触发条件 + +- **Release 事件(`release: released/prereleased`)** + - Draft release 会被忽略。 + - 当 GitHub 将 release 标记为 *prerelease* 时,tag 必须包含 `-beta`/`-rc`(可带序号),否则直接跳过。 + - 当 release 标记为稳定版时,tag 必须与 GitHub API 返回的最新稳定版本一致,防止发布历史 tag 时意外挂起工作流。 + - 满足上述条件后,工作流会根据语义化版本判断渠道(`latest`/`beta`/`rc`),并通过 `IS_PRERELEASE` 传递给脚本。 +- **手动触发(`workflow_dispatch`)** + - 必填:`tag`(例:`v2.0.1`);选填:`is_prerelease`(默认 `false`)。 + - 当 `is_prerelease=true` 时,同样要求 tag 带有 beta/rc 后缀。 + - 手动运行仍会请求 GitHub 最新 release 信息,用于在 PR 说明中标注该 tag 是否是最新稳定版。 + +### 工作流步骤 + +1. **检查与元数据准备**:`Check if should proceed` 和 `Prepare metadata` 步骤会计算 tag、prerelease 标志、是否最新版本以及用于分支名的 `safe_tag`。若任意校验失败,工作流立即退出。 +2. **检出分支**:默认分支被检出到 `main/`,长期维护的 `cs-releases` 分支则在 `cs/` 中,所有改动都发生在 `cs/`。 +3. **安装工具链**:安装 Node.js 22、启用 Corepack,并在 `main/` 目录执行 `yarn install --immutable`。 +4. **运行更新脚本**:执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json --is-prerelease `。 + - 脚本会标准化 tag(去掉 `v` 前缀等)、识别渠道、加载 `config/app-upgrade-segments.json` 中的分段规则。 + - 校验 prerelease 标志与语义后缀是否匹配、强制锁定的 segment 是否满足、生成镜像的下载地址,并检查 release 是否已经在 GitHub/GitCode 可用(latest 渠道在 GitCode 不可用时会回退到 `https://releases.cherry-ai.com`)。 + - 更新对应的渠道配置后,脚本会按 semver 排序写回 JSON,并刷新 `lastUpdated`。 +5. **检测变更并创建 PR**:若 `cs/app-upgrade-config.json` 有变更,则创建 `chore/update-app-upgrade-config/` 分支,提交信息为 `🤖 chore: sync app-upgrade-config for `,并向 `cs-releases` 提 PR;无变更则输出提示。 + +### 手动触发指南 + +1. 进入 Cherry Studio 仓库的 GitHub **Actions** 页面,选择 **Update App Upgrade Config** 工作流。 +2. 点击 **Run workflow**,保持默认分支(通常为 `main`),填写 `tag`(如 `v2.1.0`)。 +3. 只有在 tag 带 `-beta`/`-rc` 后缀时才勾选 `is_prerelease`,稳定版保持默认。 +4. 启动运行并等待完成,随后到 `cs-releases` 分支的 PR 查看 `app-upgrade-config.json` 的变更并在验证后合并。 + +## JSON 配置文件格式 + +### 文件位置 + +- **GitHub**: `https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/cs-releases/app-upgrade-config.json` +- **GitCode**: `https://gitcode.com/CherryHQ/cherry-studio/raw/cs-releases/app-upgrade-config.json` + +**说明**:两个镜像源提供相同的配置文件,统一托管在 `cs-releases` 分支上。客户端根据 IP 地理位置自动选择最优镜像源。 + +### 配置结构(当前实际配置) + +```json +{ + "lastUpdated": "2025-01-05T00:00:00Z", + "versions": { + "1.6.7": { + "minCompatibleVersion": "1.0.0", + "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "channels": { + "latest": { + "version": "1.6.7", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.7", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v1.6.7" + } + }, + "rc": { + "version": "1.6.0-rc.5", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.6.0-rc.5" + } + }, + "beta": { + "version": "1.6.7-beta.3", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3", + "gitcode": "https://github.com/CherryHQ/cherry-studio/releases/download/v1.7.0-beta.3" + } + } + } + }, + "2.0.0": { + "minCompatibleVersion": "1.7.0", + "description": "Major release v2.0 - required intermediate version for v2.x upgrades", + "channels": { + "latest": null, + "rc": null, + "beta": null + } + } + } +} +``` + +### 未来扩展示例 + +当需要发布 v3.0 时,如果需要强制用户先升级到 v2.8,可以添加: + +```json +{ + "2.8.0": { + "minCompatibleVersion": "2.0.0", + "description": "Stable v2.8 - required for v3 upgrade", + "channels": { + "latest": { + "version": "2.8.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v2.8.0", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v2.8.0" + } + }, + "rc": null, + "beta": null + } + }, + "3.0.0": { + "minCompatibleVersion": "2.8.0", + "description": "Major release v3.0", + "channels": { + "latest": { + "version": "3.0.0", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/latest", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/latest" + } + }, + "rc": { + "version": "3.0.0-rc.1", + "feedUrls": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1", + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/v3.0.0-rc.1" + } + }, + "beta": null + } + } +} +``` + +### 字段说明 + +- `lastUpdated`: 配置文件最后更新时间(ISO 8601 格式) +- `versions`: 版本配置对象,key 为版本号,按语义化版本排序 + - `minCompatibleVersion`: 可以升级到此版本的最低兼容版本 + - `description`: 版本描述 + - `channels`: 更新渠道配置 + - `latest`: 稳定版渠道 + - `rc`: Release Candidate 渠道 + - `beta`: Beta 测试渠道 + - 每个渠道包含: + - `version`: 该渠道的版本号 + - `feedUrls`: 多镜像源 URL 配置 + - `github`: GitHub 镜像源的 electron-updater feed URL + - `gitcode`: GitCode 镜像源的 electron-updater feed URL + - `metadata`: 自动化匹配所需的稳定标识 + - `segmentId`: 来自 `config/app-upgrade-segments.json` 的段位 ID + - `segmentType`: 可选字段(`legacy` | `breaking` | `latest`),便于文档/调试 + +## TypeScript 类型定义 + +```typescript +// 镜像源枚举 +enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } + metadata?: { + segmentId: string + segmentType?: 'legacy' | 'breaking' | 'latest' + } +} + +interface ChannelConfig { + version: string + feedUrls: Record + // 等同于: + // feedUrls: { + // github: string + // gitcode: string + // } +} +``` + +## 段位元数据(Break Change 标记) + +- 所有段位定义(如 `legacy-v1`、`gateway-v2` 等)集中在 `config/app-upgrade-segments.json`,用于描述匹配范围、`segmentId`、`segmentType`、默认 `minCompatibleVersion/description` 以及各渠道的 URL 模板。 +- `versions` 下的每个节点都会带上 `metadata.segmentId`。自动脚本始终依据该 ID 来定位并更新条目,即便 key 从 `2.1.5` 切换到 `2.1.6` 也不会错位。 +- 如果某段需要锁死在特定版本(例如 `2.0.0` 的 break change),可在段定义中设置 `segmentType: "breaking"` 并提供 `lockedVersion`,脚本在遇到不匹配的 tag 时会短路报错,保证升级路径安全。 +- 面对未来新的断层(例如 `3.0.0`),只需要在段定义里新增一段,自动化即可识别并更新。 + +## 自动化工作流 + +`.github/workflows/update-app-upgrade-config.yml` 会在 GitHub Release(包含正常发布与 Pre Release)触发: + +1. 同时 Checkout 仓库默认分支(用于脚本)和 `cs-releases` 分支(真实托管配置的分支)。 +2. 在默认分支目录执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag --config ../cs/app-upgrade-config.json`,直接重写 `cs-releases` 分支里的配置文件。 +3. 如果 `app-upgrade-config.json` 有变化,则通过 `peter-evans/create-pull-request` 自动创建一个指向 `cs-releases` 的 PR,Diff 仅包含该文件。 + +如需本地调试,可执行 `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json`(加 `--dry-run` 仅打印结果)来复现 CI 行为。若需要暂时跳过 GitHub/GitCode Release 页面是否就绪的校验,可在 `--dry-run` 的同时附加 `--skip-release-checks`。不加 `--config` 时默认更新当前工作目录(通常是 main 分支)下的副本,方便文档/审查。 + +## 版本匹配逻辑 + +### 算法流程 + +1. 获取用户当前版本(`currentVersion`)和请求的渠道(`requestedChannel`) +2. 获取配置文件中所有版本号,按语义化版本从大到小排序 +3. 遍历排序后的版本列表: + - 检查 `currentVersion >= minCompatibleVersion` + - 检查请求的 `channel` 是否存在且不为 `null` + - 如果满足条件,返回该渠道配置 +4. 如果没有找到匹配版本,返回 `null` + +### 伪代码实现 + +```typescript +function findCompatibleVersion( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig +): ChannelConfig | null { + // 获取所有版本号并从大到小排序 + const versions = Object.keys(config.versions).sort(semver.rcompare) + + for (const versionKey of versions) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + + // 检查版本兼容性和渠道可用性 + if ( + semver.gte(currentVersion, versionConfig.minCompatibleVersion) && + channelConfig !== null + ) { + return channelConfig + } + } + + return null // 没有找到兼容版本 +} +``` + +## 升级路径示例 + +### 场景 1: v1.6.5 用户升级(低于 1.7) + +- **当前版本**: 1.6.5 +- **请求渠道**: latest +- **匹配结果**: 1.7.0 +- **原因**: 1.6.5 >= 0.0.0(满足 1.7.0 的 minCompatibleVersion),但不满足 2.0.0 的 minCompatibleVersion (1.7.0) +- **操作**: 提示用户升级到 1.7.0,这是升级到 v2.x 的必要中间版本 + +### 场景 2: v1.6.5 用户请求 rc/beta + +- **当前版本**: 1.6.5 +- **请求渠道**: rc 或 beta +- **匹配结果**: 1.7.0 (latest) +- **原因**: 1.7.0 版本不提供 rc/beta 渠道(值为 null) +- **操作**: 升级到 1.7.0 稳定版 + +### 场景 3: v1.7.0 用户升级到最新版 + +- **当前版本**: 1.7.0 +- **请求渠道**: latest +- **匹配结果**: 2.0.0 +- **原因**: 1.7.0 >= 1.7.0(满足 2.0.0 的 minCompatibleVersion) +- **操作**: 直接升级到 2.0.0(当前最新稳定版) + +### 场景 4: v1.7.2 用户升级到 RC 版本 + +- **当前版本**: 1.7.2 +- **请求渠道**: rc +- **匹配结果**: 2.0.0-rc.1 +- **原因**: 1.7.2 >= 1.7.0(满足 2.0.0 的 minCompatibleVersion),且 rc 渠道存在 +- **操作**: 升级到 2.0.0-rc.1 + +### 场景 5: v1.7.0 用户升级到 Beta 版本 + +- **当前版本**: 1.7.0 +- **请求渠道**: beta +- **匹配结果**: 2.0.0-beta.1 +- **原因**: 1.7.0 >= 1.7.0,且 beta 渠道存在 +- **操作**: 升级到 2.0.0-beta.1 + +### 场景 6: v2.5.0 用户升级(未来) + +假设已添加 v2.8.0 和 v3.0.0 配置: +- **当前版本**: 2.5.0 +- **请求渠道**: latest +- **匹配结果**: 2.8.0 +- **原因**: 2.5.0 >= 2.0.0(满足 2.8.0 的 minCompatibleVersion),但不满足 3.0.0 的要求 +- **操作**: 提示用户升级到 2.8.0,这是升级到 v3.x 的必要中间版本 + +## 代码改动计划 + +### 主要修改 + +1. **新增方法** + - `_fetchUpdateConfig(ipCountry: string): Promise` - 根据 IP 获取配置文件 + - `_findCompatibleChannel(currentVersion: string, channel: UpgradeChannel, config: UpdateConfig): ChannelConfig | null` - 查找兼容的渠道配置 + +2. **修改方法** + - `_getReleaseVersionFromGithub()` → 移除或重构为 `_getChannelFeedUrl()` + - `_setFeedUrl()` - 使用新的配置系统替代现有逻辑 + +3. **新增类型定义** + - `UpdateConfig` + - `VersionConfig` + - `ChannelConfig` + +### 镜像源选择逻辑 + +客户端根据 IP 地理位置自动选择最优镜像源: + +```typescript +private async _setFeedUrl() { + const currentVersion = app.getVersion() + const testPlan = configManager.getTestPlan() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST + + // 根据 IP 国家确定镜像源 + const ipCountry = await getIpCountry() + const mirror = ipCountry.toLowerCase() === 'cn' ? 'gitcode' : 'github' + + // 获取更新配置 + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + const channelConfig = this._findCompatibleChannel(currentVersion, requestedChannel, config) + if (channelConfig) { + // 从配置中选择对应镜像源的 URL + const feedUrl = channelConfig.feedUrls[mirror] + this._setChannel(requestedChannel, feedUrl) + return + } + } + + // Fallback 逻辑 + const defaultFeedUrl = mirror === 'gitcode' + ? FeedUrl.PRODUCTION + : FeedUrl.GITHUB_LATEST + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) +} + +private async _fetchUpdateConfig(mirror: 'github' | 'gitcode'): Promise { + const configUrl = mirror === 'gitcode' + ? UpdateConfigUrl.GITCODE + : UpdateConfigUrl.GITHUB + + try { + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + 'Accept': 'application/json', + 'X-Client-Id': configManager.getClientId() + } + }) + return await response.json() as UpdateConfig + } catch (error) { + logger.error('Failed to fetch update config:', error) + return null + } +} +``` + +## 降级和容错策略 + +1. **配置文件获取失败**: 记录错误日志,返回当前版本,不提供更新 +2. **没有匹配的版本**: 提示用户当前版本不支持自动升级 +3. **网络异常**: 缓存上次成功获取的配置(可选) + +## GitHub Release 要求 + +为支持中间版本升级,需要保留以下文件: + +- **v1.7.0 release** 及其 latest*.yml 文件(作为 v1.7 以下用户的升级目标) +- 未来如需强制中间版本(如 v2.8.0),需要保留对应的 release 和 latest*.yml 文件 +- 各版本的完整安装包 + +### 当前需要的 Release + +| 版本 | 用途 | 必须保留 | +|------|------|---------| +| v1.7.0 | 1.7 以下用户的升级目标 | ✅ 是 | +| v2.0.0-rc.1 | RC 测试渠道 | ❌ 可选 | +| v2.0.0-beta.1 | Beta 测试渠道 | ❌ 可选 | +| latest | 最新稳定版(自动) | ✅ 是 | + +## 优势 + +1. **灵活性**: 支持任意复杂的升级路径 +2. **可扩展性**: 新增版本只需在配置文件中添加新条目 +3. **可维护性**: 配置与代码分离,无需发版即可调整升级策略 +4. **多源支持**: 自动根据地理位置选择最优配置源 +5. **版本控制**: 强制中间版本升级,确保数据迁移和兼容性 + +## 未来扩展 + +- 支持更细粒度的版本范围控制(如 `>=1.5.0 <1.8.0`) +- 支持多步升级路径提示(如提示用户需要 1.5 → 1.8 → 2.0) +- 支持 A/B 测试和灰度发布 +- 支持配置文件的本地缓存和过期策略 diff --git a/electron-builder.yml b/electron-builder.yml index 291817915..802c0d2c6 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -97,7 +97,6 @@ mac: entitlementsInherit: build/entitlements.mac.plist notarize: false artifactName: ${productName}-${version}-${arch}.${ext} - minimumSystemVersion: "20.1.0" # 最低支持 macOS 11.0 extendInfo: - NSCameraUsageDescription: Application requests access to the device's camera. - NSMicrophoneUsageDescription: Application requests access to the device's microphone. diff --git a/package.json b/package.json index 537d708f9..2a0b557c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.7.0-beta.3", + "version": "1.6.5", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -58,6 +58,7 @@ "update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts", "auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts", "update:languages": "tsx scripts/update-languages.ts", + "update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts", "test": "vitest run --silent", "test:main": "vitest run --project main", "test:renderer": "vitest run --project renderer", @@ -260,11 +261,11 @@ "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.5", "electron": "38.7.0", - "electron-builder": "26.0.15", + "electron-builder": "26.1.0", "electron-devtools-installer": "^3.2.0", "electron-reload": "^2.0.0-alpha.1", "electron-store": "^8.2.0", - "electron-updater": "6.6.4", + "electron-updater": "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch", "electron-vite": "4.0.1", "electron-window-state": "^5.0.3", "emittery": "^1.0.3", @@ -381,8 +382,6 @@ "@codemirror/lint": "6.8.5", "@codemirror/view": "6.38.1", "@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", - "app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch", - "app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch", "atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch", "esbuild": "^0.25.0", "file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch", diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 9d9240223..e3d7af823 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -197,12 +197,22 @@ export enum FeedUrl { GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download' } +export enum UpdateConfigUrl { + GITHUB = 'https://raw.githubusercontent.com/CherryHQ/cherry-studio/refs/heads/cs-releases/app-upgrade-config.json', + GITCODE = 'https://raw.gitcode.com/CherryHQ/cherry-studio/raw/cs-releases/app-upgrade-config.json' +} + export enum UpgradeChannel { LATEST = 'latest', // 最新稳定版本 RC = 'rc', // 公测版本 BETA = 'beta' // 预览版本 } +export enum UpdateMirror { + GITHUB = 'github', + GITCODE = 'gitcode' +} + export const defaultTimeout = 10 * 1000 * 60 export const occupiedDirs = ['logs', 'Network', 'Partitions/webview/Network'] diff --git a/scripts/update-app-upgrade-config.ts b/scripts/update-app-upgrade-config.ts new file mode 100644 index 000000000..4fcfa647f --- /dev/null +++ b/scripts/update-app-upgrade-config.ts @@ -0,0 +1,532 @@ +import fs from 'fs/promises' +import path from 'path' +import semver from 'semver' + +type UpgradeChannel = 'latest' | 'rc' | 'beta' +type UpdateMirror = 'github' | 'gitcode' + +const CHANNELS: UpgradeChannel[] = ['latest', 'rc', 'beta'] +const MIRRORS: UpdateMirror[] = ['github', 'gitcode'] +const GITHUB_REPO = 'CherryHQ/cherry-studio' +const GITCODE_REPO = 'CherryHQ/cherry-studio' +const DEFAULT_FEED_TEMPLATES: Record = { + github: `https://github.com/${GITHUB_REPO}/releases/download/{{tag}}`, + gitcode: `https://gitcode.com/${GITCODE_REPO}/releases/download/{{tag}}` +} +const GITCODE_LATEST_FALLBACK = 'https://releases.cherry-ai.com' + +interface CliOptions { + tag?: string + configPath?: string + segmentsPath?: string + dryRun?: boolean + skipReleaseChecks?: boolean + isPrerelease?: boolean +} + +interface ChannelTemplateConfig { + feedTemplates?: Partial> +} + +interface SegmentMatchRule { + range?: string + exact?: string[] + excludeExact?: string[] +} + +interface SegmentDefinition { + id: string + type: 'legacy' | 'breaking' | 'latest' + match: SegmentMatchRule + lockedVersion?: string + minCompatibleVersion: string + description: string + channelTemplates?: Partial> +} + +interface SegmentMetadataFile { + segments: SegmentDefinition[] +} + +interface ChannelConfig { + version: string + feedUrls: Record +} + +interface VersionMetadata { + segmentId: string + segmentType?: string +} + +interface VersionEntry { + metadata?: VersionMetadata + minCompatibleVersion: string + description: string + channels: Record +} + +interface UpgradeConfigFile { + lastUpdated: string + versions: Record +} + +interface ReleaseInfo { + tag: string + version: string + channel: UpgradeChannel +} + +interface UpdateVersionsResult { + versions: Record + updated: boolean +} + +const ROOT_DIR = path.resolve(__dirname, '..') +const DEFAULT_CONFIG_PATH = path.join(ROOT_DIR, 'app-upgrade-config.json') +const DEFAULT_SEGMENTS_PATH = path.join(ROOT_DIR, 'config/app-upgrade-segments.json') + +async function main() { + const options = parseArgs() + const releaseTag = resolveTag(options) + const normalizedVersion = normalizeVersion(releaseTag) + const releaseChannel = detectChannel(normalizedVersion) + if (!releaseChannel) { + console.warn(`[update-app-upgrade-config] Tag ${normalizedVersion} does not map to beta/rc/latest. Skipping.`) + return + } + + // Validate version format matches prerelease status + if (options.isPrerelease !== undefined) { + const hasPrereleaseSuffix = releaseChannel === 'beta' || releaseChannel === 'rc' + + if (options.isPrerelease && !hasPrereleaseSuffix) { + console.warn( + `[update-app-upgrade-config] ⚠️ Release marked as prerelease but version ${normalizedVersion} has no beta/rc suffix. Skipping.` + ) + return + } + + if (!options.isPrerelease && hasPrereleaseSuffix) { + console.warn( + `[update-app-upgrade-config] ⚠️ Release marked as latest but version ${normalizedVersion} has prerelease suffix (${releaseChannel}). Skipping.` + ) + return + } + } + + const [config, segmentFile] = await Promise.all([ + readJson(options.configPath ?? DEFAULT_CONFIG_PATH), + readJson(options.segmentsPath ?? DEFAULT_SEGMENTS_PATH) + ]) + + const segment = pickSegment(segmentFile.segments, normalizedVersion) + if (!segment) { + throw new Error(`Unable to find upgrade segment for version ${normalizedVersion}`) + } + + if (segment.lockedVersion && segment.lockedVersion !== normalizedVersion) { + throw new Error(`Segment ${segment.id} is locked to ${segment.lockedVersion}, but received ${normalizedVersion}`) + } + + const releaseInfo: ReleaseInfo = { + tag: formatTag(releaseTag), + version: normalizedVersion, + channel: releaseChannel + } + + const { versions: updatedVersions, updated } = await updateVersions( + config.versions, + segment, + releaseInfo, + Boolean(options.skipReleaseChecks) + ) + + if (!updated) { + throw new Error( + `[update-app-upgrade-config] Feed URLs are not ready for ${releaseInfo.version} (${releaseInfo.channel}). Try again after the release mirrors finish syncing.` + ) + } + + const updatedConfig: UpgradeConfigFile = { + ...config, + lastUpdated: new Date().toISOString(), + versions: updatedVersions + } + + const output = JSON.stringify(updatedConfig, null, 2) + '\n' + + if (options.dryRun) { + console.log('Dry run enabled. Generated configuration:\n') + console.log(output) + return + } + + await fs.writeFile(options.configPath ?? DEFAULT_CONFIG_PATH, output, 'utf-8') + console.log( + `✅ Updated ${path.relative(process.cwd(), options.configPath ?? DEFAULT_CONFIG_PATH)} for ${segment.id} (${releaseInfo.channel}) -> ${releaseInfo.version}` + ) +} + +function parseArgs(): CliOptions { + const args = process.argv.slice(2) + const options: CliOptions = {} + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i] + if (arg === '--tag') { + options.tag = args[i + 1] + i += 1 + } else if (arg === '--config') { + options.configPath = args[i + 1] + i += 1 + } else if (arg === '--segments') { + options.segmentsPath = args[i + 1] + i += 1 + } else if (arg === '--dry-run') { + options.dryRun = true + } else if (arg === '--skip-release-checks') { + options.skipReleaseChecks = true + } else if (arg === '--is-prerelease') { + options.isPrerelease = args[i + 1] === 'true' + i += 1 + } else if (arg === '--help') { + printHelp() + process.exit(0) + } else { + console.warn(`Ignoring unknown argument "${arg}"`) + } + } + + if (options.skipReleaseChecks && !options.dryRun) { + throw new Error('--skip-release-checks can only be used together with --dry-run') + } + + return options +} + +function printHelp() { + console.log(`Usage: tsx scripts/update-app-upgrade-config.ts [options] + +Options: + --tag Release tag (e.g. v2.1.6). Falls back to GITHUB_REF_NAME/RELEASE_TAG. + --config Path to app-upgrade-config.json. + --segments Path to app-upgrade-segments.json. + --is-prerelease Whether this is a prerelease (validates version format). + --dry-run Print the result without writing to disk. + --skip-release-checks Skip release page availability checks (only valid with --dry-run). + --help Show this help message.`) +} + +function resolveTag(options: CliOptions): string { + const envTag = process.env.RELEASE_TAG ?? process.env.GITHUB_REF_NAME ?? process.env.TAG_NAME + const tag = options.tag ?? envTag + + if (!tag) { + throw new Error('A release tag is required. Pass --tag or set RELEASE_TAG/GITHUB_REF_NAME.') + } + + return tag +} + +function normalizeVersion(tag: string): string { + const cleaned = semver.clean(tag, { loose: true }) + if (!cleaned) { + throw new Error(`Tag "${tag}" is not a valid semantic version`) + } + + const valid = semver.valid(cleaned, { loose: true }) + if (!valid) { + throw new Error(`Unable to normalize tag "${tag}" to a valid semantic version`) + } + + return valid +} + +function detectChannel(version: string): UpgradeChannel | null { + const parsed = semver.parse(version, { loose: true, includePrerelease: true }) + if (!parsed) { + return null + } + + if (parsed.prerelease.length === 0) { + return 'latest' + } + + const label = String(parsed.prerelease[0]).toLowerCase() + if (label === 'beta') { + return 'beta' + } + if (label === 'rc') { + return 'rc' + } + + return null +} + +async function readJson(filePath: string): Promise { + const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(filePath) + const data = await fs.readFile(absolute, 'utf-8') + return JSON.parse(data) as T +} + +function pickSegment(segments: SegmentDefinition[], version: string): SegmentDefinition | null { + for (const segment of segments) { + if (matchesSegment(segment.match, version)) { + return segment + } + } + return null +} + +function matchesSegment(matchRule: SegmentMatchRule, version: string): boolean { + if (matchRule.exact && matchRule.exact.includes(version)) { + return true + } + + if (matchRule.excludeExact && matchRule.excludeExact.includes(version)) { + return false + } + + if (matchRule.range && !semver.satisfies(version, matchRule.range, { includePrerelease: true })) { + return false + } + + if (matchRule.exact) { + return matchRule.exact.includes(version) + } + + return Boolean(matchRule.range) +} + +function formatTag(tag: string): string { + if (tag.startsWith('refs/tags/')) { + return tag.replace('refs/tags/', '') + } + return tag +} + +async function updateVersions( + versions: Record, + segment: SegmentDefinition, + releaseInfo: ReleaseInfo, + skipReleaseValidation: boolean +): Promise { + const versionsCopy: Record = { ...versions } + const existingKey = findVersionKeyBySegment(versionsCopy, segment.id) + const targetKey = resolveVersionKey(existingKey, segment, releaseInfo) + const shouldRename = existingKey && existingKey !== targetKey + + let entry: VersionEntry + if (existingKey) { + entry = { ...versionsCopy[existingKey], channels: { ...versionsCopy[existingKey].channels } } + } else { + entry = createEmptyVersionEntry() + } + + entry.channels = ensureChannelSlots(entry.channels) + + const channelUpdated = await applyChannelUpdate(entry, segment, releaseInfo, skipReleaseValidation) + if (!channelUpdated) { + return { versions, updated: false } + } + + if (shouldRename && existingKey) { + delete versionsCopy[existingKey] + } + + entry.metadata = { + segmentId: segment.id, + segmentType: segment.type + } + entry.minCompatibleVersion = segment.minCompatibleVersion + entry.description = segment.description + + versionsCopy[targetKey] = entry + return { + versions: sortVersionMap(versionsCopy), + updated: true + } +} + +function findVersionKeyBySegment(versions: Record, segmentId: string): string | null { + for (const [key, value] of Object.entries(versions)) { + if (value.metadata?.segmentId === segmentId) { + return key + } + } + return null +} + +function resolveVersionKey(existingKey: string | null, segment: SegmentDefinition, releaseInfo: ReleaseInfo): string { + if (segment.lockedVersion) { + return segment.lockedVersion + } + + if (releaseInfo.channel === 'latest') { + return releaseInfo.version + } + + if (existingKey) { + return existingKey + } + + const baseVersion = getBaseVersion(releaseInfo.version) + return baseVersion ?? releaseInfo.version +} + +function getBaseVersion(version: string): string | null { + const parsed = semver.parse(version, { loose: true, includePrerelease: true }) + if (!parsed) { + return null + } + return `${parsed.major}.${parsed.minor}.${parsed.patch}` +} + +function createEmptyVersionEntry(): VersionEntry { + return { + minCompatibleVersion: '', + description: '', + channels: { + latest: null, + rc: null, + beta: null + } + } +} + +function ensureChannelSlots( + channels: Record +): Record { + return CHANNELS.reduce( + (acc, channel) => { + acc[channel] = channels[channel] ?? null + return acc + }, + {} as Record + ) +} + +async function applyChannelUpdate( + entry: VersionEntry, + segment: SegmentDefinition, + releaseInfo: ReleaseInfo, + skipReleaseValidation: boolean +): Promise { + if (!CHANNELS.includes(releaseInfo.channel)) { + throw new Error(`Unsupported channel "${releaseInfo.channel}"`) + } + + const feedUrls = buildFeedUrls(segment, releaseInfo) + + if (skipReleaseValidation) { + console.warn( + `[update-app-upgrade-config] Skipping release availability validation for ${releaseInfo.version} (${releaseInfo.channel}).` + ) + } else { + const availability = await ensureReleaseAvailability(releaseInfo) + if (!availability.github) { + return false + } + if (releaseInfo.channel === 'latest' && !availability.gitcode) { + console.warn( + `[update-app-upgrade-config] gitcode release page not ready for ${releaseInfo.tag}. Falling back to ${GITCODE_LATEST_FALLBACK}.` + ) + feedUrls.gitcode = GITCODE_LATEST_FALLBACK + } + } + + entry.channels[releaseInfo.channel] = { + version: releaseInfo.version, + feedUrls + } + + return true +} + +function buildFeedUrls(segment: SegmentDefinition, releaseInfo: ReleaseInfo): Record { + return MIRRORS.reduce( + (acc, mirror) => { + const template = resolveFeedTemplate(segment, releaseInfo, mirror) + acc[mirror] = applyTemplate(template, releaseInfo) + return acc + }, + {} as Record + ) +} + +function resolveFeedTemplate(segment: SegmentDefinition, releaseInfo: ReleaseInfo, mirror: UpdateMirror): string { + if (mirror === 'gitcode' && releaseInfo.channel !== 'latest') { + return segment.channelTemplates?.[releaseInfo.channel]?.feedTemplates?.github ?? DEFAULT_FEED_TEMPLATES.github + } + + return segment.channelTemplates?.[releaseInfo.channel]?.feedTemplates?.[mirror] ?? DEFAULT_FEED_TEMPLATES[mirror] +} + +function applyTemplate(template: string, releaseInfo: ReleaseInfo): string { + return template.replace(/{{\s*tag\s*}}/gi, releaseInfo.tag).replace(/{{\s*version\s*}}/gi, releaseInfo.version) +} + +function sortVersionMap(versions: Record): Record { + const sorted = Object.entries(versions).sort(([a], [b]) => semver.rcompare(a, b)) + return sorted.reduce( + (acc, [version, entry]) => { + acc[version] = entry + return acc + }, + {} as Record + ) +} + +interface ReleaseAvailability { + github: boolean + gitcode: boolean +} + +async function ensureReleaseAvailability(releaseInfo: ReleaseInfo): Promise { + const mirrorsToCheck: UpdateMirror[] = releaseInfo.channel === 'latest' ? MIRRORS : ['github'] + const availability: ReleaseAvailability = { + github: false, + gitcode: releaseInfo.channel === 'latest' ? false : true + } + + for (const mirror of mirrorsToCheck) { + const url = getReleasePageUrl(mirror, releaseInfo.tag) + try { + const response = await fetch(url, { + method: mirror === 'github' ? 'HEAD' : 'GET', + redirect: 'follow' + }) + + if (response.ok) { + availability[mirror] = true + } else { + console.warn( + `[update-app-upgrade-config] ${mirror} release not available for ${releaseInfo.tag} (status ${response.status}, ${url}).` + ) + availability[mirror] = false + } + } catch (error) { + console.warn( + `[update-app-upgrade-config] Failed to verify ${mirror} release page for ${releaseInfo.tag} (${url}). Continuing.`, + error + ) + availability[mirror] = false + } + } + + return availability +} + +function getReleasePageUrl(mirror: UpdateMirror, tag: string): string { + if (mirror === 'github') { + return `https://github.com/${GITHUB_REPO}/releases/tag/${encodeURIComponent(tag)}` + } + // Use latest.yml download URL for GitCode to check if release exists + // Note: GitCode returns 401 for HEAD requests, so we use GET in ensureReleaseAvailability + return `https://gitcode.com/${GITCODE_REPO}/releases/download/${encodeURIComponent(tag)}/latest.yml` +} + +main().catch((error) => { + console.error('❌ Failed to update app-upgrade-config:', error) + process.exit(1) +}) diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 168084bd3..57dc3fb2a 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -2,7 +2,7 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' import { generateUserAgent } from '@main/utils/systemInfo' -import { FeedUrl, UpgradeChannel } from '@shared/config/constant' +import { FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import type { UpdateInfo } from 'builder-util-runtime' import { CancellationToken } from 'builder-util-runtime' @@ -22,7 +22,29 @@ const LANG_MARKERS = { EN_START: '', ZH_CN_START: '', END: '' -} as const +} + +interface UpdateConfig { + lastUpdated: string + versions: { + [versionKey: string]: VersionConfig + } +} + +interface VersionConfig { + minCompatibleVersion: string + description: string + channels: { + latest: ChannelConfig | null + rc: ChannelConfig | null + beta: ChannelConfig | null + } +} + +interface ChannelConfig { + version: string + feedUrls: Record +} export default class AppUpdater { autoUpdater: _AppUpdater = autoUpdater @@ -37,7 +59,9 @@ export default class AppUpdater { autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, 'User-Agent': generateUserAgent(), - 'X-Client-Id': configManager.getClientId() + 'X-Client-Id': configManager.getClientId(), + // no-cache + 'Cache-Control': 'no-cache' } autoUpdater.on('error', (error) => { @@ -75,61 +99,6 @@ export default class AppUpdater { this.autoUpdater = autoUpdater } - private async _getReleaseVersionFromGithub(channel: UpgradeChannel) { - const headers = { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - 'Accept-Language': 'en-US,en;q=0.9' - } - try { - logger.info(`get release version from github: ${channel}`) - const responses = await net.fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', { - headers - }) - const data = (await responses.json()) as GithubReleaseInfo[] - let mightHaveLatest = false - const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { - if (!item.draft && !item.prerelease) { - mightHaveLatest = true - } - - return item.prerelease && item.tag_name.includes(`-${channel}.`) - }) - - if (!release) { - return null - } - - // if the release version is the same as the current version, return null - if (release.tag_name === app.getVersion()) { - return null - } - - if (mightHaveLatest) { - logger.info(`might have latest release, get latest release`) - const latestReleaseResponse = await net.fetch( - 'https://api.github.com/repos/CherryHQ/cherry-studio/releases/latest', - { - headers - } - ) - const latestRelease = (await latestReleaseResponse.json()) as GithubReleaseInfo - if (semver.gt(latestRelease.tag_name, release.tag_name)) { - logger.info( - `latest release version is ${latestRelease.tag_name}, prerelease version is ${release.tag_name}, return null` - ) - return null - } - } - - logger.info(`release url is ${release.tag_name}, set channel to ${channel}`) - return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}` - } catch (error) { - logger.error('Failed to get latest not draft version from github:', error as Error) - return null - } - } - public setAutoUpdate(isActive: boolean) { autoUpdater.autoDownload = isActive autoUpdater.autoInstallOnAppQuit = isActive @@ -161,6 +130,88 @@ export default class AppUpdater { return UpgradeChannel.LATEST } + /** + * Fetch update configuration from GitHub or GitCode based on mirror + * @param mirror - Mirror to fetch config from + * @returns UpdateConfig object or null if fetch fails + */ + private async _fetchUpdateConfig(mirror: UpdateMirror): Promise { + const configUrl = mirror === UpdateMirror.GITCODE ? UpdateConfigUrl.GITCODE : UpdateConfigUrl.GITHUB + + try { + logger.info(`Fetching update config from ${configUrl} (mirror: ${mirror})`) + const response = await net.fetch(configUrl, { + headers: { + 'User-Agent': generateUserAgent(), + Accept: 'application/json', + 'X-Client-Id': configManager.getClientId(), + // no-cache + 'Cache-Control': 'no-cache' + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const config = (await response.json()) as UpdateConfig + logger.info(`Update config fetched successfully, last updated: ${config.lastUpdated}`) + return config + } catch (error) { + logger.error('Failed to fetch update config:', error as Error) + return null + } + } + + /** + * Find compatible channel configuration based on current version + * @param currentVersion - Current app version + * @param requestedChannel - Requested upgrade channel (latest/rc/beta) + * @param config - Update configuration object + * @returns Object containing ChannelConfig and actual channel if found, null otherwise + */ + private _findCompatibleChannel( + currentVersion: string, + requestedChannel: UpgradeChannel, + config: UpdateConfig + ): { config: ChannelConfig; channel: UpgradeChannel } | null { + // Get all version keys and sort descending (newest first) + const versionKeys = Object.keys(config.versions).sort(semver.rcompare) + + logger.info( + `Finding compatible channel for version ${currentVersion}, requested channel: ${requestedChannel}, available versions: ${versionKeys.join(', ')}` + ) + + for (const versionKey of versionKeys) { + const versionConfig = config.versions[versionKey] + const channelConfig = versionConfig.channels[requestedChannel] + const latestChannelConfig = versionConfig.channels[UpgradeChannel.LATEST] + + // Check version compatibility and channel availability + if (semver.gte(currentVersion, versionConfig.minCompatibleVersion) && channelConfig !== null) { + logger.info( + `Found compatible version: ${versionKey} (minCompatibleVersion: ${versionConfig.minCompatibleVersion}), version: ${channelConfig.version}` + ) + + if ( + requestedChannel !== UpgradeChannel.LATEST && + latestChannelConfig && + semver.gte(latestChannelConfig.version, channelConfig.version) + ) { + logger.info( + `latest channel version is greater than the requested channel version: ${latestChannelConfig.version} > ${channelConfig.version}, using latest instead` + ) + return { config: latestChannelConfig, channel: UpgradeChannel.LATEST } + } + + return { config: channelConfig, channel: requestedChannel } + } + } + + logger.warn(`No compatible channel found for version ${currentVersion} and channel ${requestedChannel}`) + return null + } + private _setChannel(channel: UpgradeChannel, feedUrl: string) { this.autoUpdater.channel = channel this.autoUpdater.setFeedURL(feedUrl) @@ -172,33 +223,42 @@ export default class AppUpdater { } private async _setFeedUrl() { + const currentVersion = app.getVersion() const testPlan = configManager.getTestPlan() - if (testPlan) { - const channel = this._getTestChannel() + const requestedChannel = testPlan ? this._getTestChannel() : UpgradeChannel.LATEST - if (channel === UpgradeChannel.LATEST) { - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) - return - } - - const releaseUrl = await this._getReleaseVersionFromGithub(channel) - if (releaseUrl) { - logger.info(`release url is ${releaseUrl}, set channel to ${channel}`) - this._setChannel(channel, releaseUrl) - return - } - - // if no prerelease url, use github latest to get release - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) - return - } - - this._setChannel(UpgradeChannel.LATEST, FeedUrl.PRODUCTION) + // Determine mirror based on IP country const ipCountry = await getIpCountry() - logger.info(`ipCountry is ${ipCountry}, set channel to ${UpgradeChannel.LATEST}`) - if (ipCountry.toLowerCase() !== 'cn') { - this._setChannel(UpgradeChannel.LATEST, FeedUrl.GITHUB_LATEST) + const mirror = ipCountry.toLowerCase() === 'cn' ? UpdateMirror.GITCODE : UpdateMirror.GITHUB + + logger.info( + `Setting feed URL for version ${currentVersion}, testPlan: ${testPlan}, requested channel: ${requestedChannel}, mirror: ${mirror} (IP country: ${ipCountry})` + ) + + // Try to fetch update config from remote + const config = await this._fetchUpdateConfig(mirror) + + if (config) { + // Use new config-based system + const result = this._findCompatibleChannel(currentVersion, requestedChannel, config) + + if (result) { + const { config: channelConfig, channel: actualChannel } = result + const feedUrl = channelConfig.feedUrls[mirror] + logger.info( + `Using config-based feed URL: ${feedUrl} for channel ${actualChannel} (requested: ${requestedChannel}, mirror: ${mirror})` + ) + this._setChannel(actualChannel, feedUrl) + return + } } + + logger.info('Failed to fetch update config, falling back to default feed URL') + // Fallback: use default feed URL based on mirror + const defaultFeedUrl = mirror === UpdateMirror.GITCODE ? FeedUrl.PRODUCTION : FeedUrl.GITHUB_LATEST + + logger.info(`Using fallback feed URL: ${defaultFeedUrl}`) + this._setChannel(UpgradeChannel.LATEST, defaultFeedUrl) } public cancelDownload() { @@ -320,8 +380,3 @@ export default class AppUpdater { return processedInfo } } -interface GithubReleaseInfo { - draft: boolean - prerelease: boolean - tag_name: string -} diff --git a/src/main/services/__tests__/AppUpdater.test.ts b/src/main/services/__tests__/AppUpdater.test.ts index 1be0e2f48..f7de00475 100644 --- a/src/main/services/__tests__/AppUpdater.test.ts +++ b/src/main/services/__tests__/AppUpdater.test.ts @@ -85,6 +85,9 @@ vi.mock('electron-updater', () => ({ })) // Import after mocks +import { UpdateMirror } from '@shared/config/constant' +import { app, net } from 'electron' + import AppUpdater from '../AppUpdater' import { configManager } from '../ConfigManager' @@ -274,4 +277,711 @@ describe('AppUpdater', () => { expect(result.releaseNotes).toBeNull() }) }) + + describe('_fetchUpdateConfig', () => { + const mockConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'Test version', + channels: { + latest: { + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: null + } + } + } + } + + it('should fetch config from GitHub mirror', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: true, + json: async () => mockConfig + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toEqual(mockConfig) + expect(net.fetch).toHaveBeenCalledWith(expect.stringContaining('github'), expect.any(Object)) + }) + + it('should fetch config from GitCode mirror', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: true, + json: async () => mockConfig + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITCODE) + + expect(result).toEqual(mockConfig) + // GitCode URL may vary, just check that fetch was called + expect(net.fetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + }) + + it('should return null on HTTP error', async () => { + vi.mocked(net.fetch).mockResolvedValue({ + ok: false, + status: 404 + } as any) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toBeNull() + }) + + it('should return null on network error', async () => { + vi.mocked(net.fetch).mockRejectedValue(new Error('Network error')) + + const result = await (appUpdater as any)._fetchUpdateConfig(UpdateMirror.GITHUB) + + expect(result).toBeNull() + }) + }) + + describe('_findCompatibleChannel', () => { + const mockConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'v1.6.7', + channels: { + latest: { + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: { + version: '1.7.0-beta.3', + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + } + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'v2.0.0', + channels: { + latest: null, + rc: null, + beta: null + } + } + } + } + + it('should find compatible latest channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'latest', mockConfig) + + expect(result?.config).toEqual({ + version: '1.6.7', + feedUrls: { + github: 'https://github.com/test/v1.6.7', + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + expect(result?.channel).toBe('latest') + }) + + it('should find compatible rc channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'rc', mockConfig) + + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + expect(result?.channel).toBe('rc') + }) + + it('should find compatible beta channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.5.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'beta', mockConfig) + + expect(result?.config).toEqual({ + version: '1.7.0-beta.3', + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + }) + expect(result?.channel).toBe('beta') + }) + + it('should return latest when latest version >= rc version', () => { + const configWithNewerLatest = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + feedUrls: { + github: 'https://github.com/test/v1.7.0', + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: { + version: '1.7.0-rc.1', + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'rc', configWithNewerLatest) + + // Should return latest instead of rc because 1.7.0 >= 1.7.0-rc.1 + expect(result?.config).toEqual({ + version: '1.7.0', + feedUrls: { + github: 'https://github.com/test/v1.7.0', + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + expect(result?.channel).toBe('latest') // ✅ 返回 latest 频道 + }) + + it('should return latest when latest version >= beta version', () => { + const configWithNewerLatest = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: null, + beta: { + version: '1.6.8-beta.1', + + feedUrls: { + github: 'https://github.com/test/v1.6.8-beta.1', + + gitcode: 'https://gitcode.com/test/v1.6.8-beta.1' + } + } + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'beta', configWithNewerLatest) + + // Should return latest instead of beta because 1.7.0 >= 1.6.8-beta.1 + expect(result?.config).toEqual({ + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + }) + + it('should not compare latest with itself when requesting latest channel', () => { + const config = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'latest', config) + + // Should return latest directly without comparing with itself + expect(result?.config).toEqual({ + version: '1.7.0', + + feedUrls: { + github: 'https://github.com/test/v1.7.0', + + gitcode: 'https://gitcode.com/test/v1.7.0' + } + }) + }) + + it('should return rc when rc version > latest version', () => { + const configWithNewerRc = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'rc', configWithNewerRc) + + // Should return rc because 1.7.0-rc.1 > 1.6.7 + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + }) + + it('should return beta when beta version > latest version', () => { + const configWithNewerBeta = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.0': { + minCompatibleVersion: '1.0.0', + description: 'v1.7.0', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: { + version: '1.7.0-beta.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.5', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.5' + } + } + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.6.0', 'beta', configWithNewerBeta) + + // Should return beta because 1.7.0-beta.5 > 1.6.7 + expect(result?.config).toEqual({ + version: '1.7.0-beta.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.5', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.5' + } + }) + }) + + it('should return lower version when higher version has no compatible channel', () => { + vi.mocked(app.getVersion).mockReturnValue('1.8.0') + + const result = (appUpdater as any)._findCompatibleChannel('1.8.0', 'latest', mockConfig) + + // 1.8.0 >= 1.7.0 but 2.0.0 has no latest channel, so return 1.6.7 + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should return null when current version does not meet minCompatibleVersion', () => { + vi.mocked(app.getVersion).mockReturnValue('0.9.0') + + const result = (appUpdater as any)._findCompatibleChannel('0.9.0', 'latest', mockConfig) + + // 0.9.0 < 1.0.0 (minCompatibleVersion) + expect(result).toBeNull() + }) + + it('should return lower version rc when higher version has no rc channel', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.8.0', 'rc', mockConfig) + + // 1.8.0 >= 1.7.0 but 2.0.0 has no rc channel, so return 1.6.7 rc + expect(result?.config).toEqual({ + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }) + }) + + it('should return null when no version has the requested channel', () => { + const configWithoutRc = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'v1.6.7', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: null, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.5.0', 'rc', configWithoutRc) + + expect(result).toBeNull() + }) + }) + + describe('Upgrade Path', () => { + const fullConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.6.7': { + minCompatibleVersion: '1.0.0', + description: 'Last v1.x', + channels: { + latest: { + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }, + rc: { + version: '1.7.0-rc.1', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-rc.1', + + gitcode: 'https://gitcode.com/test/v1.7.0-rc.1' + } + }, + beta: { + version: '1.7.0-beta.3', + + feedUrls: { + github: 'https://github.com/test/v1.7.0-beta.3', + + gitcode: 'https://gitcode.com/test/v1.7.0-beta.3' + } + } + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x', + channels: { + latest: null, + rc: null, + beta: null + } + } + } + } + + it('should upgrade from 1.6.3 to 1.6.7', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullConfig) + + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should block upgrade from 1.6.7 to 2.0.0 (minCompatibleVersion not met)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.7', 'latest', fullConfig) + + // Should return 1.6.7, not 2.0.0, because 1.6.7 < 1.7.0 (minCompatibleVersion of 2.0.0) + expect(result?.config).toEqual({ + version: '1.6.7', + + feedUrls: { + github: 'https://github.com/test/v1.6.7', + + gitcode: 'https://gitcode.com/test/v1.6.7' + } + }) + }) + + it('should allow upgrade from 1.7.0 to 2.0.0', () => { + const configWith2x = { + ...fullConfig, + versions: { + ...fullConfig.versions, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x', + channels: { + latest: { + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }, + rc: null, + beta: null + } + } + } + } + + const result = (appUpdater as any)._findCompatibleChannel('1.7.0', 'latest', configWith2x) + + expect(result?.config).toEqual({ + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }) + }) + }) + + describe('Complete Multi-Step Upgrade Path', () => { + const fullUpgradeConfig = { + lastUpdated: '2025-01-05T00:00:00Z', + versions: { + '1.7.5': { + minCompatibleVersion: '1.0.0', + description: 'Last v1.x stable', + channels: { + latest: { + version: '1.7.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.5', + + gitcode: 'https://gitcode.com/test/v1.7.5' + } + }, + rc: null, + beta: null + } + }, + '2.0.0': { + minCompatibleVersion: '1.7.0', + description: 'First v2.x - intermediate version', + channels: { + latest: { + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }, + rc: null, + beta: null + } + }, + '2.1.6': { + minCompatibleVersion: '2.0.0', + description: 'Current v2.x stable', + channels: { + latest: { + version: '2.1.6', + + feedUrls: { + github: 'https://github.com/test/latest', + + gitcode: 'https://gitcode.com/test/latest' + } + }, + rc: null, + beta: null + } + } + } + } + + it('should upgrade from 1.6.3 to 1.7.5 (step 1)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '1.7.5', + + feedUrls: { + github: 'https://github.com/test/v1.7.5', + + gitcode: 'https://gitcode.com/test/v1.7.5' + } + }) + }) + + it('should upgrade from 1.7.5 to 2.0.0 (step 2)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.7.5', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '2.0.0', + + feedUrls: { + github: 'https://github.com/test/v2.0.0', + + gitcode: 'https://gitcode.com/test/v2.0.0' + } + }) + }) + + it('should upgrade from 2.0.0 to 2.1.6 (step 3)', () => { + const result = (appUpdater as any)._findCompatibleChannel('2.0.0', 'latest', fullUpgradeConfig) + + expect(result?.config).toEqual({ + version: '2.1.6', + + feedUrls: { + github: 'https://github.com/test/latest', + + gitcode: 'https://gitcode.com/test/latest' + } + }) + }) + + it('should complete full upgrade path: 1.6.3 -> 1.7.5 -> 2.0.0 -> 2.1.6', () => { + // Step 1: 1.6.3 -> 1.7.5 + let currentVersion = '1.6.3' + let result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('1.7.5') + + // Step 2: 1.7.5 -> 2.0.0 + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.0.0') + + // Step 3: 2.0.0 -> 2.1.6 + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.1.6') + + // Final: 2.1.6 is the latest, no more upgrades + currentVersion = result?.config.version! + result = (appUpdater as any)._findCompatibleChannel(currentVersion, 'latest', fullUpgradeConfig) + expect(result?.config.version).toBe('2.1.6') + }) + + it('should block direct upgrade from 1.6.3 to 2.0.0 (skip intermediate)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.6.3', 'latest', fullUpgradeConfig) + + // Should return 1.7.5, not 2.0.0, because 1.6.3 < 1.7.0 (minCompatibleVersion of 2.0.0) + expect(result?.config.version).toBe('1.7.5') + expect(result?.config.version).not.toBe('2.0.0') + }) + + it('should block direct upgrade from 1.7.5 to 2.1.6 (skip intermediate)', () => { + const result = (appUpdater as any)._findCompatibleChannel('1.7.5', 'latest', fullUpgradeConfig) + + // Should return 2.0.0, not 2.1.6, because 1.7.5 < 2.0.0 (minCompatibleVersion of 2.1.6) + expect(result?.config.version).toBe('2.0.0') + expect(result?.config.version).not.toBe('2.1.6') + }) + }) }) diff --git a/yarn.lock b/yarn.lock index bb178dcbc..e76a773b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2827,26 +2827,6 @@ __metadata: languageName: node linkType: hard -"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2": - version: 10.2.0-electron.1 - resolution: "@electron/node-gyp@https://github.com/electron/node-gyp.git#commit=06b29aafb7708acef8b3669835c8a7857ebc92d2" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - glob: "npm:^8.1.0" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^10.2.1" - nopt: "npm:^6.0.0" - proc-log: "npm:^2.0.1" - semver: "npm:^7.3.5" - tar: "npm:^6.2.1" - which: "npm:^2.0.2" - bin: - node-gyp: ./bin/node-gyp.js - checksum: 10c0/e8c97bb5347bf0871312860010b70379069359bf05a6beb9e4d898d0831f9f8447f35b887a86d5241989e804813cf72054327928da38714a6102f791e802c8d9 - languageName: node - linkType: hard - "@electron/notarize@npm:2.5.0, @electron/notarize@npm:^2.5.0": version: 2.5.0 resolution: "@electron/notarize@npm:2.5.0" @@ -2875,20 +2855,19 @@ __metadata: languageName: node linkType: hard -"@electron/rebuild@npm:3.7.2": - version: 3.7.2 - resolution: "@electron/rebuild@npm:3.7.2" +"@electron/rebuild@npm:4.0.1": + version: 4.0.1 + resolution: "@electron/rebuild@npm:4.0.1" dependencies: - "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2" "@malept/cross-spawn-promise": "npm:^2.0.0" chalk: "npm:^4.0.0" debug: "npm:^4.1.1" detect-libc: "npm:^2.0.1" - fs-extra: "npm:^10.0.0" got: "npm:^11.7.0" - node-abi: "npm:^3.45.0" - node-api-version: "npm:^0.2.0" - node-gyp: "npm:latest" + graceful-fs: "npm:^4.2.11" + node-abi: "npm:^4.2.0" + node-api-version: "npm:^0.2.1" + node-gyp: "npm:^11.2.0" ora: "npm:^5.1.0" read-binary-file-arch: "npm:^1.0.6" semver: "npm:^7.3.5" @@ -2896,7 +2875,7 @@ __metadata: yargs: "npm:^17.0.1" bin: electron-rebuild: lib/cli.js - checksum: 10c0/e561819926c30c7ad284f721d1d66453f59f8e5ea54a7cc9148a00e8ab3cedb6aa57fe4789f39a3454a3eb90b41a5f7d7461246ee3a16c63c8d3db23db94a391 + checksum: 10c0/4863d39c34515f3fb521ce5e976e25db20e89920574ca353efd0c3272d5f4d14546ba15ce28cee4299187160b2af02e3e130100ba8dc53f313b6eb685dc54928 languageName: node linkType: hard @@ -3459,13 +3438,6 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 10c0/0b3c9958d3cd17f4add3574975e3115ae05dc7f1298a60810414b16f6f558c137b5fb3cd3905df380bacfd955ec13f67c1e6710cbb5c246a7e8d65a8289b2bff - languageName: node - linkType: hard - "@google/genai@npm:1.0.1": version: 1.0.1 resolution: "@google/genai@npm:1.0.1" @@ -3772,6 +3744,22 @@ __metadata: languageName: node linkType: hard +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -4964,16 +4952,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.2 - resolution: "@npmcli/fs@npm:2.1.2" - dependencies: - "@gar/promisify": "npm:^1.1.3" - semver: "npm:^7.3.5" - checksum: 10c0/c50d087733d0d8df23be24f700f104b19922a28677aa66fdbe06ff6af6431cc4a5bb1e27683cbc661a5dafa9bafdc603e6a0378121506dfcd394b2b6dd76a187 - languageName: node - linkType: hard - "@npmcli/fs@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/fs@npm:4.0.0" @@ -4983,16 +4961,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.1 - resolution: "@npmcli/move-file@npm:2.0.1" - dependencies: - mkdirp: "npm:^1.0.4" - rimraf: "npm:^3.0.2" - checksum: 10c0/11b2151e6d1de6f6eb23128de5aa8a429fd9097d839a5190cb77aa47a6b627022c42d50fa7c47a00f1c9f8f0c1560092b09b061855d293fa0741a2a94cfb174d - languageName: node - linkType: hard - "@openrouter/ai-sdk-provider@npm:^1.2.0": version: 1.2.0 resolution: "@openrouter/ai-sdk-provider@npm:1.2.0" @@ -8052,13 +8020,6 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: 10c0/073bfa548026b1ebaf1659eb8961e526be22fa77139b10d60e712f46d2f0f05f4e6c8bec62a087d41088ee9e29faa7f54838568e475ab2f776171003c3920858 - languageName: node - linkType: hard - "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -10089,11 +10050,11 @@ __metadata: drizzle-kit: "npm:^0.31.4" drizzle-orm: "npm:^0.44.5" electron: "npm:38.7.0" - electron-builder: "npm:26.0.15" + electron-builder: "npm:26.1.0" electron-devtools-installer: "npm:^3.2.0" electron-reload: "npm:^2.0.0-alpha.1" electron-store: "npm:^8.2.0" - electron-updater: "npm:6.6.4" + electron-updater: "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch" electron-vite: "npm:4.0.1" electron-window-state: "npm:^5.0.3" emittery: "npm:^1.0.3" @@ -10223,13 +10184,6 @@ __metadata: languageName: unknown linkType: soft -"abbrev@npm:^1.0.0": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 10c0/3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 - languageName: node - linkType: hard - "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -10300,15 +10254,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 - languageName: node - linkType: hard - "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.3 resolution: "agent-base@npm:7.1.3" @@ -10325,16 +10270,6 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: "npm:^2.0.0" - indent-string: "npm:^4.0.0" - checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 - languageName: node - linkType: hard - "ai@npm:^5.0.90": version: 5.0.90 resolution: "ai@npm:5.0.90" @@ -10593,91 +10528,48 @@ __metadata: languageName: node linkType: hard -"app-builder-lib@npm:26.0.15": - version: 26.0.15 - resolution: "app-builder-lib@npm:26.0.15" +"app-builder-lib@npm:26.1.0": + version: 26.1.0 + resolution: "app-builder-lib@npm:26.1.0" dependencies: "@develar/schema-utils": "npm:~2.6.5" "@electron/asar": "npm:3.4.1" "@electron/fuses": "npm:^1.8.0" "@electron/notarize": "npm:2.5.0" "@electron/osx-sign": "npm:1.3.3" - "@electron/rebuild": "npm:3.7.2" + "@electron/rebuild": "npm:4.0.1" "@electron/universal": "npm:2.0.3" "@malept/flatpak-bundler": "npm:^0.4.0" "@types/fs-extra": "npm:9.0.13" async-exit-hook: "npm:^2.0.1" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chromium-pickle-js: "npm:^0.2.0" - config-file-ts: "npm:0.2.8-rc1" + ci-info: "npm:^4.2.0" debug: "npm:^4.3.4" dotenv: "npm:^16.4.5" dotenv-expand: "npm:^11.0.6" ejs: "npm:^3.1.8" - electron-publish: "npm:26.0.13" + electron-publish: "npm:26.1.0" fs-extra: "npm:^10.1.0" hosted-git-info: "npm:^4.1.0" - is-ci: "npm:^3.0.0" isbinaryfile: "npm:^5.0.0" + jiti: "npm:^2.4.2" js-yaml: "npm:^4.1.0" json5: "npm:^2.2.3" lazy-val: "npm:^1.0.5" - minimatch: "npm:^10.0.0" + minimatch: "npm:^10.0.3" plist: "npm:3.1.0" resedit: "npm:^1.7.0" - semver: "npm:^7.3.8" + semver: "npm:7.7.2" tar: "npm:^6.1.12" temp-file: "npm:^3.4.0" tiny-async-pool: "npm:1.3.0" + which: "npm:^5.0.0" peerDependencies: - dmg-builder: 26.0.15 - electron-builder-squirrel-windows: 26.0.15 - checksum: 10c0/d617864aca3c61633a9a5fda8d991ea90bcbe502702a4a1d64545ae6cfaa1f4122415db02b11e0dc76a527b169ea6e5619551903456e24a7053b3f4eb04cb79f - languageName: node - linkType: hard - -"app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch": - version: 26.0.15 - resolution: "app-builder-lib@patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch::version=26.0.15&hash=1f4887" - dependencies: - "@develar/schema-utils": "npm:~2.6.5" - "@electron/asar": "npm:3.4.1" - "@electron/fuses": "npm:^1.8.0" - "@electron/notarize": "npm:2.5.0" - "@electron/osx-sign": "npm:1.3.3" - "@electron/rebuild": "npm:3.7.2" - "@electron/universal": "npm:2.0.3" - "@malept/flatpak-bundler": "npm:^0.4.0" - "@types/fs-extra": "npm:9.0.13" - async-exit-hook: "npm:^2.0.1" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" - chromium-pickle-js: "npm:^0.2.0" - config-file-ts: "npm:0.2.8-rc1" - debug: "npm:^4.3.4" - dotenv: "npm:^16.4.5" - dotenv-expand: "npm:^11.0.6" - ejs: "npm:^3.1.8" - electron-publish: "npm:26.0.13" - fs-extra: "npm:^10.1.0" - hosted-git-info: "npm:^4.1.0" - is-ci: "npm:^3.0.0" - isbinaryfile: "npm:^5.0.0" - js-yaml: "npm:^4.1.0" - json5: "npm:^2.2.3" - lazy-val: "npm:^1.0.5" - minimatch: "npm:^10.0.0" - plist: "npm:3.1.0" - resedit: "npm:^1.7.0" - semver: "npm:^7.3.8" - tar: "npm:^6.1.12" - temp-file: "npm:^3.4.0" - tiny-async-pool: "npm:1.3.0" - peerDependencies: - dmg-builder: 26.0.15 - electron-builder-squirrel-windows: 26.0.15 - checksum: 10c0/5de2bd593b21e464585ffa3424e053d41f8569b14ba2a00f29f84cb0b83347a7da3653587f9ef8b5d2f6d1e5bfc4081956b9d72f180d65960db49b5ac84b73d4 + dmg-builder: 26.1.0 + electron-builder-squirrel-windows: 26.1.0 + checksum: 10c0/c8397886e59dc6a8ae4d90bc59fd28631705c5873789463a55b3e029062d6194d38e9feb1e6595ca31a069ed37ae893703fadd09a95ed4d2b1ab92fb92b13d72 languageName: node linkType: hard @@ -11218,38 +11110,38 @@ __metadata: languageName: node linkType: hard -"builder-util-runtime@npm:9.3.2": - version: 9.3.2 - resolution: "builder-util-runtime@npm:9.3.2" +"builder-util-runtime@npm:9.5.0": + version: 9.5.0 + resolution: "builder-util-runtime@npm:9.5.0" dependencies: debug: "npm:^4.3.4" sax: "npm:^1.2.4" - checksum: 10c0/1a103268ef800a504f04021ce14db282ddcfb72dec8238e7c9624a9c651ccd9c15c45ddcdb00e7cf6a5164d9822e30efdeeff470b506ed6aa9ed27c0aaefa695 + checksum: 10c0/797f4f8129557de6f5699991974f1701e464646664a14f841870fca0ddb05cb63cb8f2ca3c082cd6215690048c5e12df8404e7ccec371640eed9edc8cb592ae6 languageName: node linkType: hard -"builder-util@npm:26.0.13": - version: 26.0.13 - resolution: "builder-util@npm:26.0.13" +"builder-util@npm:26.1.0": + version: 26.1.0 + resolution: "builder-util@npm:26.1.0" dependencies: 7zip-bin: "npm:~5.2.0" "@types/debug": "npm:^4.1.6" app-builder-bin: "npm:5.0.0-alpha.12" - builder-util-runtime: "npm:9.3.2" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.4" fs-extra: "npm:^10.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.0" - is-ci: "npm:^3.0.0" js-yaml: "npm:^4.1.0" sanitize-filename: "npm:^1.6.3" source-map-support: "npm:^0.5.19" stat-mode: "npm:^1.0.0" temp-file: "npm:^3.4.0" tiny-async-pool: "npm:1.3.0" - checksum: 10c0/e8e9d6de04ec5c60f21c8ac8a30f6edd38ae76f0438ba801ec135ddecdce7c4bfadf881bdaaed184b0cab28e04ef21869ecc67a1e44c0e38ec0fd56c90970f03 + checksum: 10c0/0e1bcc04452cda8eaa1d63f338e05c1280f0539ee9dd7a9d4d17f75dff323d0d34de184fc146e3bdb1e1f1578bc0070569b1701312b509e802c97bfe4fed24b1 languageName: node linkType: hard @@ -11274,32 +11166,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" - dependencies: - "@npmcli/fs": "npm:^2.1.0" - "@npmcli/move-file": "npm:^2.0.0" - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.1.0" - glob: "npm:^8.0.1" - infer-owner: "npm:^1.0.4" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - mkdirp: "npm:^1.0.4" - p-map: "npm:^4.0.0" - promise-inflight: "npm:^1.0.1" - rimraf: "npm:^3.0.2" - ssri: "npm:^9.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^2.0.0" - checksum: 10c0/cdf6836e1c457d2a5616abcaf5d8240c0346b1f5bd6fdb8866b9d84b6dff0b54e973226dc11e0d099f35394213d24860d1989c8358d2a41b39eb912b3000e749 - languageName: node - linkType: hard - "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -11692,10 +11558,10 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.2.0": - version: 3.9.0 - resolution: "ci-info@npm:3.9.0" - checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a +"ci-info@npm:^4.2.0": + version: 4.3.1 + resolution: "ci-info@npm:4.3.1" + checksum: 10c0/7dd82000f514d76ddfe7775e4cb0d66e5c638f5fa0e2a3be29557e898da0d32ac04f231217d414d07fb968b1fbc6d980ee17ddde0d2c516f23da9cfff608f6c1 languageName: node linkType: hard @@ -11720,13 +11586,6 @@ __metadata: languageName: node linkType: hard -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 - languageName: node - linkType: hard - "cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -12165,16 +12024,6 @@ __metadata: languageName: node linkType: hard -"config-file-ts@npm:0.2.8-rc1": - version: 0.2.8-rc1 - resolution: "config-file-ts@npm:0.2.8-rc1" - dependencies: - glob: "npm:^10.3.12" - typescript: "npm:^5.4.3" - checksum: 10c0/9839a8e33111156665c45c4e5dd6bfa81ee80596f9dc0a078465769b951e28c0fa4bd75bb9bc56f747da285b993fb7998c4c07c0f368ab6bdb019d203764cdc8 - languageName: node - linkType: hard - "console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -12893,7 +12742,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -13279,13 +13128,12 @@ __metadata: languageName: node linkType: hard -"dmg-builder@npm:26.0.15": - version: 26.0.15 - resolution: "dmg-builder@npm:26.0.15" +"dmg-builder@npm:26.1.0": + version: 26.1.0 + resolution: "dmg-builder@npm:26.1.0" dependencies: - app-builder-lib: "npm:26.0.15" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + app-builder-lib: "npm:26.1.0" + builder-util: "npm:26.1.0" dmg-license: "npm:^1.0.11" fs-extra: "npm:^10.1.0" iconv-lite: "npm:^0.6.2" @@ -13293,7 +13141,7 @@ __metadata: dependenciesMeta: dmg-license: optional: true - checksum: 10c0/fe9ea305abf05e16d96f7f7435db14f0264f82d4f49a09a64645425a3d2a69ed9cc346f36278b958fb1197fea72f5afc9661a22307d2c0ab5192843dc31f794c + checksum: 10c0/0dc4e993516dfb896b45b7de6ee88bc99a95205e64bbcac4425dba4fc3b608d5117f8ff14c4204ae916cb567b7c1ab5acc91fa223856ed66e9f22446d440c3dc languageName: node linkType: hard @@ -13633,24 +13481,24 @@ __metadata: languageName: node linkType: hard -"electron-builder@npm:26.0.15": - version: 26.0.15 - resolution: "electron-builder@npm:26.0.15" +"electron-builder@npm:26.1.0": + version: 26.1.0 + resolution: "electron-builder@npm:26.1.0" dependencies: - app-builder-lib: "npm:26.0.15" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + app-builder-lib: "npm:26.1.0" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" - dmg-builder: "npm:26.0.15" + ci-info: "npm:^4.2.0" + dmg-builder: "npm:26.1.0" fs-extra: "npm:^10.1.0" - is-ci: "npm:^3.0.0" lazy-val: "npm:^1.0.5" simple-update-notifier: "npm:2.0.0" yargs: "npm:^17.6.2" bin: electron-builder: cli.js install-app-deps: install-app-deps.js - checksum: 10c0/bb21e4b547c8dfa590017930340ab9b7e2b017c5ba9286e5d0ccbe6481f4b13bbf905429124a1350a2282ee35dd52e9ba9d9d1d730fc1957c9e7789d0eb39374 + checksum: 10c0/9255a77f1124d3bc722ce9670380144eda42508f8a4695cad5346a44a7b547febe09e736b1b0046b7ddf84c4ea07ab385f87e2c8053dfa996a823d79e2bd05c8 languageName: node linkType: hard @@ -13666,19 +13514,19 @@ __metadata: languageName: node linkType: hard -"electron-publish@npm:26.0.13": - version: 26.0.13 - resolution: "electron-publish@npm:26.0.13" +"electron-publish@npm:26.1.0": + version: 26.1.0 + resolution: "electron-publish@npm:26.1.0" dependencies: "@types/fs-extra": "npm:^9.0.11" - builder-util: "npm:26.0.13" - builder-util-runtime: "npm:9.3.2" + builder-util: "npm:26.1.0" + builder-util-runtime: "npm:9.5.0" chalk: "npm:^4.1.2" form-data: "npm:^4.0.0" fs-extra: "npm:^10.1.0" lazy-val: "npm:^1.0.5" mime: "npm:^2.5.2" - checksum: 10c0/d00fd7bb904a9cf7731f194eef47147febc9c2b23b1003a00e8d678c04d00029f998cdccd9a9cacacbb46893741961137e92d392e1bb946019c4fc51ceedc922 + checksum: 10c0/f6593e007f47bea311ab9678c31f724a3c0826de4e0f8ea917d4c3d073d3470ede6a093b51408cd53dd790bb1baa4d5b7647a8cd935d0ff3b4d011050e861f0b languageName: node linkType: hard @@ -13708,19 +13556,35 @@ __metadata: languageName: node linkType: hard -"electron-updater@npm:6.6.4": - version: 6.6.4 - resolution: "electron-updater@npm:6.6.4" +"electron-updater@npm:6.7.0": + version: 6.7.0 + resolution: "electron-updater@npm:6.7.0" dependencies: - builder-util-runtime: "npm:9.3.2" + builder-util-runtime: "npm:9.5.0" fs-extra: "npm:^10.1.0" js-yaml: "npm:^4.1.0" lazy-val: "npm:^1.0.5" lodash.escaperegexp: "npm:^4.1.2" lodash.isequal: "npm:^4.5.0" - semver: "npm:^7.6.3" + semver: "npm:7.7.2" tiny-typed-emitter: "npm:^2.1.0" - checksum: 10c0/92ed7b39be1cf9cfe7be56e1054c44a405421ef7faeec365f8c19a6614428ec1e5116ba1f0fb731691d346e00cf16a555804e41feee2b9ac1a10759219de4406 + checksum: 10c0/8310af4a0a795de4bc68a75dc87e4116be1cfc324b60b60492b4afa2a8866ef58a53aa888d8709bed3fba4202deac8a9f02bf7ba03cb1a8fdbbed1a6fb1dad31 + languageName: node + linkType: hard + +"electron-updater@patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch": + version: 6.7.0 + resolution: "electron-updater@patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch::version=6.7.0&hash=5680de" + dependencies: + builder-util-runtime: "npm:9.5.0" + fs-extra: "npm:^10.1.0" + js-yaml: "npm:^4.1.0" + lazy-val: "npm:^1.0.5" + lodash.escaperegexp: "npm:^4.1.2" + lodash.isequal: "npm:^4.5.0" + semver: "npm:7.7.2" + tiny-typed-emitter: "npm:^2.1.0" + checksum: 10c0/8f80f2d76a254404abc43d9c03b68cf5a0d8ff933aa2d43d77f13d24f58e28a903828dac244c05b3391497d53e94d36452066920feb3c1b04ebdcf91faf47293 languageName: node linkType: hard @@ -15331,7 +15195,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": +"fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -15589,7 +15453,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.7, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -15619,19 +15483,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1, glob@npm:^8.1.0": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - checksum: 10c0/cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f - languageName: node - linkType: hard - "global-agent@npm:^3.0.0": version: 3.0.0 resolution: "global-agent@npm:3.0.0" @@ -16192,7 +16043,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 @@ -16212,17 +16063,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": "npm:2" - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/32a05e413430b2c1e542e5c74b38a9f14865301dd69dff2e53ddb684989440e3d2ce0c4b64d25eb63cf6283e6265ff979a61cf93e3ca3d23047ddfdc8df34a32 - languageName: node - linkType: hard - "http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -16253,16 +16093,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 - languageName: node - linkType: hard - "https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" @@ -16394,13 +16224,6 @@ __metadata: languageName: node linkType: hard -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 10c0/a7b241e3149c26e37474e3435779487f42f36883711f198c45794703c7556bc38af224088bd4d1a221a45b8208ae2c2bcf86200383621434d0c099304481c5b9 - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -16534,17 +16357,6 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^3.0.0": - version: 3.0.1 - resolution: "is-ci@npm:3.0.1" - dependencies: - ci-info: "npm:^3.2.0" - bin: - is-ci: bin.js - checksum: 10c0/0e81caa62f4520d4088a5bef6d6337d773828a88610346c4b1119fb50c842587ed8bef1e5d9a656835a599e7209405b5761ddf2339668f2d0f4e889a92fe6051 - languageName: node - linkType: hard - "is-decimal@npm:^1.0.0": version: 1.0.4 resolution: "is-decimal@npm:1.0.4" @@ -16665,13 +16477,6 @@ __metadata: languageName: node linkType: hard -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d - languageName: node - linkType: hard - "is-natural-number@npm:^4.0.1": version: 4.0.1 resolution: "is-natural-number@npm:4.0.1" @@ -17947,7 +17752,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.14.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: 10c0/b3a452b491433db885beed95041eb104c157ef7794b9c9b4d647be503be91769d11206bb573849a16b4cc0d03cbd15ffd22df7960997788b74c1d399ac7a4fed @@ -18033,30 +17838,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.2.1": - version: 10.2.1 - resolution: "make-fetch-happen@npm:10.2.1" - dependencies: - agentkeepalive: "npm:^4.2.1" - cacache: "npm:^16.1.0" - http-cache-semantics: "npm:^4.1.0" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" - is-lambda: "npm:^1.0.1" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-fetch: "npm:^2.0.3" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" - promise-retry: "npm:^2.0.1" - socks-proxy-agent: "npm:^7.0.0" - ssri: "npm:^9.0.0" - checksum: 10c0/28ec392f63ab93511f400839dcee83107eeecfaad737d1e8487ea08b4332cd89a8f3319584222edd9f6f1d0833cf516691469496d46491863f9e88c658013949 - languageName: node - linkType: hard - "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -19230,12 +19011,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.0": - version: 10.0.1 - resolution: "minimatch@npm:10.0.1" +"minimatch@npm:^10.0.3": + version: 10.1.1 + resolution: "minimatch@npm:10.1.1" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10c0/e6c29a81fe83e1877ad51348306be2e8aeca18c88fdee7a99df44322314279e15799e41d7cb274e4e8bb0b451a3bc622d6182e157dfa1717d6cda75e9cd8cd5d + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10c0/c85d44821c71973d636091fddbfbffe62370f5ee3caf0241c5b60c18cd289e916200acb2361b7e987558cd06896d153e25d505db9fc1e43e6b4b6752e2702902 languageName: node linkType: hard @@ -19273,15 +19054,6 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/8f82bd1f3095b24f53a991b04b67f4c710c894e518b813f0864a31de5570441a509be1ca17e0bb92b047591a8fdbeb886f502764fefb00d2f144f4011791e898 - languageName: node - linkType: hard - "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -19291,21 +19063,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^2.0.3": - version: 2.1.2 - resolution: "minipass-fetch@npm:2.1.2" - dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^3.1.6" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" - dependenciesMeta: - encoding: - optional: true - checksum: 10c0/33ab2c5bdb3d91b9cb8bc6ae42d7418f4f00f7f7beae14b3bb21ea18f9224e792f560a6e17b6f1be12bbeb70dbe99a269f4204c60e5d99130a0777b153505c43 - languageName: node - linkType: hard - "minipass-fetch@npm:^4.0.0": version: 4.0.1 resolution: "minipass-fetch@npm:4.0.1" @@ -19348,7 +19105,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -19371,7 +19128,7 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": +"minizlib@npm:^2.1.1": version: 2.1.2 resolution: "minizlib@npm:2.1.2" dependencies: @@ -19415,7 +19172,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": +"mkdirp@npm:^1.0.3": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" bin: @@ -19602,13 +19359,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": - version: 0.6.4 - resolution: "negotiator@npm:0.6.4" - checksum: 10c0/3e677139c7fb7628a6f36335bf11a885a62c21d5390204590a1a214a5631fcbe5ea74ef6a610b60afe84b4d975cbe0566a23f20ee17c77c73e74b80032108dea - languageName: node - linkType: hard - "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -19682,7 +19432,7 @@ __metadata: languageName: node linkType: hard -"node-api-version@npm:^0.2.0": +"node-api-version@npm:^0.2.1": version: 0.2.1 resolution: "node-api-version@npm:0.2.1" dependencies: @@ -19741,6 +19491,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:^11.2.0": + version: 11.5.0 + resolution: "node-gyp@npm:11.5.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.4.3" + tinyglobby: "npm:^0.2.12" + which: "npm:^5.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/31ff49586991b38287bb15c3d529dd689cfc32f992eed9e6997b9d712d5d21fe818a8b1bbfe3b76a7e33765c20210c5713212f4aa329306a615b87d8a786da3a + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 11.2.0 resolution: "node-gyp@npm:11.2.0" @@ -19782,17 +19552,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^6.0.0": - version: 6.0.0 - resolution: "nopt@npm:6.0.0" - dependencies: - abbrev: "npm:^1.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/837b52c330df16fcaad816b1f54fec6b2854ab1aa771d935c1603fbcf9b023bb073f1466b1b67f48ea4dce127ae675b85b9d9355700e9b109de39db490919786 - languageName: node - linkType: hard - "nopt@npm:^8.0.0": version: 8.1.0 resolution: "nopt@npm:8.1.0" @@ -20264,15 +20023,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: "npm:^3.0.0" - checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 - languageName: node - linkType: hard - "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -20927,13 +20677,6 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^2.0.1": - version: 2.0.1 - resolution: "proc-log@npm:2.0.1" - checksum: 10c0/701c501429775ce34cec28ef6a1c976537274b42917212fb8a5975ebcecb0a85612907fd7f99ff28ff4c2112bb84a0f4322fc9b9e1e52a8562fcbb1d5b3ce608 - languageName: node - linkType: hard - "proc-log@npm:^5.0.0": version: 5.0.0 resolution: "proc-log@npm:5.0.0" @@ -20962,13 +20705,6 @@ __metadata: languageName: node linkType: hard -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 10c0/d179d148d98fbff3d815752fa9a08a87d3190551d1420f17c4467f628214db12235ae068d98cd001f024453676d8985af8f28f002345646c4ece4600a79620bc - languageName: node - linkType: hard - "promise-limit@npm:^2.7.0": version: 2.7.0 resolution: "promise-limit@npm:2.7.0" @@ -23171,6 +22907,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.7.2, semver@npm:^7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + "semver@npm:^5.5.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -23189,7 +22934,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.7.1 resolution: "semver@npm:7.7.1" bin: @@ -23198,15 +22943,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.7.2": - version: 7.7.2 - resolution: "semver@npm:7.7.2" - bin: - semver: bin/semver.js - checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea - languageName: node - linkType: hard - "send@npm:^1.1.0, send@npm:^1.2.0": version: 1.2.0 resolution: "send@npm:1.2.0" @@ -23611,17 +23347,6 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "socks-proxy-agent@npm:7.0.0" - dependencies: - agent-base: "npm:^6.0.2" - debug: "npm:^4.3.3" - socks: "npm:^2.6.2" - checksum: 10c0/b859f7eb8e96ec2c4186beea233ae59c02404094f3eb009946836af27d6e5c1627d1975a69b4d2e20611729ed543b6db3ae8481eb38603433c50d0345c987600 - languageName: node - linkType: hard - "socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" @@ -23633,7 +23358,7 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.6.2, socks@npm:^2.8.2, socks@npm:^2.8.3": +"socks@npm:^2.8.2, socks@npm:^2.8.3": version: 2.8.6 resolution: "socks@npm:2.8.6" dependencies: @@ -23724,15 +23449,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.0": - version: 9.0.1 - resolution: "ssri@npm:9.0.1" - dependencies: - minipass: "npm:^3.1.1" - checksum: 10c0/c5d153ce03b5980d683ecaa4d805f6a03d8dc545736213803e168a1907650c46c08a4e5ce6d670a0205482b35c35713d9d286d9133bdd79853a406e22ad81f04 - languageName: node - linkType: hard - "stack-trace@npm:0.0.x": version: 0.0.10 resolution: "stack-trace@npm:0.0.10" @@ -24261,7 +23977,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.2.1": +"tar@npm:^6.0.5, tar@npm:^6.1.12": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -24938,16 +24654,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.4.3, typescript@npm:~5.8.2": - version: 5.8.3 - resolution: "typescript@npm:5.8.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 - languageName: node - linkType: hard - "typescript@npm:^5.8.2": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -24958,6 +24664,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~5.8.2": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^5.0.0#optional!builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" @@ -24968,16 +24684,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.2#optional!builtin": - version: 5.8.3 - resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" @@ -24988,6 +24694,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A~5.8.2#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb + languageName: node + linkType: hard + "ua-parser-js@npm:^1.0.35": version: 1.0.40 resolution: "ua-parser-js@npm:1.0.40" @@ -25115,15 +24831,6 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^2.0.0": - version: 2.0.1 - resolution: "unique-filename@npm:2.0.1" - dependencies: - unique-slug: "npm:^3.0.0" - checksum: 10c0/55d95cd670c4a86117ebc34d394936d712d43b56db6bc511f9ca00f666373818bf9f075fb0ab76bcbfaf134592ef26bb75aad20786c1ff1ceba4457eaba90fb8 - languageName: node - linkType: hard - "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -25133,15 +24840,6 @@ __metadata: languageName: node linkType: hard -"unique-slug@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-slug@npm:3.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10c0/617240eb921af803b47d322d75a71a363dacf2e56c29ae5d1404fad85f64f4ec81ef10ee4fd79215d0202cbe1e5a653edb0558d59c9c81d3bd538c2d58e4c026 - languageName: node - linkType: hard - "unique-slug@npm:^5.0.0": version: 5.0.0 resolution: "unique-slug@npm:5.0.0" @@ -25900,7 +25598,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: