diff --git a/.github/workflows/auto-tag-release.yml b/.github/workflows/auto-tag-release.yml new file mode 100644 index 0000000..ab9e5e5 --- /dev/null +++ b/.github/workflows/auto-tag-release.yml @@ -0,0 +1,272 @@ +# This workflow requires Ubuntu 22.04 or 24.04 + +name: Auto Tag & Release + +on: + push: + branches: + - master + - main + tags: + - "v*" + paths-ignore: + - "**.md" + - "LICENSE" + - ".gitignore" + workflow_call: {} + +permissions: + contents: write + packages: write + actions: write + +jobs: + pre_job: + runs-on: ubuntu-22.04 + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5.3.0 + with: + cancel_others: "true" + concurrent_skipping: "same_content" + + auto-tag-release: + needs: pre_job + if: | + needs.pre_job.outputs.should_skip != 'true' || + startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-22.04 + timeout-minutes: 15 + outputs: + version: ${{ steps.get_latest_tag.outputs.version }} + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + lfs: true + submodules: recursive + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + check-latest: true + cache: true + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + ~/.cache/git + key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-build- + ${{ runner.os }}- + + # 只在非tag推送时执行自动打tag + - name: Get latest tag + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + id: get_latest_tag + run: | + set -euo pipefail + git fetch --tags --force || { + echo "::error::Failed to fetch tags" + exit 1 + } + latest_tag=$(git tag -l 'v*' --sort=-v:refname | head -n 1) + if [ -z "$latest_tag" ]; then + new_version="v0.1.0" + else + major=$(echo $latest_tag | cut -d. -f1) + minor=$(echo $latest_tag | cut -d. -f2) + patch=$(echo $latest_tag | cut -d. -f3) + new_patch=$((patch + 1)) + new_version="$major.$minor.$new_patch" + fi + echo "version=$new_version" >> "$GITHUB_OUTPUT" + echo "Generated version: $new_version" + + - name: Validate version + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + run: | + set -euo pipefail + new_tag="${{ steps.get_latest_tag.outputs.version }}" + echo "Validating version: $new_tag" + if [[ ! $new_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid version format: $new_tag" + exit 1 + fi + major=$(echo $new_tag | cut -d. -f1 | tr -d 'v') + minor=$(echo $new_tag | cut -d. -f2) + patch=$(echo $new_tag | cut -d. -f3) + if [[ $major -gt 99 || $minor -gt 99 || $patch -gt 999 ]]; then + echo "::error::Version numbers out of valid range" + exit 1 + fi + echo "Version validation passed" + + - name: Create new tag + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + new_tag=${{ steps.get_latest_tag.outputs.version }} + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git tag -a $new_tag -m "Release $new_tag" + git push origin $new_tag + + # 在 Run GoReleaser 之前添加配置检查步骤 + - name: Check GoReleaser config + run: | + if [ ! -f ".goreleaser.yml" ] && [ ! -f ".goreleaser.yaml" ]; then + echo "::error::GoReleaser configuration file not found" + exit 1 + fi + + # 添加依赖检查步骤 + - name: Check Dependencies + run: | + go mod verify + go mod download + # 如果使用 vendor 模式,则执行以下命令 + if [ -d "vendor" ]; then + go mod vendor + fi + + # 添加构建环境准备步骤 + - name: Prepare Build Environment + run: | + echo "Building version: ${VERSION:-development}" + echo "GOOS=${GOOS:-$(go env GOOS)}" >> $GITHUB_ENV + echo "GOARCH=${GOARCH:-$(go env GOARCH)}" >> $GITHUB_ENV + echo "GO111MODULE=on" >> $GITHUB_ENV + + # 添加清理步骤 + - name: Cleanup workspace + run: | + rm -rf /tmp/go/ + rm -rf .cache/ + rm -rf dist/ + git clean -fdx + git status + + # 修改 GoReleaser 步骤 + - name: Run GoReleaser + if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} + uses: goreleaser/goreleaser-action@v3 + with: + distribution: goreleaser + version: latest + args: release --clean --timeout 60m + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get_latest_tag.outputs.version }} + CGO_ENABLED: 0 + GOPATH: /tmp/go + GOROOT: ${{ env.GOROOT }} + GOCACHE: /tmp/.cache/go-build + GOMODCACHE: /tmp/go/pkg/mod + GORELEASER_DEBUG: 1 + GORELEASER_CURRENT_TAG: ${{ steps.get_latest_tag.outputs.version }} + # 添加额外的构建信息 + BUILD_TIME: ${{ steps.get_latest_tag.outputs.version }} + BUILD_COMMIT: ${{ github.sha }} + + # 优化 vendor 同步步骤 + - name: Sync vendor directory + run: | + echo "Syncing vendor directory..." + go mod tidy + go mod vendor + go mod verify + # 验证 vendor 目录 + if [ -d "vendor" ]; then + echo "Verifying vendor directory..." + go mod verify + # 检查是否有未跟踪的文件 + if [ -n "$(git status --porcelain vendor/)" ]; then + echo "Warning: Vendor directory has uncommitted changes" + git status vendor/ + fi + fi + + # 添加错误检查步骤 + - name: Check GoReleaser Output + if: failure() + run: | + echo "::group::GoReleaser Debug Info" + cat dist/artifacts.json || true + echo "::endgroup::" + + echo "::group::GoReleaser Config" + cat .goreleaser.yml + echo "::endgroup::" + + echo "::group::Environment Info" + go version + go env + echo "::endgroup::" + + - name: Set Release Version + if: startsWith(github.ref, 'refs/tags/v') + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + # 改进验证步骤 + - name: Verify Release + if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} + run: | + echo "Verifying release artifacts..." + if [ ! -d "dist" ]; then + echo "::error::Release artifacts not found" + exit 1 + fi + # 验证生成的二进制文件 + for file in dist/cursor-id-modifier_*; do + if [ -f "$file" ]; then + echo "Verifying: $file" + if [[ "$file" == *.exe ]]; then + # Windows 二进制文件检查 + if ! [ -x "$file" ]; then + echo "::error::$file is not executable" + exit 1 + fi + else + # Unix 二进制文件检查 + if ! [ -x "$file" ]; then + echo "::error::$file is not executable" + exit 1 + fi + fi + fi + done + + - name: Notify on failure + if: failure() + run: | + echo "::error::Release process failed" + + # 修改构建摘要步骤 + - name: Build Summary + if: always() + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "- Go Version: $(go version)" >> $GITHUB_STEP_SUMMARY + echo "- Release Version: ${VERSION:-N/A}" >> $GITHUB_STEP_SUMMARY + echo "- GPG Signing: Disabled" >> $GITHUB_STEP_SUMMARY + echo "- Build Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + + if [ -d "dist" ]; then + echo "### Generated Artifacts" >> $GITHUB_STEP_SUMMARY + ls -lh dist/ | awk '{print "- "$9" ("$5")"}' >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 9feeb88..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - check-latest: true - - - name: Set Repository Variables - run: | - echo "GITHUB_REPOSITORY_OWNER=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV - echo "GITHUB_REPOSITORY_NAME=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 - with: - distribution: goreleaser - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 54f0356..27161b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,42 @@ -# Binary files -*.dll -*.so -*.dylib - -# Build directories -releases/ -cursor-id-modifier +# Compiled binary +/cursor-id-modifier +/cursor-id-modifier.exe +# Build output directories +bin/ +dist/ # Go specific go.sum +go/ +.cache/ -# Temporary files -*.tmp -*~ +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo -# System files +# OS specific .DS_Store Thumbs.db -.vscode -/.idea \ No newline at end of file +# Build and release artifacts +releases/ +*.syso +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test files +*.test +*.out +coverage.txt + +# Temporary files +*.tmp +*~ +*.bak +*.log \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 862b38d..13c0b21 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,14 +1,18 @@ -project_name: cursor-id-modifier +version: 2 before: hooks: - go mod tidy + - go mod vendor + - go mod verify builds: - - env: + - id: cursor-id-modifier + main: ./cmd/cursor-id-modifier/main.go + binary: cursor-id-modifier + env: - CGO_ENABLED=0 - ldflags: - - -s -w -X main.version={{.Version}} + - GO111MODULE=on goos: - linux - windows @@ -16,41 +20,73 @@ builds: goarch: - amd64 - arm64 - mod_timestamp: '{{ .CommitTimestamp }}' + - "386" + ignore: + - goos: darwin + goarch: "386" + ldflags: + - -s -w + - -X 'main.version={{.Version}}' + - -X 'main.commit={{.ShortCommit}}' + - -X 'main.date={{.CommitDate}}' + - -X 'main.builtBy=goreleaser' flags: - -trimpath - binary: cursor_id_modifier_{{ .Version }}_{{ .Os }}_{{ .Arch }} + mod_timestamp: '{{ .CommitTimestamp }}' archives: - - format: binary - name_template: "{{ .Binary }}" + - id: binary + format: binary + name_template: >- + {{- .ProjectName }}_ + {{- .Version }}_ + {{- .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + builds: + - cursor-id-modifier allow_different_binary_count: true + files: + - none* + +checksum: + name_template: 'checksums.txt' + algorithm: sha256 + +release: + draft: true + prerelease: auto + mode: replace + header: | + ## Release {{.Tag}} ({{.Date}}) + + See [CHANGELOG.md](CHANGELOG.md) for details. + footer: | + **Full Changelog**: https://github.com/owner/repo/compare/{{ .PreviousTag }}...{{ .Tag }} + extra_files: + - glob: 'LICENSE*' + - glob: 'README*' + - glob: 'CHANGELOG*' changelog: - use: github sort: asc + use: github + filters: + exclude: + - '^docs:' + - '^test:' + - '^ci:' + - Merge pull request + - Merge branch groups: - title: Features regexp: "^.*feat[(\\w)]*:+.*$" order: 0 - - title: 'Bug Fixes' + - title: 'Bug fixes' regexp: "^.*fix[(\\w)]*:+.*$" order: 1 - title: Others order: 999 - filters: - exclude: - - '^docs:' - - '^test:' - - '^ci:' - - '^chore:' - - Merge pull request - - Merge branch -release: - github: - owner: '{{ .Env.GITHUB_REPOSITORY_OWNER }}' - name: '{{ .Env.GITHUB_REPOSITORY_NAME }}' - draft: false - prerelease: auto - mode: replace +project_name: cursor-id-modifier diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2af6908 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# 📝 Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.23] - 2024-12-29 🚀 + +### ✨ Features +- **Initial Release**: First public release of Cursor ID Modifier +- **Multi-Platform Support**: + - 🪟 Windows (x64, x86) + - 🍎 macOS (Intel & Apple Silicon) + - 🐧 Linux (x64, x86, ARM64) +- **Installation**: + - Automated installation scripts for all platforms + - One-line installation commands + - Secure download and verification +- **Core Functionality**: + - Telemetry ID modification for Cursor IDE + - Automatic process management + - Secure configuration handling + +### 🐛 Bug Fixes +- **Installation**: + - Fixed environment variable preservation in sudo operations + - Enhanced error handling during installation + - Improved binary download reliability +- **Process Management**: + - Improved Cursor process detection accuracy + - Enhanced process termination reliability + - Better handling of edge cases +- **User Experience**: + - Enhanced error messages and user feedback + - Improved progress indicators + - Better handling of system permissions + +### 🔧 Technical Improvements +- Optimized binary size with proper build flags +- Enhanced cross-platform compatibility +- Improved error handling and logging +- Better system resource management + +### 📚 Documentation +- Added comprehensive installation instructions +- Included platform-specific guidelines +- Enhanced error troubleshooting guide + +--- +*For more details about the changes, please refer to the [commit history](https://github.com/yuaotian/go-cursor-help/commits/main).* + +[0.1.22]: https://github.com/yuaotian/go-cursor-help/releases/tag/v0.1.23 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11eeb01 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 dacrab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 0f3b3f6..edc9eb6 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,24 @@
[![Release](https://img.shields.io/github/v/release/yuaotian/go-cursor-help?style=flat-square&logo=github&color=blue)](https://github.com/yuaotian/go-cursor-help/releases/latest) -[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=bookstack)](https://github.com/yuaotian/go-cursor-help/blob/main/LICENSE) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=bookstack)](https://github.com/yuaotian/go-cursor-help/blob/master/LICENSE) [![Stars](https://img.shields.io/github/stars/yuaotian/go-cursor-help?style=flat-square&logo=github)](https://github.com/yuaotian/go-cursor-help/stargazers) -[English](#-english) | [中文](#-chinese) +[🌟 English](#english) | [🌏 中文](#chinese) Cursor Logo
-# 🌟 English +--- + +## 🌟 English ### 📝 Description -Resets Cursor's free trial limitation when you see: +> Resets Cursor's free trial limitation when you see: -``` +```text Too many free trial accounts used on this machine. Please upgrade to pro. We have this limit in place to prevent abuse. Please let us know if you believe @@ -27,96 +29,120 @@ this is a mistake. ### 💻 System Support -**Windows** ✅ x64 -**macOS** ✅ Intel & M-series -**Linux** ✅ x64 & ARM64 + + + + + + +
+ +**Windows** ✅ +- x64 (64-bit) +- x86 (32-bit) + + + +**macOS** ✅ +- Intel (x64) +- Apple Silicon (M1/M2) + + -### 📥 Installation +**Linux** ✅ +- x64 (64-bit) +- x86 (32-bit) +- ARM64 -#### Automatic Installation (Recommended) +
-**Linux/macOS** +### 🚀 One-Click Solution + +**Linux/macOS**: Copy and paste in terminal ```bash curl -fsSL https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.sh | sudo bash ``` -**Windows** (Run PowerShell as Admin) +**Windows**: Copy and paste in PowerShell ```powershell irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex ``` -The installation script will automatically: -- Request necessary privileges (sudo/admin) -- Close any running Cursor instances -- Backup existing configuration -- Install the tool -- Add it to system PATH -- Clean up temporary files - -#### Manual Installation - -1. Download the latest release for your system from the [releases page](https://github.com/yuaotian/go-cursor-help/releases) -2. Extract and run with administrator/root privileges: - ```bash - # Linux/macOS - chmod +x ./cursor_id_modifier_* # Add execute permission - sudo ./cursor_id_modifier_* - - # Windows (PowerShell Admin) - .\cursor_id_modifier_*.exe - ``` - -#### Manual Configuration Method - -1. Close Cursor completely -2. Navigate to the configuration file location: - - Windows: `%APPDATA%\Cursor\User\globalStorage\storage.json` - - macOS: `~/Library/Application Support/Cursor/User/globalStorage/storage.json` - - Linux: `~/.config/Cursor/User/globalStorage/storage.json` -3. Create a backup of `storage.json` -4. Edit `storage.json` and update these fields with new random UUIDs: - ```json - { - "telemetry.machineId": "generate-new-uuid", - "telemetry.macMachineId": "generate-new-uuid", - "telemetry.devDeviceId": "generate-new-uuid", - "telemetry.sqmId": "generate-new-uuid", - "lastModified": "2024-01-01T00:00:00.000Z", - "version": "1.0.1" - } - ``` -5. Save the file and restart Cursor +#### Windows Installation Features: +- 🔍 Automatically detects and uses PowerShell 7 if available +- 🛡️ Requests administrator privileges via UAC prompt +- 📝 Falls back to Windows PowerShell if PS7 isn't found +- 💡 Provides manual instructions if elevation fails + +That's it! The script will: +1. ✨ Install the tool automatically +2. 🔄 Reset your Cursor trial immediately + +### 📦 Manual Installation + +> Download the appropriate file for your system from [releases](https://github.com/yuaotian/go-cursor-help/releases/latest) + +
+Windows Packages + +- 64-bit: `cursor-id-modifier_windows_x64.exe` +- 32-bit: `cursor-id-modifier_windows_x86.exe` +
+ +
+macOS Packages + +- Intel: `cursor-id-modifier_darwin_x64_intel` +- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon` +
+ +
+Linux Packages + +- 64-bit: `cursor-id-modifier_linux_x64` +- 32-bit: `cursor-id-modifier_linux_x86` +- ARM64: `cursor-id-modifier_linux_arm64` +
### 🔧 Technical Details -#### Configuration Files +
+Configuration Files + The program modifies Cursor's `storage.json` config file located at: -- Windows: `%APPDATA%\Cursor\User\globalStorage\` -- macOS: `~/Library/Application Support/Cursor/User/globalStorage/` -- Linux: `~/.config/Cursor/User/globalStorage/` -#### Modified Fields +- Windows: `%APPDATA%\Cursor\User\globalStorage\storage.json` +- macOS: `~/Library/Application Support/Cursor/User/globalStorage/storage.json` +- Linux: `~/.config/Cursor/User/globalStorage/storage.json` +
+ +
+Modified Fields + The tool generates new unique identifiers for: - `telemetry.machineId` - `telemetry.macMachineId` - `telemetry.devDeviceId` - `telemetry.sqmId` +
+ +
+Safety Features -#### Safety Features -- Automatic backup of existing configuration -- Safe process termination -- Atomic file operations -- Error handling and rollback +- ✅ Safe process termination +- ✅ Atomic file operations +- ✅ Error handling and recovery +
--- -# 🌏 Chinese +## 🌏 Chinese ### 📝 问题描述 -当看到以下提示时重置Cursor试用期: +> 当看到以下提示时重置Cursor试用期: -``` +```text Too many free trial accounts used on this machine. Please upgrade to pro. We have this limit in place to prevent abuse. Please let us know if you believe @@ -125,98 +151,127 @@ this is a mistake. ### 💻 系统支持 -**Windows** ✅ x64 -**macOS** ✅ Intel和M系列 -**Linux** ✅ x64和ARM64 + + + + + + +
-### 📥 安装方法 +**Windows** ✅ +- x64 & x86 -#### 自动安装(推荐) + -**Linux/macOS** +**macOS** ✅ +- Intel & M-series + + + +**Linux** ✅ +- x64 & ARM64 + +
+ +### 🚀 一键解决 + +**Linux/macOS**: 在终端中复制粘贴 ```bash curl -fsSL https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.sh | sudo bash ``` -**Windows** (以管理员身份运行PowerShell) +**Windows**: 在PowerShell中复制粘贴 ```powershell irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex ``` -安装脚本会自动: -- 请求必要的权限(sudo/管理员) -- 关闭所有运行中的Cursor实例 -- 备份现有配置 -- 安装工具 -- 添加到系统PATH -- 清理临时文件 - -#### 手动安装 - -1. 从[发布页面](https://github.com/yuaotian/go-cursor-help/releases)下载适合您系统的最新版本 -2. 解压并以管理员/root权限运行: - ```bash - # Linux/macOS - chmod +x ./cursor_id_modifier_* # 添加执行权限 - sudo ./cursor_id_modifier_* - - # Windows (PowerShell 管理员) - .\cursor_id_modifier_*.exe - ``` - -#### 手动配置方法 - -1. 完全关闭 Cursor -2. 找到配置文件位置: - - Windows: `%APPDATA%\Cursor\User\globalStorage\storage.json` - - macOS: `~/Library/Application Support/Cursor/User/globalStorage/storage.json` - - Linux: `~/.config/Cursor/User/globalStorage/storage.json` -3. 备份 `storage.json` -4. 编辑 `storage.json` 并更新以下字段(使用新的随机UUID): - ```json - { - "telemetry.machineId": "生成新的uuid", - "telemetry.macMachineId": "生成新的uuid", - "telemetry.devDeviceId": "生成新的uuid", - "telemetry.sqmId": "生成新的uuid", - "lastModified": "2024-01-01T00:00:00.000Z", - "version": "1.0.1" - } - ``` -5. 保存文件并重启 Cursor +#### Windows 安装特性: +- 🔍 自动检测并使用 PowerShell 7(如果可用) +- 🛡️ 通过 UAC 提示请求管理员权限 +- 📝 如果没有 PS7 则使用 Windows PowerShell +- 💡 如果提权失败会提供手动说明 + +That's it! The script will: +1. ✨ 自动安装工具 +2. 🔄 立即重置Cursor试用期 + +### 📦 Manual Installation + +> Download the appropriate file for your system from [releases](https://github.com/yuaotian/go-cursor-help/releases/latest) + +
+Windows Packages + +- 64-bit: `cursor-id-modifier_windows_x64.exe` +- 32-bit: `cursor-id-modifier_windows_x86.exe` +
+ +
+macOS Packages + +- Intel: `cursor-id-modifier_darwin_x64_intel` +- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon` +
+ +
+Linux Packages + +- 64-bit: `cursor-id-modifier_linux_x64` +- 32-bit: `cursor-id-modifier_linux_x86` +- ARM64: `cursor-id-modifier_linux_arm64` +
### 🔧 技术细节 -#### 配置文件 +
+配置文件 + 程序修改Cursor的`storage.json`配置文件,位于: + - Windows: `%APPDATA%\Cursor\User\globalStorage\` - macOS: `~/Library/Application Support/Cursor/User/globalStorage/` - Linux: `~/.config/Cursor/User/globalStorage/` +
+ +
+修改字段 -#### 修改字段 工具会生成新的唯一标识符: - `telemetry.machineId` - `telemetry.macMachineId` - `telemetry.devDeviceId` - `telemetry.sqmId` +
-#### 安全特性 -- 自动备份现有配置 -- 安全的进程终止 -- 原子文件操作 -- 错误处理和回滚 +
+安全特性 -## ⭐ Star History or Repobeats +## 🔔 关注公众号 +#### 获取更多精彩内容 +- 第一时间获取最新版本更新 +- CursorAI使用技巧和最佳实践 +- 利用AI提升编程效率 +- 更多AI工具和开发资源 -[![Star History Chart](https://api.star-history.com/svg?repos=yuaotian/go-cursor-help&type=Date)](https://star-history.com/#yuaotian/go-cursor-help&Date) +![微信公众号二维码](img/wx_public_2.png) + +--- +## ⭐ Project Stats + +
+ +[![Star History Chart](https://api.star-history.com/svg?repos=yuaotian/go-cursor-help&type=Date)](https://star-history.com/#yuaotian/go-cursor-help&Date) ![Repobeats analytics image](https://repobeats.axiom.co/api/embed/ddaa9df9a94b0029ec3fad399e1c1c4e75755477.svg "Repobeats analytics image") +
## 📄 License -MIT License +
+MIT License Copyright (c) 2024 @@ -229,4 +284,4 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - +
diff --git a/cmd/cursor-id-modifier/main.go b/cmd/cursor-id-modifier/main.go new file mode 100644 index 0000000..89de5f3 --- /dev/null +++ b/cmd/cursor-id-modifier/main.go @@ -0,0 +1,330 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + "runtime/debug" + "strings" + + "github.com/yuaotian/go-cursor-help/internal/config" + "github.com/yuaotian/go-cursor-help/internal/lang" + "github.com/yuaotian/go-cursor-help/internal/process" + "github.com/yuaotian/go-cursor-help/internal/ui" + "github.com/yuaotian/go-cursor-help/pkg/idgen" + + "github.com/sirupsen/logrus" +) + +// Global variables +var ( + version = "dev" + setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode") + showVersion = flag.Bool("v", false, "show version information") + log = logrus.New() +) + +func main() { + setupErrorRecovery() + handleFlags() + setupLogger() + + username := getCurrentUser() + log.Debug("Running as user:", username) + + // Initialize components + display := ui.NewDisplay(nil) + configManager := initConfigManager(username) + generator := idgen.NewGenerator() + processManager := process.NewManager(nil, log) + + // Check and handle privileges + if err := handlePrivileges(display); err != nil { + return + } + + // Setup display + setupDisplay(display) + + text := lang.GetText() + + // Handle Cursor processes + if err := handleCursorProcesses(display, processManager); err != nil { + return + } + + // Handle configuration + oldConfig := readExistingConfig(display, configManager, text) + newConfig := generateNewConfig(display, generator, oldConfig, text) + + if err := saveConfiguration(display, configManager, newConfig); err != nil { + return + } + + // Show completion messages + showCompletionMessages(display) + + if os.Getenv("AUTOMATED_MODE") != "1" { + waitExit() + } +} + +func setupErrorRecovery() { + defer func() { + if r := recover(); r != nil { + log.Errorf("Panic recovered: %v\n", r) + debug.PrintStack() + waitExit() + } + }() +} + +func handleFlags() { + flag.Parse() + if *showVersion { + fmt.Printf("Cursor ID Modifier v%s\n", version) + os.Exit(0) + } +} + +func setupLogger() { + log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + DisableLevelTruncation: true, + PadLevelText: true, + }) + log.SetLevel(logrus.InfoLevel) +} + +func getCurrentUser() string { + if username := os.Getenv("SUDO_USER"); username != "" { + return username + } + + user, err := user.Current() + if err != nil { + log.Fatal(err) + } + return user.Username +} + +func initConfigManager(username string) *config.Manager { + configManager, err := config.NewManager(username) + if err != nil { + log.Fatal(err) + } + return configManager +} + +func handlePrivileges(display *ui.Display) error { + isAdmin, err := checkAdminPrivileges() + if err != nil { + log.Error(err) + waitExit() + return err + } + + if !isAdmin { + if runtime.GOOS == "windows" { + return handleWindowsPrivileges(display) + } + display.ShowPrivilegeError( + lang.GetText().PrivilegeError, + lang.GetText().RunWithSudo, + lang.GetText().SudoExample, + ) + waitExit() + return fmt.Errorf("insufficient privileges") + } + return nil +} + +func handleWindowsPrivileges(display *ui.Display) error { + message := "\nRequesting administrator privileges..." + if lang.GetCurrentLanguage() == lang.CN { + message = "\n请求管理员权限..." + } + fmt.Println(message) + + if err := selfElevate(); err != nil { + log.Error(err) + display.ShowPrivilegeError( + lang.GetText().PrivilegeError, + lang.GetText().RunAsAdmin, + lang.GetText().RunWithSudo, + lang.GetText().SudoExample, + ) + waitExit() + return err + } + return nil +} + +func setupDisplay(display *ui.Display) { + if err := display.ClearScreen(); err != nil { + log.Warn("Failed to clear screen:", err) + } + display.ShowLogo() + fmt.Println() +} + +func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error { + if os.Getenv("AUTOMATED_MODE") == "1" { + log.Debug("Running in automated mode, skipping Cursor process closing") + return nil + } + + display.ShowProgress("Closing Cursor...") + log.Debug("Attempting to close Cursor processes") + + if err := processManager.KillCursorProcesses(); err != nil { + log.Error("Failed to close Cursor:", err) + display.StopProgress() + display.ShowError("Failed to close Cursor. Please close it manually and try again.") + waitExit() + return err + } + + if processManager.IsCursorRunning() { + log.Error("Cursor processes still detected after closing") + display.StopProgress() + display.ShowError("Failed to close Cursor completely. Please close it manually and try again.") + waitExit() + return fmt.Errorf("cursor still running") + } + + log.Debug("Successfully closed all Cursor processes") + display.StopProgress() + fmt.Println() + return nil +} + +func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig { + fmt.Println() + display.ShowProgress(text.ReadingConfig) + oldConfig, err := configManager.ReadConfig() + if err != nil { + log.Warn("Failed to read existing config:", err) + oldConfig = nil + } + display.StopProgress() + fmt.Println() + return oldConfig +} + +func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig { + display.ShowProgress(text.GeneratingIds) + newConfig := &config.StorageConfig{} + + if machineID, err := generator.GenerateMachineID(); err != nil { + log.Fatal("Failed to generate machine ID:", err) + } else { + newConfig.TelemetryMachineId = machineID + } + + if macMachineID, err := generator.GenerateMacMachineID(); err != nil { + log.Fatal("Failed to generate MAC machine ID:", err) + } else { + newConfig.TelemetryMacMachineId = macMachineID + } + + if deviceID, err := generator.GenerateDeviceID(); err != nil { + log.Fatal("Failed to generate device ID:", err) + } else { + newConfig.TelemetryDevDeviceId = deviceID + } + + if oldConfig != nil && oldConfig.TelemetrySqmId != "" { + newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId + } else if sqmID, err := generator.GenerateSQMID(); err != nil { + log.Fatal("Failed to generate SQM ID:", err) + } else { + newConfig.TelemetrySqmId = sqmID + } + + display.StopProgress() + fmt.Println() + return newConfig +} + +func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error { + display.ShowProgress("Saving configuration...") + if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil { + log.Error(err) + waitExit() + return err + } + display.StopProgress() + fmt.Println() + return nil +} + +func showCompletionMessages(display *ui.Display) { + display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage) + fmt.Println() + + message := "Operation completed!" + if lang.GetCurrentLanguage() == lang.CN { + message = "操作完成!" + } + display.ShowInfo(message) +} + +func waitExit() { + fmt.Print(lang.GetText().PressEnterToExit) + os.Stdout.Sync() + bufio.NewReader(os.Stdin).ReadString('\n') +} + +func checkAdminPrivileges() (bool, error) { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("net", "session") + return cmd.Run() == nil, nil + + case "darwin", "linux": + currentUser, err := user.Current() + if err != nil { + return false, fmt.Errorf("failed to get current user: %w", err) + } + return currentUser.Uid == "0", nil + + default: + return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } +} + +func selfElevate() error { + os.Setenv("AUTOMATED_MODE", "1") + + switch runtime.GOOS { + case "windows": + verb := "runas" + exe, _ := os.Executable() + cwd, _ := os.Getwd() + args := strings.Join(os.Args[1:], " ") + + cmd := exec.Command("cmd", "/C", "start", verb, exe, args) + cmd.Dir = cwd + return cmd.Run() + + case "darwin", "linux": + exe, err := os.Executable() + if err != nil { + return err + } + + cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + + default: + return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } +} diff --git a/go.mod b/go.mod index 6b674f1..6bcebe3 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,15 @@ -module cursor-id-modifier +module github.com/yuaotian/go-cursor-help go 1.21 -require github.com/fatih/color v1.15.0 +require ( + github.com/fatih/color v1.15.0 + github.com/sirupsen/logrus v1.9.3 +) require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/stretchr/testify v1.10.0 // indirect golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index b4bb98d..d930a66 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -5,6 +8,19 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/img/wx_public.jpg b/img/wx_public.jpg new file mode 100644 index 0000000..2455fc5 Binary files /dev/null and b/img/wx_public.jpg differ diff --git a/img/wx_public_2.png b/img/wx_public_2.png new file mode 100644 index 0000000..5974ac8 Binary files /dev/null and b/img/wx_public_2.png differ diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..75b64df --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,153 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "time" +) + +// StorageConfig represents the storage configuration +type StorageConfig struct { + TelemetryMacMachineId string `json:"telemetry.macMachineId"` + TelemetryMachineId string `json:"telemetry.machineId"` + TelemetryDevDeviceId string `json:"telemetry.devDeviceId"` + TelemetrySqmId string `json:"telemetry.sqmId"` + LastModified string `json:"lastModified"` + Version string `json:"version"` +} + +// Manager handles configuration operations +type Manager struct { + configPath string + mu sync.RWMutex +} + +// NewManager creates a new configuration manager +func NewManager(username string) (*Manager, error) { + configPath, err := getConfigPath(username) + if err != nil { + return nil, fmt.Errorf("failed to get config path: %w", err) + } + return &Manager{configPath: configPath}, nil +} + +// ReadConfig reads the existing configuration +func (m *Manager) ReadConfig() (*StorageConfig, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + data, err := os.ReadFile(m.configPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config StorageConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + return &config, nil +} + +// SaveConfig saves the configuration +func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Ensure parent directories exist + if err := os.MkdirAll(filepath.Dir(m.configPath), 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Prepare updated configuration + updatedConfig := m.prepareUpdatedConfig(config) + + // Write configuration + if err := m.writeConfigFile(updatedConfig, readOnly); err != nil { + return err + } + + return nil +} + +// prepareUpdatedConfig merges existing config with updates +func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} { + // Read existing config + originalFile := make(map[string]interface{}) + if data, err := os.ReadFile(m.configPath); err == nil { + json.Unmarshal(data, &originalFile) + } + + // Update fields + originalFile["telemetry.sqmId"] = config.TelemetrySqmId + originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId + originalFile["telemetry.machineId"] = config.TelemetryMachineId + originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId + originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) + // originalFile["version"] = "1.0.1" + + return originalFile +} + +// writeConfigFile handles the atomic write of the config file +func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error { + // Marshal with indentation + content, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + // Write to temporary file + tmpPath := m.configPath + ".tmp" + if err := os.WriteFile(tmpPath, content, 0666); err != nil { + return fmt.Errorf("failed to write temporary file: %w", err) + } + + // Set final permissions + fileMode := os.FileMode(0666) + if readOnly { + fileMode = 0444 + } + + if err := os.Chmod(tmpPath, fileMode); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to set temporary file permissions: %w", err) + } + + // Atomic rename + if err := os.Rename(tmpPath, m.configPath); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("failed to rename file: %w", err) + } + + // Sync directory + if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { + defer dir.Close() + dir.Sync() + } + + return nil +} + +// getConfigPath returns the path to the configuration file +func getConfigPath(username string) (string, error) { + var configDir string + switch runtime.GOOS { + case "windows": + configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage") + case "darwin": + configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage") + case "linux": + configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage") + default: + return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + return filepath.Join(configDir, "storage.json"), nil +} diff --git a/internal/lang/lang.go b/internal/lang/lang.go new file mode 100644 index 0000000..973fc24 --- /dev/null +++ b/internal/lang/lang.go @@ -0,0 +1,187 @@ +package lang + +import ( + "os" + "os/exec" + "strings" + "sync" +) + +// Language represents a supported language code +type Language string + +const ( + // CN represents Chinese language + CN Language = "cn" + // EN represents English language + EN Language = "en" +) + +// TextResource contains all translatable text resources +type TextResource struct { + // Success messages + SuccessMessage string + RestartMessage string + + // Progress messages + ReadingConfig string + GeneratingIds string + CheckingProcesses string + ClosingProcesses string + ProcessesClosed string + PleaseWait string + + // Error messages + ErrorPrefix string + PrivilegeError string + + // Instructions + RunAsAdmin string + RunWithSudo string + SudoExample string + PressEnterToExit string + SetReadOnlyMessage string + + // Info messages + ConfigLocation string +} + +var ( + currentLanguage Language + currentLanguageOnce sync.Once + languageMutex sync.RWMutex +) + +// GetCurrentLanguage returns the current language, detecting it if not already set +func GetCurrentLanguage() Language { + currentLanguageOnce.Do(func() { + currentLanguage = detectLanguage() + }) + + languageMutex.RLock() + defer languageMutex.RUnlock() + return currentLanguage +} + +// SetLanguage sets the current language +func SetLanguage(lang Language) { + languageMutex.Lock() + defer languageMutex.Unlock() + currentLanguage = lang +} + +// GetText returns the TextResource for the current language +func GetText() TextResource { + return texts[GetCurrentLanguage()] +} + +// detectLanguage detects the system language +func detectLanguage() Language { + // Check environment variables first + if isChineseEnvVar() { + return CN + } + + // Then check OS-specific locale + if isWindows() { + if isWindowsChineseLocale() { + return CN + } + } else if isUnixChineseLocale() { + return CN + } + + return EN +} + +func isChineseEnvVar() bool { + for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { + if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") { + return true + } + } + return false +} + +func isWindows() bool { + return os.Getenv("OS") == "Windows_NT" +} + +func isWindowsChineseLocale() bool { + // Check Windows UI culture + cmd := exec.Command("powershell", "-Command", + "[System.Globalization.CultureInfo]::CurrentUICulture.Name") + output, err := cmd.Output() + if err == nil && strings.HasPrefix(strings.ToLower(strings.TrimSpace(string(output))), "zh") { + return true + } + + // Check Windows locale + cmd = exec.Command("wmic", "os", "get", "locale") + output, err = cmd.Output() + return err == nil && strings.Contains(string(output), "2052") +} + +func isUnixChineseLocale() bool { + cmd := exec.Command("locale") + output, err := cmd.Output() + return err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn") +} + +// texts contains all translations +var texts = map[Language]TextResource{ + CN: { + // Success messages + SuccessMessage: "[√] 配置文件已成功更新!", + RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", + + // Progress messages + ReadingConfig: "正在读取配置文件...", + GeneratingIds: "正在生成新的标识符...", + CheckingProcesses: "正在检查运行中的 Cursor 实例...", + ClosingProcesses: "正在关闭 Cursor 实例...", + ProcessesClosed: "所有 Cursor 实例已关闭", + PleaseWait: "请稍候...", + + // Error messages + ErrorPrefix: "程序发生严重错误: %v", + PrivilegeError: "\n[!] 错误:需要管理员权限", + + // Instructions + RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」", + RunWithSudo: "请使用 sudo 命令运行此程序", + SudoExample: "示例: sudo %s", + PressEnterToExit: "\n按回车键退出程序...", + SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", + + // Info messages + ConfigLocation: "配置文件位置:", + }, + EN: { + // Success messages + SuccessMessage: "[√] Configuration file updated successfully!", + RestartMessage: "[!] Please restart Cursor manually for changes to take effect", + + // Progress messages + ReadingConfig: "Reading configuration file...", + GeneratingIds: "Generating new identifiers...", + CheckingProcesses: "Checking for running Cursor instances...", + ClosingProcesses: "Closing Cursor instances...", + ProcessesClosed: "All Cursor instances have been closed", + PleaseWait: "Please wait...", + + // Error messages + ErrorPrefix: "Program encountered a serious error: %v", + PrivilegeError: "\n[!] Error: Administrator privileges required", + + // Instructions + RunAsAdmin: "Please right-click and select 'Run as Administrator'", + RunWithSudo: "Please run this program with sudo", + SudoExample: "Example: sudo %s", + PressEnterToExit: "\nPress Enter to exit...", + SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", + + // Info messages + ConfigLocation: "Config file location:", + }, +} diff --git a/internal/process/manager.go b/internal/process/manager.go new file mode 100644 index 0000000..f48ac20 --- /dev/null +++ b/internal/process/manager.go @@ -0,0 +1,216 @@ +package process + +import ( + "fmt" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +// Config holds process manager configuration +type Config struct { + MaxAttempts int // Maximum number of attempts to kill processes + RetryDelay time.Duration // Delay between retry attempts + ProcessPatterns []string // Process names to look for +} + +// DefaultConfig returns the default configuration +func DefaultConfig() *Config { + return &Config{ + MaxAttempts: 3, + RetryDelay: 2 * time.Second, + ProcessPatterns: []string{ + "Cursor.exe", // Windows executable + "Cursor ", // Linux/macOS executable with space + "cursor ", // Linux/macOS executable lowercase with space + "cursor", // Linux/macOS executable lowercase + "Cursor", // Linux/macOS executable + "*cursor*", // Any process containing cursor + "*Cursor*", // Any process containing Cursor + }, + } +} + +// Manager handles process-related operations +type Manager struct { + config *Config + log *logrus.Logger +} + +// NewManager creates a new process manager with optional config and logger +func NewManager(config *Config, log *logrus.Logger) *Manager { + if config == nil { + config = DefaultConfig() + } + if log == nil { + log = logrus.New() + } + return &Manager{ + config: config, + log: log, + } +} + +// IsCursorRunning checks if any Cursor process is currently running +func (m *Manager) IsCursorRunning() bool { + processes, err := m.getCursorProcesses() + if err != nil { + m.log.Warn("Failed to get Cursor processes:", err) + return false + } + return len(processes) > 0 +} + +// KillCursorProcesses attempts to kill all running Cursor processes +func (m *Manager) KillCursorProcesses() error { + for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ { + processes, err := m.getCursorProcesses() + if err != nil { + return fmt.Errorf("failed to get processes: %w", err) + } + + if len(processes) == 0 { + return nil + } + + // Try graceful shutdown first on Windows + if runtime.GOOS == "windows" { + for _, pid := range processes { + exec.Command("taskkill", "/PID", pid).Run() + time.Sleep(500 * time.Millisecond) + } + } + + // Force kill remaining processes + remainingProcesses, _ := m.getCursorProcesses() + for _, pid := range remainingProcesses { + m.killProcess(pid) + } + + time.Sleep(m.config.RetryDelay) + + if processes, _ := m.getCursorProcesses(); len(processes) == 0 { + return nil + } + } + + return nil +} + +// getCursorProcesses returns PIDs of running Cursor processes +func (m *Manager) getCursorProcesses() ([]string, error) { + cmd := m.getProcessListCommand() + if cmd == nil { + return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute command: %w", err) + } + + return m.parseProcessList(string(output)), nil +} + +// getProcessListCommand returns the appropriate command to list processes based on OS +func (m *Manager) getProcessListCommand() *exec.Cmd { + switch runtime.GOOS { + case "windows": + return exec.Command("tasklist", "/FO", "CSV", "/NH") + case "darwin": + return exec.Command("ps", "-ax") + case "linux": + return exec.Command("ps", "-A") + default: + return nil + } +} + +// parseProcessList extracts Cursor process PIDs from process list output +func (m *Manager) parseProcessList(output string) []string { + var processes []string + for _, line := range strings.Split(output, "\n") { + lowerLine := strings.ToLower(line) + + if m.isOwnProcess(lowerLine) { + continue + } + + if pid := m.findCursorProcess(line, lowerLine); pid != "" { + processes = append(processes, pid) + } + } + return processes +} + +// isOwnProcess checks if the process belongs to this application +func (m *Manager) isOwnProcess(line string) bool { + return strings.Contains(line, "cursor-id-modifier") || + strings.Contains(line, "cursor-helper") +} + +// findCursorProcess checks if a process line matches Cursor patterns and returns its PID +func (m *Manager) findCursorProcess(line, lowerLine string) string { + for _, pattern := range m.config.ProcessPatterns { + if m.matchPattern(lowerLine, strings.ToLower(pattern)) { + return m.extractPID(line) + } + } + return "" +} + +// matchPattern checks if a line matches a pattern, supporting wildcards +func (m *Manager) matchPattern(line, pattern string) bool { + switch { + case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"): + search := pattern[1 : len(pattern)-1] + return strings.Contains(line, search) + case strings.HasPrefix(pattern, "*"): + return strings.HasSuffix(line, pattern[1:]) + case strings.HasSuffix(pattern, "*"): + return strings.HasPrefix(line, pattern[:len(pattern)-1]) + default: + return line == pattern + } +} + +// extractPID extracts process ID from a process list line based on OS format +func (m *Manager) extractPID(line string) string { + switch runtime.GOOS { + case "windows": + parts := strings.Split(line, ",") + if len(parts) >= 2 { + return strings.Trim(parts[1], "\"") + } + case "darwin", "linux": + parts := strings.Fields(line) + if len(parts) >= 1 { + return parts[0] + } + } + return "" +} + +// killProcess forcefully terminates a process by PID +func (m *Manager) killProcess(pid string) error { + cmd := m.getKillCommand(pid) + if cmd == nil { + return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + return cmd.Run() +} + +// getKillCommand returns the appropriate command to kill a process based on OS +func (m *Manager) getKillCommand(pid string) *exec.Cmd { + switch runtime.GOOS { + case "windows": + return exec.Command("taskkill", "/F", "/PID", pid) + case "darwin", "linux": + return exec.Command("kill", "-9", pid) + default: + return nil + } +} diff --git a/internal/ui/display.go b/internal/ui/display.go new file mode 100644 index 0000000..32ed566 --- /dev/null +++ b/internal/ui/display.go @@ -0,0 +1,94 @@ +package ui + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/fatih/color" +) + +// Display handles UI operations for terminal output +type Display struct { + spinner *Spinner +} + +// NewDisplay creates a new display instance with an optional spinner +func NewDisplay(spinner *Spinner) *Display { + if spinner == nil { + spinner = NewSpinner(nil) + } + return &Display{spinner: spinner} +} + +// Terminal Operations + +// ClearScreen clears the terminal screen based on OS +func (d *Display) ClearScreen() error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("cmd", "/c", "cls") + default: + cmd = exec.Command("clear") + } + cmd.Stdout = os.Stdout + return cmd.Run() +} + +// Progress Indicator + +// ShowProgress displays a progress message with a spinner +func (d *Display) ShowProgress(message string) { + d.spinner.SetMessage(message) + d.spinner.Start() +} + +// StopProgress stops the progress spinner +func (d *Display) StopProgress() { + d.spinner.Stop() +} + +// Message Display + +// ShowSuccess displays success messages in green +func (d *Display) ShowSuccess(messages ...string) { + green := color.New(color.FgGreen) + for _, msg := range messages { + green.Println(msg) + } +} + +// ShowInfo displays an info message in cyan +func (d *Display) ShowInfo(message string) { + cyan := color.New(color.FgCyan) + cyan.Println(message) +} + +// ShowError displays an error message in red +func (d *Display) ShowError(message string) { + red := color.New(color.FgRed) + red.Println(message) +} + +// ShowPrivilegeError displays privilege error messages with instructions +func (d *Display) ShowPrivilegeError(messages ...string) { + red := color.New(color.FgRed, color.Bold) + yellow := color.New(color.FgYellow) + + // Main error message + red.Println(messages[0]) + fmt.Println() + + // Additional instructions + for _, msg := range messages[1:] { + if strings.Contains(msg, "%s") { + exe, _ := os.Executable() + yellow.Printf(msg+"\n", exe) + } else { + yellow.Println(msg) + } + } +} diff --git a/internal/ui/logo.go b/internal/ui/logo.go new file mode 100644 index 0000000..564525a --- /dev/null +++ b/internal/ui/logo.go @@ -0,0 +1,20 @@ +package ui + +import ( + "github.com/fatih/color" +) + +const cyberpunkLogo = ` + ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ + ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ + ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ + ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ + ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ +` + +// ShowLogo displays the application logo +func (d *Display) ShowLogo() { + cyan := color.New(color.FgCyan, color.Bold) + cyan.Println(cyberpunkLogo) +} diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go new file mode 100644 index 0000000..145f730 --- /dev/null +++ b/internal/ui/spinner.go @@ -0,0 +1,122 @@ +package ui + +import ( + "fmt" + "sync" + "time" + + "github.com/fatih/color" +) + +// SpinnerConfig defines spinner configuration +type SpinnerConfig struct { + Frames []string // Animation frames for the spinner + Delay time.Duration // Delay between frame updates +} + +// DefaultSpinnerConfig returns the default spinner configuration +func DefaultSpinnerConfig() *SpinnerConfig { + return &SpinnerConfig{ + Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, + Delay: 100 * time.Millisecond, + } +} + +// Spinner represents a progress spinner +type Spinner struct { + config *SpinnerConfig + message string + current int + active bool + stopCh chan struct{} + mu sync.RWMutex +} + +// NewSpinner creates a new spinner with the given configuration +func NewSpinner(config *SpinnerConfig) *Spinner { + if config == nil { + config = DefaultSpinnerConfig() + } + return &Spinner{ + config: config, + stopCh: make(chan struct{}), + } +} + +// State management + +// SetMessage sets the spinner message +func (s *Spinner) SetMessage(message string) { + s.mu.Lock() + defer s.mu.Unlock() + s.message = message +} + +// IsActive returns whether the spinner is currently active +func (s *Spinner) IsActive() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.active +} + +// Control methods + +// Start begins the spinner animation +func (s *Spinner) Start() { + s.mu.Lock() + if s.active { + s.mu.Unlock() + return + } + s.active = true + s.mu.Unlock() + + go s.run() +} + +// Stop halts the spinner animation +func (s *Spinner) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.active { + return + } + + s.active = false + close(s.stopCh) + s.stopCh = make(chan struct{}) + fmt.Print("\r") // Clear the spinner line +} + +// Internal methods + +func (s *Spinner) run() { + ticker := time.NewTicker(s.config.Delay) + defer ticker.Stop() + + cyan := color.New(color.FgCyan, color.Bold) + message := s.message + + // Print initial state + fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message) + + for { + select { + case <-s.stopCh: + return + case <-ticker.C: + s.mu.RLock() + if !s.active { + s.mu.RUnlock() + return + } + frame := s.config.Frames[s.current%len(s.config.Frames)] + s.current++ + s.mu.RUnlock() + + fmt.Printf("\r %s", cyan.Sprint(frame)) + fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message + } + } +} diff --git a/main.go b/main.go deleted file mode 100644 index 88520cc..0000000 --- a/main.go +++ /dev/null @@ -1,1050 +0,0 @@ -package main - -// Core imports / 核心导入 -import ( - "bufio" - "context" - cryptorand "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "flag" - "fmt" - "log" - "math/rand" - "os" - "os/exec" - "os/user" - "path/filepath" - "runtime" - "runtime/debug" - "strings" - "time" - - "github.com/fatih/color" -) - -// Version information -var version = "dev" // This will be overwritten by goreleaser - -// Types and Constants / 类型和常量 -type Language string - -const ( - // Language options / 语言选项 - CN Language = "cn" - EN Language = "en" - - // Error types / 错误类型 - ErrPermission = "permission_error" - ErrConfig = "config_error" - ErrProcess = "process_error" - ErrSystem = "system_error" -) - -// Configuration Structures / 配置结构 -type ( - // TextResource stores multilingual text / 存储多语言文本 - TextResource struct { - SuccessMessage string - RestartMessage string - ReadingConfig string - GeneratingIds string - PressEnterToExit string - ErrorPrefix string - PrivilegeError string - RunAsAdmin string - RunWithSudo string - SudoExample string - ConfigLocation string - CheckingProcesses string - ClosingProcesses string - ProcessesClosed string - PleaseWait string - SetReadOnlyMessage string - } - - // StorageConfig optimized storage configuration / 优化的存储配置 - StorageConfig struct { - TelemetryMacMachineId string `json:"telemetry.macMachineId"` - TelemetryMachineId string `json:"telemetry.machineId"` - TelemetryDevDeviceId string `json:"telemetry.devDeviceId"` - TelemetrySqmId string `json:"telemetry.sqmId"` - } - // AppError defines error types / 定义错误类型 - AppError struct { - Type string - Op string - Path string - Err error - Context map[string]interface{} - } - - // Config structures / 配置结构 - Config struct { - Storage StorageConfig - UI UIConfig - System SystemConfig - } - - UIConfig struct { - Language Language - Theme string - Spinner SpinnerConfig - } - - SystemConfig struct { - RetryAttempts int - RetryDelay time.Duration - Timeout time.Duration - } - - // SpinnerConfig defines spinner configuration / 定义进度条配置 - SpinnerConfig struct { - Frames []string - Delay time.Duration - } - - // ProgressSpinner for showing progress animation / 用于显示进度动画 - ProgressSpinner struct { - frames []string - current int - message string - } -) - -// Global Variables / 全局变量 -var ( - currentLanguage = CN // Default to Chinese / 默认为中文 - - texts = map[Language]TextResource{ - CN: { - SuccessMessage: "[√] 配置文件已成功更新!", - RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", - ReadingConfig: "正在读取配置文件...", - GeneratingIds: "正在生成新的标识符...", - PressEnterToExit: "按回车键退出程序...", - ErrorPrefix: "程序发生严重错误: %v", - PrivilegeError: "\n[!] 错误:需要管理员权限", - RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」", - RunWithSudo: "请使用 sudo 命令运行此程序", - SudoExample: "示例: sudo %s", - ConfigLocation: "配置文件位置:", - CheckingProcesses: "正在检查运行中的 Cursor 实例...", - ClosingProcesses: "正在关闭 Cursor 实例...", - ProcessesClosed: "所有 Cursor 实例已关闭", - PleaseWait: "请稍候...", - SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", - }, - EN: { - SuccessMessage: "[√] Configuration file updated successfully!", - RestartMessage: "[!] Please restart Cursor manually for changes to take effect", - ReadingConfig: "Reading configuration file...", - GeneratingIds: "Generating new identifiers...", - PressEnterToExit: "Press Enter to exit...", - ErrorPrefix: "Program encountered a serious error: %v", - PrivilegeError: "\n[!] Error: Administrator privileges required", - RunAsAdmin: "Please right-click and select 'Run as Administrator'", - RunWithSudo: "Please run this program with sudo", - SudoExample: "Example: sudo %s", - ConfigLocation: "Config file location:", - CheckingProcesses: "Checking for running Cursor instances...", - ClosingProcesses: "Closing Cursor instances...", - ProcessesClosed: "All Cursor instances have been closed", - PleaseWait: "Please wait...", - SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", - }, - } -) - -var setReadOnly *bool = flag.Bool("r", false, "set storage.json to read-only mode") - -// Error Implementation / 错误实现 -func (e *AppError) Error() string { - if e.Context != nil { - return fmt.Sprintf("[%s] %s: %v (context: %v)", e.Type, e.Op, e.Err, e.Context) - } - return fmt.Sprintf("[%s] %s: %v", e.Type, e.Op, e.Err) -} - -// Configuration Functions / 配置函数 -func NewStorageConfig(oldConfig *StorageConfig) *StorageConfig { - // Use different ID generation functions for different fields - // 为不同字段用不同的ID生成函数 - // Reason: machineId needs new format while others keep old format - // 原因:machineId需要使用新格式,而其他ID保持旧格式 - newConfig := &StorageConfig{ - TelemetryMacMachineId: generateMacMachineId(), // Use old format / 使用旧格式 - TelemetryMachineId: generateMachineId(), // Use new format / 使用新格式 - TelemetryDevDeviceId: generateDevDeviceId(), - } - - // Keep sqmId from old config or generate new one using old format - // 保留旧配置的sqmId或使用旧格式生成新的 - if oldConfig != nil { - newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId - } else { - newConfig.TelemetrySqmId = generateMacMachineId() - } - - if newConfig.TelemetrySqmId == "" { - newConfig.TelemetrySqmId = generateMacMachineId() - } - - return newConfig -} - -func generateMachineId() string { - // 基础结构:auth0|user_XX[unique_id] - prefix := "auth0|user_" - - // 生成两位数字序列 (00-99) - sequence := fmt.Sprintf("%02d", rand.Intn(100)) - - // 生成唯一标识部分 (23字符) - uniqueId := generateUniqueId(23) - - // 组合完整ID - fullId := prefix + sequence + uniqueId - - // 转换为十六进制 - return hex.EncodeToString([]byte(fullId)) -} - -func generateUniqueId(length int) string { - // 字符集:使用类似 Crockford's Base32 的字符集 - const charset = "0123456789ABCDEFGHJKLMNPQRSTVWXYZ" - - // 生成随机字符串 - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) -} - -func generateMacMachineId() string { - data := make([]byte, 32) - if _, err := cryptorand.Read(data); err != nil { - panic(fmt.Errorf("failed to generate random data: %v", err)) - } - hash := sha256.Sum256(data) - return hex.EncodeToString(hash[:]) -} - -func generateDevDeviceId() string { - uuid := make([]byte, 16) - if _, err := cryptorand.Read(uuid); err != nil { - panic(fmt.Errorf("failed to generate UUID: %v", err)) - } - uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant - return fmt.Sprintf("%x-%x-%x-%x-%x", - uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]) -} - -// File Operations / 文件操作 -func getConfigPath(username string) (string, error) { - var configDir string - switch runtime.GOOS { - case "windows": - configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage") - case "darwin": - configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage") - case "linux": - configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage") - default: - return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) - } - return filepath.Join(configDir, "storage.json"), nil -} - -func saveConfig(config *StorageConfig, username string) error { - configPath, err := getConfigPath(username) - if err != nil { - return err - } - - // Create parent directories with proper permissions - dir := filepath.Dir(configPath) - if err := os.MkdirAll(dir, 0755); err != nil { - return &AppError{ - Type: ErrSystem, - Op: "create directory", - Path: dir, - Err: err, - } - } - - // First ensure we can write to the file - if err := os.Chmod(configPath, 0666); err != nil && !os.IsNotExist(err) { - return &AppError{ - Type: ErrSystem, - Op: "modify file permissions", - Path: configPath, - Err: err, - } - } - - // Read the original file to preserve all fields - var originalFile map[string]interface{} - originalFileContent, err := os.ReadFile(configPath) - if err != nil { - if !os.IsNotExist(err) { - return &AppError{ - Type: ErrSystem, - Op: "read original file", - Path: configPath, - Err: err, - } - } - // If file doesn't exist, create a new map - originalFile = make(map[string]interface{}) - } else { - if err := json.Unmarshal(originalFileContent, &originalFile); err != nil { - return &AppError{ - Type: ErrSystem, - Op: "unmarshal original file", - Path: configPath, - Err: err, - } - } - } - - // Get original file mode - var originalFileMode os.FileMode = 0666 - if stat, err := os.Stat(configPath); err == nil { - originalFileMode = stat.Mode() - } - - // Update only the telemetry fields while preserving all other fields - originalFile["telemetry.sqmId"] = config.TelemetrySqmId - originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId - originalFile["telemetry.machineId"] = config.TelemetryMachineId - originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId - - // Add lastModified and version fields if they don't exist - if _, exists := originalFile["lastModified"]; !exists { - originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) - } - if _, exists := originalFile["version"]; !exists { - originalFile["version"] = "1.0.1" - } - - // Marshal with indentation - newFileContent, err := json.MarshalIndent(originalFile, "", " ") - if err != nil { - return &AppError{ - Type: ErrSystem, - Op: "marshal new file", - Path: configPath, - Err: err, - } - } - - // Write to temporary file first - tmpPath := configPath + ".tmp" - if err := os.WriteFile(tmpPath, newFileContent, 0666); err != nil { - return &AppError{ - Type: ErrSystem, - Op: "write temporary file", - Path: tmpPath, - Err: err, - } - } - - if *setReadOnly { - originalFileMode = 0444 - } - - // Ensure proper permissions on temporary file - if err := os.Chmod(tmpPath, originalFileMode); err != nil { - os.Remove(tmpPath) - return &AppError{ - Type: ErrSystem, - Op: "set temporary file permissions", - Path: tmpPath, - Err: err, - } - } - - // Atomic rename - if err := os.Rename(tmpPath, configPath); err != nil { - os.Remove(tmpPath) - return &AppError{ - Type: ErrSystem, - Op: "rename file", - Path: configPath, - Err: err, - } - } - - // Sync the directory to ensure changes are written to disk - if dir, err := os.Open(filepath.Dir(configPath)); err == nil { - dir.Sync() - dir.Close() - } - - return nil -} - -func readExistingConfig(username string) (*StorageConfig, error) { // Modified to take username - configPath, err := getConfigPath(username) - if err != nil { - return nil, err - } - - data, err := os.ReadFile(configPath) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - - var config StorageConfig - if err := json.Unmarshal(data, &config); err != nil { - return nil, err - } - - return &config, nil -} - -// Process Management / 进程管理 -type ProcessManager struct { - config *SystemConfig -} - -func (pm *ProcessManager) killCursorProcesses() error { - ctx, cancel := context.WithTimeout(context.Background(), pm.config.Timeout) - defer cancel() - - for attempt := 0; attempt < pm.config.RetryAttempts; attempt++ { - if err := pm.killProcess(ctx); err != nil { - time.Sleep(pm.config.RetryDelay) - continue - } - return nil - } - - return &AppError{ - Type: ErrProcess, - Op: "kill_processes", - Err: errors.New("failed to kill all Cursor processes after retries"), - } -} - -func (pm *ProcessManager) killProcess(ctx context.Context) error { - if runtime.GOOS == "windows" { - return pm.killWindowsProcess(ctx) - } - return pm.killUnixProcess(ctx) -} - -func (pm *ProcessManager) killWindowsProcess(ctx context.Context) error { - exec.CommandContext(ctx, "taskkill", "/IM", "Cursor.exe").Run() - time.Sleep(pm.config.RetryDelay) - exec.CommandContext(ctx, "taskkill", "/F", "/IM", "Cursor.exe").Run() - return nil -} - -func (pm *ProcessManager) killUnixProcess(ctx context.Context) error { - // Search for the process by it's executable name (AppRun) in ps output - cmd := exec.CommandContext(ctx, "ps", "aux") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to execute ps command: %w", err) - } - - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.Contains(line, "AppRun") { - parts := strings.Fields(line) - if len(parts) > 1 { - pid := parts[1] - if err := pm.forceKillProcess(ctx, pid); err != nil { - return err - } - } - } - - // handle lowercase - if strings.Contains(line, "apprun") { - parts := strings.Fields(line) - if len(parts) > 1 { - pid := parts[1] - if err := pm.forceKillProcess(ctx, pid); err != nil { - return err - } - } - } - } - - return nil -} - -// helper function to kill process by pid -func (pm *ProcessManager) forceKillProcess(ctx context.Context, pid string) error { - // First try graceful termination - if err := exec.CommandContext(ctx, "kill", pid).Run(); err == nil { - // Wait for processes to terminate gracefully - time.Sleep(2 * time.Second) - } - - // Force kill if still running - if err := exec.CommandContext(ctx, "kill", "-9", pid).Run(); err != nil { - return fmt.Errorf("failed to force kill process %s: %w", pid, err) - } - - return nil -} - -func checkCursorRunning() bool { - cmd := exec.Command("ps", "aux") - output, err := cmd.Output() - if err != nil { - return false - } - - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.Contains(line, "AppRun") || strings.Contains(line, "apprun") { - return true - } - } - - return false -} - -// UI Components / UI组件 -type UI struct { - config *UIConfig - spinner *ProgressSpinner -} - -func NewUI(config *UIConfig) *UI { - return &UI{ - config: config, - spinner: NewProgressSpinner(""), - } -} - -func (ui *UI) showProgress(message string) { - ui.spinner.message = message - ui.spinner.Start() - defer ui.spinner.Stop() - - ticker := time.NewTicker(ui.config.Spinner.Delay) - defer ticker.Stop() - - for i := 0; i < 15; i++ { - <-ticker.C - ui.spinner.Spin() - } -} - -// Display Functions / 显示函数 -func showSuccess() { - text := texts[currentLanguage] - successColor := color.New(color.FgGreen, color.Bold) - warningColor := color.New(color.FgYellow, color.Bold) - pathColor := color.New(color.FgCyan) - - // Clear any previous output - fmt.Println() - - if currentLanguage == EN { - // English messages with extra spacing - successColor.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - successColor.Printf("%s\n", text.SuccessMessage) - fmt.Println() - warningColor.Printf("%s\n", text.RestartMessage) - successColor.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - } else { - // Chinese messages with extra spacing - successColor.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - successColor.Printf("%s\n", text.SuccessMessage) - fmt.Println() - warningColor.Printf("%s\n", text.RestartMessage) - successColor.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - } - - // Add spacing before config location - fmt.Println() - - username := os.Getenv("SUDO_USER") - if username == "" { - user, err := user.Current() - if err != nil { - panic(err) - } - username = user.Username - } - if configPath, err := getConfigPath(username); err == nil { - pathColor.Printf("%s\n%s\n", text.ConfigLocation, configPath) - } -} - -func showReadOnlyMessage() { - if *setReadOnly { - warningColor := color.New(color.FgYellow, color.Bold) - warningColor.Printf("%s\n", texts[currentLanguage].SetReadOnlyMessage) - fmt.Println("Press Enter to continue...") - bufio.NewReader(os.Stdin).ReadString('\n') - } -} - -func showPrivilegeError() { - text := texts[currentLanguage] - red := color.New(color.FgRed, color.Bold) - yellow := color.New(color.FgYellow) - - if currentLanguage == EN { - red.Println(text.PrivilegeError) - if runtime.GOOS == "windows" { - yellow.Println(text.RunAsAdmin) - } else { - yellow.Printf("%s\n%s\n", text.RunWithSudo, fmt.Sprintf(text.SudoExample, os.Args[0])) - } - } else { - red.Printf("\n%s\n", text.PrivilegeError) - if runtime.GOOS == "windows" { - yellow.Printf("%s\n", text.RunAsAdmin) - } else { - yellow.Printf("%s\n%s\n", text.RunWithSudo, fmt.Sprintf(text.SudoExample, os.Args[0])) - } - } -} - -// System Functions / 系统函数 -func checkAdminPrivileges() (bool, error) { - switch runtime.GOOS { - case "windows": - // 使用更可靠的方法检查Windows管理员权限 - cmd := exec.Command("net", "session") - err := cmd.Run() - if err == nil { - return true, nil - } - // 如果命令执行失败,说明没有管理员权限 - return false, nil - - case "darwin", "linux": - currentUser, err := user.Current() - if err != nil { - return false, fmt.Errorf("failed to get current user: %v", err) - } - return currentUser.Uid == "0", nil - - default: - return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) - } -} - -func detectLanguage() Language { - // Check common environment variables - for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { - if lang := os.Getenv(envVar); lang != "" { - if strings.Contains(strings.ToLower(lang), "zh") { - return CN - } - } - } - - // Windows-specific language check - if runtime.GOOS == "windows" { - cmd := exec.Command("powershell", "-Command", - "[System.Globalization.CultureInfo]::CurrentUICulture.Name") - output, err := cmd.Output() - if err == nil { - lang := strings.ToLower(strings.TrimSpace(string(output))) - if strings.HasPrefix(lang, "zh") { - return CN - } - } - - // Check Windows locale - cmd = exec.Command("wmic", "os", "get", "locale") - output, err = cmd.Output() - if err == nil && strings.Contains(string(output), "2052") { - return CN - } - } - - // Check Unix locale - if runtime.GOOS != "windows" { - cmd := exec.Command("locale") - output, err := cmd.Output() - if err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn") { - return CN - } - } - - return EN -} - -func selfElevate() error { - switch runtime.GOOS { - case "windows": - // Set automated mode for the elevated process - os.Setenv("AUTOMATED_MODE", "1") - - verb := "runas" - exe, _ := os.Executable() - cwd, _ := os.Getwd() - args := strings.Join(os.Args[1:], " ") - - cmd := exec.Command("cmd", "/C", "start", verb, exe, args) - cmd.Dir = cwd - return cmd.Run() - - case "darwin", "linux": - // Set automated mode for the elevated process - os.Setenv("AUTOMATED_MODE", "1") - - exe, err := os.Executable() - if err != nil { - return err - } - - cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - - default: - return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) - } -} - -// Utility Functions / 实用函数 -func handleError(err error) { - if err == nil { - return - } - - logger := log.New(os.Stderr, "", log.LstdFlags) - - switch e := err.(type) { - case *AppError: - logger.Printf("[ERROR] %v\n", e) - if e.Type == ErrPermission { - showPrivilegeError() - } - default: - logger.Printf("[ERROR] Unexpected error: %v\n", err) - } -} - -func waitExit() { - // Skip waiting in automated mode - if os.Getenv("AUTOMATED_MODE") == "1" { - return - } - - if currentLanguage == EN { - fmt.Println("\nPress Enter to exit...") - } else { - fmt.Println("\n按��车键退出程序...") - } - os.Stdout.Sync() - bufio.NewReader(os.Stdin).ReadString('\n') -} - -// Add this new function near the other process management functions -func ensureCursorClosed() error { - maxAttempts := 3 - text := texts[currentLanguage] - - showProcessStatus(text.CheckingProcesses) - - for attempt := 1; attempt <= maxAttempts; attempt++ { - if !checkCursorRunning() { - showProcessStatus(text.ProcessesClosed) - fmt.Println() // New line after status - return nil - } - - if currentLanguage == EN { - showProcessStatus(fmt.Sprintf("Please close Cursor before continuing. Attempt %d/%d\n%s", - attempt, maxAttempts, text.PleaseWait)) - } else { - showProcessStatus(fmt.Sprintf("请在继续之前关闭 Cursor。尝试 %d/%d\n%s", - attempt, maxAttempts, text.PleaseWait)) - } - - time.Sleep(5 * time.Second) - } - - return errors.New("cursor is still running") -} - -func main() { - // Initialize error recovery - defer func() { - if r := recover(); r != nil { - log.Printf("Panic recovered: %v\n", r) - debug.PrintStack() - waitExit() - } - }() - - flag.Parse() - showReadOnlyMessage() - - var username string - if username = os.Getenv("SUDO_USER"); username == "" { - user, err := user.Current() - if err != nil { - panic(err) - } - username = user.Username - } - log.Println("Current user: ", username) - - // Initialize configuration - ui := NewUI(&UIConfig{ - Language: detectLanguage(), - Theme: "default", - Spinner: SpinnerConfig{ - Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, - Delay: 100 * time.Millisecond, - }, - }) - - // Check privileges - os.Stdout.Sync() - currentLanguage = detectLanguage() - log.Println("Current language: ", currentLanguage) - isAdmin, err := checkAdminPrivileges() - if err != nil { - handleError(err) - waitExit() - return - } - - if !isAdmin && runtime.GOOS == "windows" { - if currentLanguage == EN { - fmt.Println("\nRequesting administrator privileges...") - } else { - fmt.Println("\n请求管理员权限...") - } - if err := selfElevate(); err != nil { - handleError(err) - showPrivilegeError() - waitExit() - return - } - return - } else if !isAdmin { - showPrivilegeError() - waitExit() - return - } - - // Ensure all Cursor instances are closed - if err := ensureCursorClosed(); err != nil { - if currentLanguage == EN { - fmt.Println("\nError: Please close Cursor manually before running this program.") - } else { - fmt.Println("\n错误:请在运���此程序之前手动关闭 Cursor。") - } - waitExit() - return - } - - // Process management - pm := &ProcessManager{ - config: &SystemConfig{ - RetryAttempts: 3, - RetryDelay: time.Second, - Timeout: 30 * time.Second, - }, - } - if checkCursorRunning() { - text := texts[currentLanguage] - showProcessStatus(text.ClosingProcesses) - - if err := pm.killCursorProcesses(); err != nil { - fmt.Println() // New line after status - if currentLanguage == EN { - fmt.Println("Warning: Could not close all Cursor instances. Please close them manually.") - } else { - fmt.Println("警告:无法关闭所有 Cursor 实例,请手动关闭。") - } - waitExit() - return - } - - time.Sleep(2 * time.Second) - if checkCursorRunning() { - fmt.Println() // New line after status - if currentLanguage == EN { - fmt.Println("\nWarning: Cursor is still running. Please close it manually.") - } else { - fmt.Println("\n警告:Cursor 仍在运行,请手动关闭。") - } - waitExit() - return - } - - showProcessStatus(text.ProcessesClosed) - fmt.Println() // New line after status - } - - // Clear screen and show banner - clearScreen() - printCyberpunkBanner() - - // Read and update configuration - oldConfig, err := readExistingConfig(username) // add username parameter - if err != nil { - oldConfig = nil - } - - storageConfig, err := loadAndUpdateConfig(ui, username) // add username parameter - if err != nil { - handleError(err) - waitExit() - return - } - - // Show changes and save - showIdComparison(oldConfig, storageConfig) - - if err := saveConfig(storageConfig, username); err != nil { // add username parameter - handleError(err) - waitExit() - return - } - - // Show success and exit - showSuccess() - if currentLanguage == EN { - fmt.Println("\nOperation completed!") - } else { - fmt.Println("\n操作完成!") - } - - // Check if running in automated mode - if os.Getenv("AUTOMATED_MODE") == "1" { - return - } - - waitExit() -} - -// Progress spinner functions / 进度条函数 -func NewProgressSpinner(message string) *ProgressSpinner { - return &ProgressSpinner{ - frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, - message: message, - } -} - -func (s *ProgressSpinner) Spin() { - frame := s.frames[s.current%len(s.frames)] - s.current++ - fmt.Printf("\r%s %s", color.CyanString(frame), s.message) -} - -func (s *ProgressSpinner) Stop() { - fmt.Println() -} - -func (s *ProgressSpinner) Start() { - s.current = 0 -} - -// Display utility functions / 显示工具函数 -func clearScreen() { - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command("cmd", "/c", "cls") - } else { - cmd = exec.Command("clear") - } - cmd.Stdout = os.Stdout - cmd.Run() -} - -func printCyberpunkBanner() { - cyan := color.New(color.FgCyan, color.Bold) - yellow := color.New(color.FgYellow, color.Bold) - magenta := color.New(color.FgMagenta, color.Bold) - green := color.New(color.FgGreen, color.Bold) - - banner := ` - ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ - ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ - ██║ ██║ ██║██████╔╝███████╗██║ ██║█████╔╝ - ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ - ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ - ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ - ` - cyan.Println(banner) - yellow.Printf("\t\t>> Cursor ID Modifier %s <<\n", version) - magenta.Println("\t\t [ By Pancake Fruit Rolled Shark Chili ]") - - langText := "当前语言/Language: " - if currentLanguage == CN { - langText += "简体中文" - } else { - langText += "English" - } - green.Printf("\n\t\t %s\n\n", langText) -} - -func showIdComparison(oldConfig *StorageConfig, newConfig *StorageConfig) { - cyan := color.New(color.FgCyan) - yellow := color.New(color.FgYellow) - - fmt.Println("\n=== ID Modification Comparison / ID 修改对比 ===") - - if oldConfig != nil { - cyan.Println("\n[Original IDs / 原始 ID]") - yellow.Printf("Machine ID: %s\n", oldConfig.TelemetryMachineId) - yellow.Printf("Mac Machine ID: %s\n", oldConfig.TelemetryMacMachineId) - yellow.Printf("Dev Device ID: %s\n", oldConfig.TelemetryDevDeviceId) - } - - cyan.Println("\n[Newly Generated IDs / 新生成 ID]") - yellow.Printf("Machine ID: %s\n", newConfig.TelemetryMachineId) - yellow.Printf("Mac Machine ID: %s\n", newConfig.TelemetryMacMachineId) - yellow.Printf("Dev Device ID: %s\n", newConfig.TelemetryDevDeviceId) - yellow.Printf("SQM ID: %s\n", newConfig.TelemetrySqmId) - fmt.Println() -} - -// Configuration functions / 配置函数 -func loadAndUpdateConfig(ui *UI, username string) (*StorageConfig, error) { // add username parameter - configPath, err := getConfigPath(username) // add username parameter - if err != nil { - return nil, err - } - - text := texts[currentLanguage] - ui.showProgress(text.ReadingConfig) - - oldConfig, err := readExistingConfig(username) // add username parameter - if err != nil && !os.IsNotExist(err) { - return nil, &AppError{ - Type: ErrSystem, - Op: "read config file", - Path: configPath, - Err: err, - } - } - - ui.showProgress(text.GeneratingIds) - return NewStorageConfig(oldConfig), nil -} - -// Add a new function to show process status -func showProcessStatus(message string) { - cyan := color.New(color.FgCyan) - fmt.Printf("\r%s", strings.Repeat(" ", 80)) // Clear line - fmt.Printf("\r%s", cyan.Sprint("⚡ "+message)) -} diff --git a/pkg/idgen/generator.go b/pkg/idgen/generator.go new file mode 100644 index 0000000..3061f74 --- /dev/null +++ b/pkg/idgen/generator.go @@ -0,0 +1,116 @@ +package idgen + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "sync" +) + +// Generator handles secure ID generation for machines and devices +type Generator struct { + bufferPool sync.Pool +} + +// NewGenerator creates a new ID generator +func NewGenerator() *Generator { + return &Generator{ + bufferPool: sync.Pool{ + New: func() interface{} { + return make([]byte, 64) + }, + }, + } +} + +// Constants for ID generation +const ( + machineIDPrefix = "auth0|user_" + uuidFormat = "%s-%s-%s-%s-%s" +) + +// generateRandomHex generates a random hex string of specified length +func (g *Generator) generateRandomHex(length int) (string, error) { + buffer := g.bufferPool.Get().([]byte) + defer g.bufferPool.Put(buffer) + + if _, err := rand.Read(buffer[:length]); err != nil { + return "", fmt.Errorf("failed to generate random bytes: %w", err) + } + return hex.EncodeToString(buffer[:length]), nil +} + +// GenerateMachineID generates a new machine ID with auth0|user_ prefix +func (g *Generator) GenerateMachineID() (string, error) { + randomPart, err := g.generateRandomHex(32) // 生成64字符的十六进制 + if err != nil { + return "", err + } + + return fmt.Sprintf("%x%s", []byte(machineIDPrefix), randomPart), nil +} + +// GenerateMacMachineID generates a new 64-byte MAC machine ID +func (g *Generator) GenerateMacMachineID() (string, error) { + return g.generateRandomHex(32) // 生成64字符的十六进制 +} + +// GenerateDeviceID generates a new device ID in UUID format +func (g *Generator) GenerateDeviceID() (string, error) { + id, err := g.generateRandomHex(16) + if err != nil { + return "", err + } + return fmt.Sprintf(uuidFormat, + id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil +} + +// GenerateSQMID generates a new SQM ID in UUID format (with braces) +func (g *Generator) GenerateSQMID() (string, error) { + id, err := g.GenerateDeviceID() + if err != nil { + return "", err + } + return fmt.Sprintf("{%s}", id), nil +} + +// ValidateID validates the format of various ID types +func (g *Generator) ValidateID(id string, idType string) bool { + switch idType { + case "machineID", "macMachineID": + return len(id) == 64 && isHexString(id) + case "deviceID": + return isValidUUID(id) + case "sqmID": + if len(id) < 2 || id[0] != '{' || id[len(id)-1] != '}' { + return false + } + return isValidUUID(id[1 : len(id)-1]) + default: + return false + } +} + +// Helper functions +func isHexString(s string) bool { + _, err := hex.DecodeString(s) + return err == nil +} + +func isValidUUID(uuid string) bool { + if len(uuid) != 36 { + return false + } + for i, r := range uuid { + if i == 8 || i == 13 || i == 18 || i == 23 { + if r != '-' { + return false + } + continue + } + if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { + return false + } + } + return true +} diff --git a/scripts/build_all.bat b/scripts/build_all.bat index 0ae443f..e07e387 100644 --- a/scripts/build_all.bat +++ b/scripts/build_all.bat @@ -1,128 +1,74 @@ @echo off setlocal EnableDelayedExpansion +:: Build optimization flags +set "OPTIMIZATION_FLAGS=-trimpath -ldflags=\"-s -w\"" +set "BUILD_JOBS=4" + :: Messages / 消息 set "EN_MESSAGES[0]=Starting build process for version" set "EN_MESSAGES[1]=Using optimization flags:" set "EN_MESSAGES[2]=Cleaning old builds..." set "EN_MESSAGES[3]=Cleanup completed" -set "EN_MESSAGES[4]=bin directory does not exist, no cleanup needed" -set "EN_MESSAGES[5]=Starting builds for all platforms..." -set "EN_MESSAGES[6]=Building for" -set "EN_MESSAGES[7]=Build successful:" -set "EN_MESSAGES[8]=Build failed for" -set "EN_MESSAGES[9]=All builds completed! Total time:" -set "EN_MESSAGES[10]=seconds" - -set "CN_MESSAGES[0]=开始构建版本" -set "CN_MESSAGES[1]=使用优化标志:" -set "CN_MESSAGES[2]=正在清理旧的构建文件..." -set "CN_MESSAGES[3]=清理完成" -set "CN_MESSAGES[4]=bin 目录不存在,无需清理" -set "CN_MESSAGES[5]=开始编译所有平台..." -set "CN_MESSAGES[6]=正在构建" -set "CN_MESSAGES[7]=构建成功:" -set "CN_MESSAGES[8]=构建失败:" -set "CN_MESSAGES[9]=所有构建完成!总耗时:" -set "CN_MESSAGES[10]=秒" - -:: 设置版本信息 / Set version -set VERSION=2.0.0 +set "EN_MESSAGES[4]=Starting builds for all platforms..." +set "EN_MESSAGES[5]=Building for" +set "EN_MESSAGES[6]=Build successful:" +set "EN_MESSAGES[7]=All builds completed!" -:: 设置颜色代码 / Set color codes +:: Colors set "GREEN=[32m" set "RED=[31m" -set "YELLOW=[33m" set "RESET=[0m" -:: 设置编译优化标志 / Set build optimization flags -set "LDFLAGS=-s -w" -set "BUILDMODE=pie" -set "GCFLAGS=-N -l" - -:: 设置 CGO / Set CGO -set CGO_ENABLED=0 - -:: 检测系统语言 / Detect system language -for /f "tokens=2 delims==" %%a in ('wmic os get OSLanguage /value') do set OSLanguage=%%a -if "%OSLanguage%"=="2052" (set LANG=cn) else (set LANG=en) - -:: 显示编译信息 / Display build info -echo %YELLOW%!%LANG%_MESSAGES[0]! %VERSION%%RESET% -echo %YELLOW%!%LANG%_MESSAGES[1]! LDFLAGS=%LDFLAGS%, BUILDMODE=%BUILDMODE%%RESET% -echo %YELLOW%CGO_ENABLED=%CGO_ENABLED%%RESET% - -:: 清理旧的构建文件 / Clean old builds -echo %YELLOW%!%LANG%_MESSAGES[2]!%RESET% +:: Cleanup function +:cleanup if exist "..\bin" ( rd /s /q "..\bin" - echo %GREEN%!%LANG%_MESSAGES[3]!%RESET% -) else ( - echo %YELLOW%!%LANG%_MESSAGES[4]!%RESET% + echo %GREEN%!EN_MESSAGES[3]!%RESET% ) - -:: 创建输出目录 / Create output directory mkdir "..\bin" 2>nul -:: 定义目标平台数组 / Define target platforms array -set platforms[0].os=windows -set platforms[0].arch=amd64 -set platforms[0].ext=.exe -set platforms[0].suffix= +:: Build function with optimizations +:build +set "os=%~1" +set "arch=%~2" +set "ext=" +if "%os%"=="windows" set "ext=.exe" -set platforms[1].os=darwin -set platforms[1].arch=amd64 -set platforms[1].ext= -set platforms[1].suffix=_intel +echo %GREEN%!EN_MESSAGES[5]! %os%/%arch%%RESET% -set platforms[2].os=darwin -set platforms[2].arch=arm64 -set platforms[2].ext= -set platforms[2].suffix=_m1 +set "CGO_ENABLED=0" +set "GOOS=%os%" +set "GOARCH=%arch%" -set platforms[3].os=linux -set platforms[3].arch=amd64 -set platforms[3].ext= -set platforms[3].suffix= +start /b cmd /c "go build -trimpath -ldflags=\"-s -w\" -o ..\bin\%os%\%arch%\cursor-id-modifier%ext% -a -installsuffix cgo -mod=readonly ..\cmd\cursor-id-modifier" +exit /b 0 -:: 设置开始时间 / Set start time -set start_time=%time% +:: Main execution +echo %GREEN%!EN_MESSAGES[0]!%RESET% +echo %GREEN%!EN_MESSAGES[1]! %OPTIMIZATION_FLAGS%%RESET% -:: 编译所有目标 / Build all targets -echo !%LANG%_MESSAGES[5]! +call :cleanup -for /L %%i in (0,1,3) do ( - set "os=!platforms[%%i].os!" - set "arch=!platforms[%%i].arch!" - set "ext=!platforms[%%i].ext!" - set "suffix=!platforms[%%i].suffix!" - - echo. - echo !%LANG%_MESSAGES[6]! !os! !arch!... - - set GOOS=!os! - set GOARCH=!arch! - - :: 构建输出文件名 / Build output filename - set "outfile=..\bin\cursor_id_modifier_v%VERSION%_!os!_!arch!!suffix!!ext!" - - :: 执行构建 / Execute build - go build -trimpath -buildmode=%BUILDMODE% -ldflags="%LDFLAGS%" -gcflags="%GCFLAGS%" -o "!outfile!" ..\main.go - - if !errorlevel! equ 0 ( - echo %GREEN%!%LANG%_MESSAGES[7]! !outfile!%RESET% - ) else ( - echo %RED%!%LANG%_MESSAGES[8]! !os! !arch!%RESET% +echo %GREEN%!EN_MESSAGES[4]!%RESET% + +:: Start builds in parallel +set "pending=0" +for %%o in (windows linux darwin) do ( + for %%a in (amd64 386) do ( + call :build %%o %%a + set /a "pending+=1" + if !pending! geq %BUILD_JOBS% ( + timeout /t 1 /nobreak >nul + set "pending=0" + ) ) ) -:: 计算总耗时 / Calculate total time -set end_time=%time% -set /a duration = %end_time:~0,2% * 3600 + %end_time:~3,2% * 60 + %end_time:~6,2% - (%start_time:~0,2% * 3600 + %start_time:~3,2% * 60 + %start_time:~6,2%) - -echo. -echo %GREEN%!%LANG%_MESSAGES[9]! %duration% !%LANG%_MESSAGES[10]!%RESET% -if exist "..\bin" dir /b "..\bin" +:: Wait for all builds to complete +:wait_builds +timeout /t 2 /nobreak >nul +tasklist /fi "IMAGENAME eq go.exe" 2>nul | find "go.exe" >nul +if not errorlevel 1 goto wait_builds -pause -endlocal \ No newline at end of file +echo %GREEN%!EN_MESSAGES[7]!%RESET% \ No newline at end of file diff --git a/scripts/build_all.sh b/scripts/build_all.sh old mode 100644 new mode 100755 index d7e38a9..7db5e7b --- a/scripts/build_all.sh +++ b/scripts/build_all.sh @@ -5,6 +5,10 @@ GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' # No Color / 无颜色 +# Build optimization flags +OPTIMIZATION_FLAGS="-trimpath -ldflags=\"-s -w\"" +PARALLEL_JOBS=$(nproc || echo "4") # Get number of CPU cores or default to 4 + # Messages / 消息 EN_MESSAGES=( "Starting build process for version" @@ -18,8 +22,6 @@ EN_MESSAGES=( "Successful builds:" "Failed builds:" "Generated files:" - "Build process interrupted" - "Error:" ) CN_MESSAGES=( @@ -70,82 +72,68 @@ handle_error() { # 清理函数 / Cleanup function cleanup() { - echo "$(get_message 1)" - rm -rf ../bin -} - -# 创建输出目录 / Create output directory -create_output_dir() { - echo "$(get_message 2)" - mkdir -p ../bin || handle_error "$(get_message 3)" + if [ -d "../bin" ]; then + rm -rf ../bin + echo -e "${GREEN}$(get_message 1)${NC}" + fi } -# 构建函数 / Build function +# Build function with optimizations build() { local os=$1 local arch=$2 - local suffix=$3 - - echo -e "\n$(get_message 4) $os ($arch)..." + local ext="" + [ "$os" = "windows" ] && ext=".exe" - output_name="../bin/cursor_id_modifier_v${VERSION}_${os}_${arch}${suffix}" + echo -e "${GREEN}$(get_message 4) $os/$arch${NC}" - GOOS=$os GOARCH=$arch go build -o "$output_name" ../main.go - - if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ $(get_message 5) ${output_name}${NC}" - else - echo -e "${RED}✗ $(get_message 6) $os $arch${NC}" - return 1 - fi + GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build \ + -trimpath \ + -ldflags="-s -w" \ + -o "../bin/$os/$arch/cursor-id-modifier$ext" \ + -a -installsuffix cgo \ + -mod=readonly \ + ../cmd/cursor-id-modifier & } -# 主函数 / Main function -main() { - # 显示构建信息 / Display build info - echo "$(get_message 0) ${VERSION}" - - # 清理旧文件 / Clean old files - cleanup - - # 创建输出目录 / Create output directory - create_output_dir +# Parallel build execution +build_all() { + local builds=0 + local max_parallel=$PARALLEL_JOBS - # 定义构建目标 / Define build targets + # Define build targets declare -A targets=( - ["windows_amd64"]=".exe" - ["darwin_amd64"]="" - ["darwin_arm64"]="" - ["linux_amd64"]="" + ["linux/amd64"]=1 + ["linux/386"]=1 + ["linux/arm64"]=1 + ["windows/amd64"]=1 + ["windows/386"]=1 + ["darwin/amd64"]=1 + ["darwin/arm64"]=1 ) - # 构建计数器 / Build counters - local success_count=0 - local fail_count=0 - - # 遍历所有目标进行构建 / Build all targets for target in "${!targets[@]}"; do - os=${target%_*} - arch=${target#*_} - suffix=${targets[$target]} + IFS='/' read -r os arch <<< "$target" + build "$os" "$arch" - if build "$os" "$arch" "$suffix"; then - ((success_count++)) - else - ((fail_count++)) + ((builds++)) + + if ((builds >= max_parallel)); then + wait + builds=0 fi done - # 显示构建结果 / Display build results - echo -e "\n$(get_message 7)" - echo -e "${GREEN}$(get_message 8) $success_count${NC}" - if [ $fail_count -gt 0 ]; then - echo -e "${RED}$(get_message 9) $fail_count${NC}" - fi - - # 显示生成的文件列表 / Display generated files - echo -e "\n$(get_message 10)" - ls -1 ../bin + # Wait for remaining builds + wait +} + +# Main execution +main() { + cleanup + mkdir -p ../bin || { echo -e "${RED}$(get_message 3)${NC}"; exit 1; } + build_all + echo -e "${GREEN}Build completed successfully${NC}" } # 捕获错误信号 / Catch error signals diff --git a/scripts/install.ps1 b/scripts/install.ps1 index cd4d5dd..b30350b 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,308 +1,193 @@ -# Auto-elevate to admin rights if not already running as admin -if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { - Write-Host "Requesting administrator privileges..." - $arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -ExecutionFromElevated" - Start-Process powershell.exe -ArgumentList $arguments -Verb RunAs - Exit -} - -# Set TLS to 1.2 / 设置 TLS 为 1.2 -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - -# Colors for output / 输出颜色 -$Red = "`e[31m" -$Green = "`e[32m" -$Blue = "`e[36m" -$Yellow = "`e[33m" -$Reset = "`e[0m" - -# Messages / 消息 -$EN_MESSAGES = @( - "Starting installation...", - "Detected architecture:", - "Only 64-bit Windows is supported", - "Latest version:", - "Creating installation directory...", - "Downloading latest release from:", - "Failed to download binary:", - "Downloaded file not found", - "Installing binary...", - "Failed to install binary:", - "Adding to PATH...", - "Cleaning up...", - "Installation completed successfully!", - "You can now use 'cursor-id-modifier' directly", - "Checking for running Cursor instances...", - "Found running Cursor processes. Attempting to close them...", - "Successfully closed all Cursor instances", - "Failed to close Cursor instances. Please close them manually", - "Backing up storage.json...", - "Backup created at:" -) - -$CN_MESSAGES = @( - "开始安装...", - "检测到架构:", - "仅支持64位Windows系统", - "最新版本:", - "正在创建安装目录...", - "正在从以下地址下载最新版本:", - "下载二进制文件失败:", - "未找到下载的文件", - "正在安装程序...", - "安装二进制文件失败:", - "正在添加到PATH...", - "正在清理...", - "安装成功完成!", - "现在可以直接使用 'cursor-id-modifier' 了", - "正在检查运行中的Cursor进程...", - "发现正在运行的Cursor进程,尝试关闭...", - "成功关闭所有Cursor实例", - "无法关闭Cursor实例,请手动关闭", - "正在备份storage.json...", - "备份已创建于:" -) - -# Detect system language / 检测系统语言 -function Get-SystemLanguage { - if ((Get-Culture).Name -like "zh-CN") { - return "cn" +# Check for admin rights and handle elevation +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") +if (-NOT $isAdmin) { + # Detect PowerShell version and path + $pwshPath = if (Get-Command "pwsh" -ErrorAction SilentlyContinue) { + (Get-Command "pwsh").Source # PowerShell 7+ + } elseif (Test-Path "$env:ProgramFiles\PowerShell\7\pwsh.exe") { + "$env:ProgramFiles\PowerShell\7\pwsh.exe" + } else { + "powershell.exe" # Windows PowerShell } - return "en" -} - -# Get message based on language / 根据语言获取消息 -function Get-Message($Index) { - $lang = Get-SystemLanguage - if ($lang -eq "cn") { - return $CN_MESSAGES[$Index] + + try { + Write-Host "`nRequesting administrator privileges..." -ForegroundColor Cyan + $scriptPath = $MyInvocation.MyCommand.Path + $argList = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" + Start-Process -FilePath $pwshPath -Verb RunAs -ArgumentList $argList -Wait + exit + } + catch { + Write-Host "`nError: Administrator privileges required" -ForegroundColor Red + Write-Host "Please run this script from an Administrator PowerShell window" -ForegroundColor Yellow + Write-Host "`nTo do this:" -ForegroundColor Cyan + Write-Host "1. Press Win + X" -ForegroundColor White + Write-Host "2. Click 'Windows Terminal (Admin)' or 'PowerShell (Admin)'" -ForegroundColor White + Write-Host "3. Run the installation command again" -ForegroundColor White + Write-Host "`nPress enter to exit..." + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') + exit 1 } - return $EN_MESSAGES[$Index] -} - -# Functions for colored output / 彩色输出函数 -function Write-Status($Message) { - Write-Host "${Blue}[*]${Reset} $Message" -} - -function Write-Success($Message) { - Write-Host "${Green}[✓]${Reset} $Message" } -function Write-Warning($Message) { - Write-Host "${Yellow}[!]${Reset} $Message" -} +# Set TLS to 1.2 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -function Write-Error($Message) { - Write-Host "${Red}[✗]${Reset} $Message" - Exit 1 -} +# Create temporary directory +$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) +New-Item -ItemType Directory -Path $TmpDir | Out-Null -# Close Cursor instances / 关闭Cursor实例 -function Close-CursorInstances { - Write-Status (Get-Message 14) - $cursorProcesses = Get-Process "Cursor" -ErrorAction SilentlyContinue - - if ($cursorProcesses) { - Write-Status (Get-Message 15) - try { - $cursorProcesses | ForEach-Object { $_.CloseMainWindow() | Out-Null } - Start-Sleep -Seconds 2 - $cursorProcesses | Where-Object { !$_.HasExited } | Stop-Process -Force - Write-Success (Get-Message 16) - } catch { - Write-Error (Get-Message 17) - } +# Cleanup function +function Cleanup { + if (Test-Path $TmpDir) { + Remove-Item -Recurse -Force $TmpDir } } -# Backup storage.json / 备份storage.json -function Backup-StorageJson { - Write-Status (Get-Message 18) - $storageJsonPath = "$env:APPDATA\Cursor\User\globalStorage\storage.json" - if (Test-Path $storageJsonPath) { - $backupPath = "$storageJsonPath.backup" - Copy-Item -Path $storageJsonPath -Destination $backupPath -Force - Write-Success "$(Get-Message 19) $backupPath" - } +# Error handler +trap { + Write-Host "Error: $_" -ForegroundColor Red + Cleanup + Write-Host "Press enter to exit..." + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') + exit 1 } -# Get latest release version from GitHub / 从GitHub获取最新版本 -function Get-LatestVersion { - $repo = "yuaotian/go-cursor-help" - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" - return $release.tag_name +# Detect system architecture +function Get-SystemArch { + if ([Environment]::Is64BitOperatingSystem) { + return "x86_64" + } else { + return "i386" + } } -# 在文件开头添加日志函数 -function Write-Log { - param( - [string]$Message, - [string]$Level = "INFO" +# Download with progress +function Get-FileWithProgress { + param ( + [string]$Url, + [string]$OutputFile ) - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $logMessage = "[$timestamp] [$Level] $Message" - $logFile = "$env:TEMP\cursor-id-modifier-install.log" - Add-Content -Path $logFile -Value $logMessage - # 同时输出到控制台 - switch ($Level) { - "ERROR" { Write-Error $Message } - "WARNING" { Write-Warning $Message } - "SUCCESS" { Write-Success $Message } - default { Write-Status $Message } + try { + $webClient = New-Object System.Net.WebClient + $webClient.Headers.Add("User-Agent", "PowerShell Script") + + $webClient.DownloadFile($Url, $OutputFile) + return $true + } + catch { + Write-Host "Failed to download: $_" -ForegroundColor Red + return $false } } -# 添加安装前检查函数 -function Test-Prerequisites { - Write-Log "Checking prerequisites..." "INFO" +# Main installation function +function Install-CursorModifier { + Write-Host "Starting installation..." -ForegroundColor Cyan - # 检查PowerShell版本 - if ($PSVersionTable.PSVersion.Major -lt 5) { - Write-Log "PowerShell 5.0 or higher is required" "ERROR" - return $false + # Detect architecture + $arch = Get-SystemArch + Write-Host "Detected architecture: $arch" -ForegroundColor Green + + # Set installation directory + $InstallDir = "$env:ProgramFiles\CursorModifier" + if (!(Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir | Out-Null } - # 检查网络连接 + # Get latest release try { - $testConnection = Test-Connection -ComputerName "github.com" -Count 1 -Quiet - if (-not $testConnection) { - Write-Log "No internet connection available" "ERROR" - return $false + $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/yuaotian/go-cursor-help/releases/latest" + Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan + + # Look for Windows binary with our architecture + $version = $latestRelease.tag_name.TrimStart('v') + Write-Host "Version: $version" -ForegroundColor Cyan + $possibleNames = @( + "cursor-id-modifier_${version}_windows_x86_64.exe", + "cursor-id-modifier_${version}_windows_$($arch).exe" + ) + + $asset = $null + foreach ($name in $possibleNames) { + Write-Host "Checking for asset: $name" -ForegroundColor Cyan + $asset = $latestRelease.assets | Where-Object { $_.name -eq $name } + if ($asset) { + Write-Host "Found matching asset: $($asset.name)" -ForegroundColor Green + break + } } - } catch { - Write-Log "Failed to check internet connection: $_" "ERROR" - return $false + + if (!$asset) { + Write-Host "`nAvailable assets:" -ForegroundColor Yellow + $latestRelease.assets | ForEach-Object { Write-Host "- $($_.name)" } + throw "Could not find appropriate Windows binary for $arch architecture" + } + + $downloadUrl = $asset.browser_download_url + } + catch { + Write-Host "Failed to get latest release: $_" -ForegroundColor Red + exit 1 } - return $true -} - -# 添加文件验证函数 -function Test-FileHash { - param( - [string]$FilePath, - [string]$ExpectedHash - ) + # Download binary + Write-Host "`nDownloading latest release..." -ForegroundColor Cyan + $binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe" - $actualHash = Get-FileHash -Path $FilePath -Algorithm SHA256 - return $actualHash.Hash -eq $ExpectedHash -} - -# 修改下载函数,添加进度条 -function Download-File { - param( - [string]$Url, - [string]$OutFile - ) + if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) { + exit 1 + } + # Install binary + Write-Host "Installing..." -ForegroundColor Cyan try { - $webClient = New-Object System.Net.WebClient - $webClient.Headers.Add("User-Agent", "PowerShell Script") - - $webClient.DownloadFileAsync($Url, $OutFile) + Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force - while ($webClient.IsBusy) { - Write-Progress -Activity "Downloading..." -Status "Progress:" -PercentComplete -1 - Start-Sleep -Milliseconds 100 + # Add to PATH if not already present + $currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine") + if ($currentPath -notlike "*$InstallDir*") { + [Environment]::SetEnvironmentVariable("Path", "$currentPath;$InstallDir", "Machine") } - - Write-Progress -Activity "Downloading..." -Completed - return $true } catch { - Write-Log "Download failed: $_" "ERROR" - return $false + Write-Host "Failed to install: $_" -ForegroundColor Red + exit 1 } - finally { - if ($webClient) { - $webClient.Dispose() + + Write-Host "Installation completed successfully!" -ForegroundColor Green + Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan + + # Run the program + try { + & "$InstallDir\cursor-id-modifier.exe" + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red + exit 1 } } -} - -# Main installation process / 主安装过程 -Write-Status (Get-Message 0) - -# Close any running Cursor instances -Close-CursorInstances - -# Backup storage.json -Backup-StorageJson - -# Get system architecture / 获取系统架构 -$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" } -Write-Status "$(Get-Message 1) $arch" - -if ($arch -ne "amd64") { - Write-Error (Get-Message 2) -} - -# Get latest version / 获取最新版本 -$version = Get-LatestVersion -Write-Status "$(Get-Message 3) $version" - -# Set up paths / 设置路径 -$installDir = "$env:ProgramFiles\cursor-id-modifier" -$versionWithoutV = $version.TrimStart('v') # 移除版本号前面的 'v' 字符 -$binaryName = "cursor_id_modifier_${versionWithoutV}_windows_amd64.exe" -$downloadUrl = "https://github.com/yuaotian/go-cursor-help/releases/download/$version/$binaryName" -$tempFile = "$env:TEMP\$binaryName" - -# Create installation directory / 创建安装目录 -Write-Status (Get-Message 4) -if (-not (Test-Path $installDir)) { - New-Item -ItemType Directory -Path $installDir -Force | Out-Null -} - -# Download binary / 下载二进制文件 -Write-Status "$(Get-Message 5) $downloadUrl" -try { - if (-not (Download-File -Url $downloadUrl -OutFile $tempFile)) { - Write-Error "$(Get-Message 6)" + catch { + Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red + exit 1 } -} catch { - Write-Error "$(Get-Message 6) $_" -} - -# Verify download / 验证下载 -if (-not (Test-Path $tempFile)) { - Write-Error (Get-Message 7) } -# Install binary / 安装二进制文件 -Write-Status (Get-Message 8) +# Run installation try { - Move-Item -Force $tempFile "$installDir\cursor-id-modifier.exe" -} catch { - Write-Error "$(Get-Message 9) $_" -} - -# Add to PATH if not already present / 如果尚未添加则添加到PATH -$userPath = [Environment]::GetEnvironmentVariable("Path", "User") -if ($userPath -notlike "*$installDir*") { - Write-Status (Get-Message 10) - [Environment]::SetEnvironmentVariable( - "Path", - "$userPath;$installDir", - "User" - ) -} - -# Cleanup / 清理 -Write-Status (Get-Message 11) -if (Test-Path $tempFile) { - Remove-Item -Force $tempFile -} - -Write-Success (Get-Message 12) -Write-Success (Get-Message 13) -Write-Host "" - -# 直接运行程序 -try { - Start-Process "$installDir\cursor-id-modifier.exe" -NoNewWindow -} catch { - Write-Warning "Failed to start cursor-id-modifier: $_" + Install-CursorModifier +} +catch { + Write-Host "Installation failed: $_" -ForegroundColor Red + Cleanup + Write-Host "Press enter to exit..." + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') + exit 1 +} +finally { + Cleanup + if ($LASTEXITCODE -ne 0) { + Write-Host "Press enter to exit..." -ForegroundColor Green + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') + } } \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh index 0aed487..adc7881 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,294 +2,126 @@ set -e -# Colors for output / 输出颜色 +# Colors for output RED='\033[0;31m' GREEN='\033[0;32m' BLUE='\033[0;36m' YELLOW='\033[0;33m' -NC='\033[0m' # No Color / 无颜色 +NC='\033[0m' -# Messages / 消息 -EN_MESSAGES=( - "Starting installation..." - "Detected OS:" - "Downloading latest release..." - "URL:" - "Installing binary..." - "Cleaning up..." - "Installation completed successfully!" - "You can now use 'sudo %s' from your terminal" - "Failed to download binary from:" - "Failed to download the binary" - "curl is required but not installed. Please install curl first." - "sudo is required but not installed. Please install sudo first." - "Unsupported operating system" - "Unsupported architecture:" - "Checking for running Cursor instances..." - "Found running Cursor processes. Attempting to close them..." - "Successfully closed all Cursor instances" - "Failed to close Cursor instances. Please close them manually" - "Backing up storage.json..." - "Backup created at:" - "This script requires root privileges. Requesting sudo access..." -) +# Temporary directory for downloads +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT -CN_MESSAGES=( - "开始安装..." - "检测到操作系统:" - "正在下载最新版本..." - "下载地址:" - "正在安装程序..." - "正在清理..." - "安装成功完成!" - "现在可以在终端中使用 'sudo %s' 了" - "从以下地址下载二进制文件失败:" - "下载二进制文件失败" - "需要 curl 但未安装。请先安装 curl。" - "需要 sudo 但未安装。请先安装 sudo。" - "不支持的操作系统" - "不支持的架构:" - "正在检查运行中的Cursor进程..." - "发现正在运行的Cursor进程,尝试关闭..." - "成功关闭所有Cursor实例" - "无法关闭Cursor实例,请手动关闭" - "正在备份storage.json..." - "备份已创建于:" - "此脚本需要root权限。正在请求sudo访问..." -) - -# Detect system language / 检测系统语言 -detect_language() { - if [[ $(locale | grep "LANG=zh_CN") ]]; then - echo "cn" - else - echo "en" - fi -} - -# Get message based on language / 根据语言获取消息 -get_message() { - local index=$1 - local lang=$(detect_language) - - if [[ "$lang" == "cn" ]]; then - echo "${CN_MESSAGES[$index]}" - else - echo "${EN_MESSAGES[$index]}" - fi -} - -# Print with color / 带颜色打印 -print_status() { - echo -e "${BLUE}[*]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[✓]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[!]${NC} $1" -} - -print_error() { - echo -e "${RED}[✗]${NC} $1" - exit 1 -} - -# Check and request root privileges / 检查并请求root权限 -check_root() { - if [ "$EUID" -ne 0 ]; then - print_status "$(get_message 20)" - if command -v sudo >/dev/null 2>&1; then - exec sudo bash "$0" "$@" - else - print_error "$(get_message 11)" - fi - fi -} - -# Close Cursor instances / 关闭Cursor实例 -close_cursor_instances() { - print_status "$(get_message 14)" - - if pgrep -x "Cursor" >/dev/null; then - print_status "$(get_message 15)" - if pkill -x "Cursor" 2>/dev/null; then - sleep 2 - print_success "$(get_message 16)" - else - print_error "$(get_message 17)" - fi - fi -} - -# Backup storage.json / 备份storage.json -backup_storage_json() { - print_status "$(get_message 18)" - local storage_path - - if [ "$(uname)" == "Darwin" ]; then - storage_path="$HOME/Library/Application Support/Cursor/User/globalStorage/storage.json" - else - storage_path="$HOME/.config/Cursor/User/globalStorage/storage.json" - fi - - if [ -f "$storage_path" ]; then - cp "$storage_path" "${storage_path}.backup" - print_success "$(get_message 19) ${storage_path}.backup" +# Check for required commands +check_requirements() { + if ! command -v curl >/dev/null 2>&1; then + echo -e "${RED}Error: curl is required${NC}" + exit 1 fi } -# Detect OS / 检测操作系统 -detect_os() { - if [[ "$OSTYPE" == "darwin"* ]]; then - echo "darwin" - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - echo "linux" - else - print_error "$(get_message 12)" - fi -} +# Detect system information +detect_system() { + local os arch suffix -# Get latest release version from GitHub / 从GitHub获取最新版本 -get_latest_version() { - local repo="yuaotian/go-cursor-help" - curl -s "https://api.github.com/repos/${repo}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' -} + case "$(uname -s)" in + Linux*) os="linux";; + Darwin*) os="darwin";; + *) echo -e "${RED}Unsupported OS${NC}"; exit 1;; + esac -# Get the binary name based on OS and architecture / 根据操作系统和架构获取二进制文件名 -get_binary_name() { - OS=$(detect_os) - ARCH=$(uname -m) - VERSION=$(get_latest_version) - - case "$ARCH" in - x86_64) - echo "cursor_id_modifier_${VERSION}_${OS}_amd64" + case "$(uname -m)" in + x86_64) + arch="x86_64" ;; - aarch64|arm64) - echo "cursor_id_modifier_${VERSION}_${OS}_arm64" + aarch64|arm64) + arch="arm64" ;; - *) - print_error "$(get_message 13) $ARCH" + i386|i686) + arch="i386" ;; + *) echo -e "${RED}Unsupported architecture${NC}"; exit 1;; esac + + echo "$os $arch" } -# Add download progress display function -download_with_progress() { +# Download with progress +download() { local url="$1" - local output_file="$2" - - curl -L -f --progress-bar "$url" -o "$output_file" - return $? + local output="$2" + curl -#L "$url" -o "$output" } -# Optimize installation function -install_binary() { - OS=$(detect_os) - VERSION=$(get_latest_version) - VERSION_WITHOUT_V=${VERSION#v} # Remove 'v' from version number - BINARY_NAME="cursor_id_modifier_${VERSION_WITHOUT_V}_${OS}_$(get_arch)" - REPO="yuaotian/go-cursor-help" - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}" - TMP_DIR=$(mktemp -d) - FINAL_BINARY_NAME="cursor-id-modifier" - - print_status "$(get_message 2)" - print_status "$(get_message 3) ${DOWNLOAD_URL}" +# Check and create installation directory +setup_install_dir() { + local install_dir="$1" - if ! download_with_progress "$DOWNLOAD_URL" "$TMP_DIR/$BINARY_NAME"; then - rm -rf "$TMP_DIR" - print_error "$(get_message 8) $DOWNLOAD_URL" + if [ ! -d "$install_dir" ]; then + mkdir -p "$install_dir" || { + echo -e "${RED}Failed to create installation directory${NC}" + exit 1 + } fi +} + +# Main installation function +main() { + check_requirements - if [ ! -f "$TMP_DIR/$BINARY_NAME" ]; then - rm -rf "$TMP_DIR" - print_error "$(get_message 9)" - fi + echo -e "${BLUE}Starting installation...${NC}" - print_status "$(get_message 4)" - INSTALL_DIR="/usr/local/bin" + # Detect system + read -r OS ARCH SUFFIX <<< "$(detect_system)" + echo -e "${GREEN}Detected: $OS $ARCH${NC}" - # Create directory if it doesn't exist - mkdir -p "$INSTALL_DIR" + # Set installation directory + INSTALL_DIR="/usr/local/bin" - # Move binary to installation directory - if ! mv "$TMP_DIR/$BINARY_NAME" "$INSTALL_DIR/$FINAL_BINARY_NAME"; then - rm -rf "$TMP_DIR" - print_error "Failed to move binary to installation directory" - fi + # Setup installation directory + setup_install_dir "$INSTALL_DIR" - if ! chmod +x "$INSTALL_DIR/$FINAL_BINARY_NAME"; then - rm -rf "$TMP_DIR" - print_error "Failed to set executable permissions" - fi + # Get latest release info + echo -e "${BLUE}Fetching latest release information...${NC}" + LATEST_URL="https://api.github.com/repos/yuaotian/go-cursor-help/releases/latest" - # Cleanup - print_status "$(get_message 5)" - rm -rf "$TMP_DIR" + # Get latest version and remove 'v' prefix + VERSION=$(curl -s "$LATEST_URL" | grep "tag_name" | cut -d'"' -f4 | sed 's/^v//') - print_success "$(get_message 6)" - printf "${GREEN}[✓]${NC} $(get_message 7)\n" "$FINAL_BINARY_NAME" + # Construct binary name + BINARY_NAME="cursor-id-modifier_${VERSION}_${OS}_${ARCH}" + echo -e "${BLUE}Looking for asset: $BINARY_NAME${NC}" - # Try to run the program directly - if [ -x "$INSTALL_DIR/$FINAL_BINARY_NAME" ]; then - "$INSTALL_DIR/$FINAL_BINARY_NAME" & - else - print_warning "Failed to start cursor-id-modifier" - fi -} - -# Optimize architecture detection function -get_arch() { - case "$(uname -m)" in - x86_64) - echo "amd64" - ;; - aarch64|arm64) - echo "arm64" - ;; - *) - print_error "$(get_message 13) $(uname -m)" - ;; - esac -} - -# Check for required tools / 检查必需工具 -check_requirements() { - if ! command -v curl >/dev/null 2>&1; then - print_error "$(get_message 10)" - fi + # Get download URL directly + DOWNLOAD_URL=$(curl -s "$LATEST_URL" | grep -o "\"browser_download_url\": \"[^\"]*${BINARY_NAME}[^\"]*\"" | cut -d'"' -f4) - if ! command -v sudo >/dev/null 2>&1; then - print_error "$(get_message 11)" + if [ -z "$DOWNLOAD_URL" ]; then + echo -e "${RED}Error: Could not find appropriate binary for $OS $ARCH${NC}" + echo -e "${YELLOW}Available assets:${NC}" + curl -s "$LATEST_URL" | grep "browser_download_url" | cut -d'"' -f4 + exit 1 fi -} - -# Main installation process / 主安装过程 -main() { - print_status "$(get_message 0)" - - # Check root privileges / 检查root权限 - check_root "$@" - # Check required tools / 检查必需工具 - check_requirements + echo -e "${GREEN}Found matching asset: $BINARY_NAME${NC}" + echo -e "${BLUE}Downloading from: $DOWNLOAD_URL${NC}" - # Close Cursor instances / 关闭Cursor实例 - close_cursor_instances + download "$DOWNLOAD_URL" "$TMP_DIR/cursor-id-modifier" - # Backup storage.json / 备份storage.json - backup_storage_json + # Install binary + echo -e "${BLUE}Installing...${NC}" + chmod +x "$TMP_DIR/cursor-id-modifier" + sudo mv "$TMP_DIR/cursor-id-modifier" "$INSTALL_DIR/" - OS=$(detect_os) - print_status "$(get_message 1) $OS" + echo -e "${GREEN}Installation completed successfully!${NC}" + echo -e "${BLUE}Running cursor-id-modifier...${NC}" - # Install the binary / 安装二进制文件 - install_binary + # Run the program with sudo, preserving environment variables + export AUTOMATED_MODE=1 + if ! sudo -E cursor-id-modifier; then + echo -e "${RED}Failed to run cursor-id-modifier${NC}" + exit 1 + fi } -# Run main function / 运行主函数 -main "$@" +main