diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9feeb88..47d5628 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,33 @@ permissions: contents: write jobs: - goreleaser: + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + arch: [amd64, arm64] + exclude: + - os: windows-latest + arch: arm64 + - os: ubuntu-latest + arch: arm64 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: true + + - name: Run tests + run: go test -v ./... + + release: + needs: test runs-on: ubuntu-latest steps: - name: Checkout @@ -20,8 +46,13 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: + go-version: '1.21' + cache: true check-latest: true + - name: Install dependencies + run: go mod tidy + - name: Set Repository Variables run: | echo "GITHUB_REPOSITORY_OWNER=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV @@ -35,3 +66,56 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CGO_ENABLED: 0 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries + path: dist/* + retention-days: 5 + + - name: Generate changelog + if: success() + run: | + echo "# 🚀 Cursor ID Modifier ${{ github.ref_name }}" > ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + + echo "## ✨ Supported Platforms" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "### 🪟 Windows" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Windows x64 (64-bit)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Windows x86 (32-bit)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "### 🍎 macOS" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- macOS Intel (x64)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- macOS Apple Silicon (M1/M2)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "### 🐧 Linux" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Linux x64 (64-bit)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Linux x86 (32-bit)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Linux ARM64" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + + echo "## 📦 Quick Start" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "### Linux/macOS" >> ${{ github.workspace }}-CHANGELOG.txt + echo "\`\`\`bash" >> ${{ github.workspace }}-CHANGELOG.txt + echo "curl -fsSL https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.sh | sudo bash && cursor-id-modifier" >> ${{ github.workspace }}-CHANGELOG.txt + echo "\`\`\`" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "### Windows (PowerShell Admin)" >> ${{ github.workspace }}-CHANGELOG.txt + echo "\`\`\`powershell" >> ${{ github.workspace }}-CHANGELOG.txt + echo "irm https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.ps1 | iex; cursor-id-modifier" >> ${{ github.workspace }}-CHANGELOG.txt + echo "\`\`\`" >> ${{ github.workspace }}-CHANGELOG.txt + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + + echo "## 🔄 Changes" >> ${{ github.workspace }}-CHANGELOG.txt + echo "* 📦 Release version: ${{ github.ref_name }}" >> ${{ github.workspace }}-CHANGELOG.txt + echo "* 📝 Full changelog: https://github.com/${{ github.repository }}/commits/${{ github.ref_name }}" >> ${{ github.workspace }}-CHANGELOG.txt + + echo "" >> ${{ github.workspace }}-CHANGELOG.txt + echo "## 🔍 Download Files" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Windows: \`cursor-id-modifier_${{ github.ref_name }}_Windows_[x64/x86].zip\`" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- macOS: \`cursor-id-modifier_${{ github.ref_name }}_macOS_[x64/arm64]_[intel/apple_silicon].tar.gz\`" >> ${{ github.workspace }}-CHANGELOG.txt + echo "- Linux: \`cursor-id-modifier_${{ github.ref_name }}_Linux_[x64/x86/arm64].tar.gz\`" >> ${{ github.workspace }}-CHANGELOG.txt diff --git a/.gitignore b/.gitignore index 54f0356..ef225bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,40 @@ -# Binary files -*.dll -*.so -*.dylib - -# Build directories -releases/ +# Compiled binary cursor-id-modifier +cursor-id-modifier.exe +# Build output directories +bin/ +dist/ # Go specific go.sum -# 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..4a01b1f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,56 +1,110 @@ -project_name: cursor-id-modifier - before: hooks: - go mod tidy builds: - - env: + - main: ./cmd/cursor-id-modifier + binary: cursor-id-modifier + env: - CGO_ENABLED=0 - ldflags: - - -s -w -X main.version={{.Version}} goos: - linux - windows - darwin goarch: - - amd64 - - arm64 - mod_timestamp: '{{ .CommitTimestamp }}' + - amd64 # Intel 64-bit + - arm64 # Apple Silicon/ARM64 + - "386" # Intel 32-bit + ignore: + - goos: darwin + goarch: "386" # No 32-bit support for macOS + ldflags: + - -s -w + - -X main.version={{.Version}} flags: - -trimpath - binary: cursor_id_modifier_{{ .Version }}_{{ .Os }}_{{ .Arch }} + mod_timestamp: '{{ .CommitTimestamp }}' + + # Build matrix + matrix: + # Special builds for macOS + - goos: [darwin] + goarch: [amd64] + tags: ["intel"] + - goos: [darwin] + goarch: [arm64] + tags: ["apple_silicon"] + # Windows builds + - goos: [windows] + goarch: [amd64, "386"] + # Linux builds + - goos: [linux] + goarch: [amd64, arm64, "386"] archives: - - format: binary - name_template: "{{ .Binary }}" - allow_different_binary_count: true + - format: tar.gz + format_overrides: + - goos: windows + format: zip + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- .Os }}_ + {{- .Arch }} + {{- with .Tags }}_{{ . }}{{ end }} + files: + - README.md + - LICENSE + - scripts/* # Include installation scripts + replacements: + darwin: macOS + linux: Linux + windows: Windows + 386: x86 + amd64: x64 + arm64: arm64 + +checksum: + name_template: 'checksums.txt' + algorithm: sha256 changelog: - use: github sort: asc - groups: - - title: Features - regexp: "^.*feat[(\\w)]*:+.*$" - order: 0 - - 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 + header: | + ## Cursor ID Modifier {{ .Version }} + + ### Supported Platforms + - Windows: x64, x86 + - macOS: Intel (x64), Apple Silicon (M1/M2) + - Linux: x64, x86, ARM64 + + See [CHANGELOG](CHANGELOG.md) for more details. + footer: | + **Full Changelog**: https://github.com/dacrab/cursor-id-modifier/compare/{{ .PreviousTag }}...{{ .Tag }} + + ## Quick Installation + + **Linux/macOS**: + ```bash + curl -fsSL https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.sh | sudo bash && cursor-id-modifier + ``` + + **Windows** (PowerShell Admin): + ```powershell + irm https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.ps1 | iex; cursor-id-modifier + ``` + +snapshot: + name_template: "{{ incpatch .Version }}-next" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f60895c --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: build clean test vet + +# Build the application +build: + go build -v ./cmd/cursor-id-modifier + +# Clean build artifacts +clean: + rm -f cursor-id-modifier + go clean + +# Run tests +test: + go test -v ./... + +# Run go vet +vet: + go vet ./... + +# Run all checks +all: vet test build \ No newline at end of file diff --git a/README.md b/README.md index 0f3b3f6..ec98ffc 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@
-[![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) -[![Stars](https://img.shields.io/github/stars/yuaotian/go-cursor-help?style=flat-square&logo=github)](https://github.com/yuaotian/go-cursor-help/stargazers) +[![Release](https://img.shields.io/github/v/release/dacrab/cursor-id-modifier?style=flat-square&logo=github&color=blue)](https://github.com/dacrab/cursor-id-modifier/releases/latest) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=bookstack)](https://github.com/dacrab/cursor-id-modifier/blob/main/LICENSE) +[![Stars](https://img.shields.io/github/stars/dacrab/cursor-id-modifier?style=flat-square&logo=github)](https://github.com/dacrab/cursor-id-modifier/stargazers) [English](#-english) | [中文](#-chinese) @@ -27,73 +27,59 @@ this is a mistake. ### 💻 System Support -**Windows** ✅ x64 -**macOS** ✅ Intel & M-series -**Linux** ✅ x64 & ARM64 +**Windows** ✅ +- x64 (64-bit) +- x86 (32-bit) -### 📥 Installation +**macOS** ✅ +- Intel (x64) +- Apple Silicon (M1/M2) -#### Automatic Installation (Recommended) +**Linux** ✅ +- x64 (64-bit) +- x86 (32-bit) +- ARM64 -**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 +curl -fsSL https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.sh | sudo bash && cursor-id-modifier ``` -**Windows** (Run PowerShell as Admin) +**Windows**: Copy and paste in PowerShell (Admin): ```powershell -irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex +irm https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.ps1 | iex; cursor-id-modifier ``` -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 +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/dacrab/cursor-id-modifier/releases/latest): + +**Windows**: +- 64-bit: `cursor-id-modifier_vX.X.X_Windows_x64.zip` +- 32-bit: `cursor-id-modifier_vX.X.X_Windows_x86.zip` + +**macOS**: +- Intel: `cursor-id-modifier_vX.X.X_macOS_x64_intel.tar.gz` +- M1/M2: `cursor-id-modifier_vX.X.X_macOS_arm64_apple_silicon.tar.gz` + +**Linux**: +- 64-bit: `cursor-id-modifier_vX.X.X_Linux_x64.tar.gz` +- 32-bit: `cursor-id-modifier_vX.X.X_Linux_x86.tar.gz` +- ARM64: `cursor-id-modifier_vX.X.X_Linux_arm64.tar.gz` ### 🔧 Technical Details #### 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/` +- 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: @@ -103,10 +89,9 @@ The tool generates new unique identifiers for: - `telemetry.sqmId` #### 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 --- @@ -125,65 +110,25 @@ this is a mistake. ### 💻 系统支持 -**Windows** ✅ x64 -**macOS** ✅ Intel和M系列 -**Linux** ✅ x64和ARM64 - -### 📥 安装方法 +**Windows** ✅ x64 & x86 +**macOS** ✅ Intel & M-series +**Linux** ✅ x64 & ARM64 -#### 自动安装(推荐) +### 📥 一键解决 -**Linux/macOS** +**Linux/macOS**: 在终端中复制粘贴: ```bash -curl -fsSL https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.sh | sudo bash +curl -fsSL https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.sh | sudo bash && cursor-id-modifier ``` -**Windows** (以管理员身份运行PowerShell) +**Windows**: 在PowerShell(管理员)中复制粘贴: ```powershell -irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex +irm https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.ps1 | iex; cursor-id-modifier ``` -安装脚本会自动: -- 请求必要的权限(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 +就这样!脚本会: +1. 自动安装工具 +2. 立即重置Cursor试用期 ### 🔧 技术细节 @@ -201,10 +146,9 @@ irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/ins - `telemetry.sqmId` #### 安全特性 -- 自动备份现有配置 -- 安全的进程终止 -- 原子文件操作 -- 错误处理和回滚 +- ✅ 安全的进程终止 +- ✅ 原子文件操作 +- ✅ 错误处理和恢复 ## ⭐ Star History or Repobeats diff --git a/go.mod b/go.mod index 6b674f1..05e7116 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,17 @@ module cursor-id-modifier 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 + github.com/stretchr/testify v1.10.0 +) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b4bb98d..75c1611 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,20 @@ 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +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/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e3f3f53 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,150 @@ +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) + } + + // Set file permissions + if err := os.Chmod(m.configPath, 0666); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to set file permissions: %w", err) + } + + // Read existing config to preserve other fields + var originalFile map[string]interface{} + originalFileContent, err := os.ReadFile(m.configPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read original file: %w", err) + } else if err == nil { + if err := json.Unmarshal(originalFileContent, &originalFile); err != nil { + return fmt.Errorf("failed to parse original file: %w", err) + } + } else { + originalFile = make(map[string]interface{}) + } + + // 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" + + // Marshal with indentation + newFileContent, err := json.MarshalIndent(originalFile, "", " ") + 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, newFileContent, 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 { + dir.Sync() + dir.Close() + } + + 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..f5d8a7a --- /dev/null +++ b/internal/lang/lang.go @@ -0,0 +1,156 @@ +package lang + +import ( + "os" + "os/exec" + "strings" + "sync" +) + +// Language represents a supported language +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 { + 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 +} + +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 + for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { + if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") { + return CN + } + } + + // Check Windows language settings + if isWindows() { + if isWindowsChineseLocale() { + return CN + } + } else { + // Check Unix locale + if isUnixChineseLocale() { + return CN + } + } + + return EN +} + +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: { + 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", + }, +} diff --git a/internal/process/manager.go b/internal/process/manager.go new file mode 100644 index 0000000..3d29ee9 --- /dev/null +++ b/internal/process/manager.go @@ -0,0 +1,162 @@ +package process + +import ( + "context" + "fmt" + "os/exec" + "runtime" + "strings" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +// Config holds process manager configuration +type Config struct { + RetryAttempts int + RetryDelay time.Duration + Timeout time.Duration +} + +// DefaultConfig returns the default configuration +func DefaultConfig() *Config { + return &Config{ + RetryAttempts: 3, + RetryDelay: time.Second, + Timeout: 30 * time.Second, + } +} + +// Manager handles process-related operations +type Manager struct { + config *Config + log *logrus.Logger + mu sync.Mutex +} + +// NewManager creates a new process manager +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, + } +} + +// KillCursorProcesses attempts to kill all Cursor processes +func (m *Manager) KillCursorProcesses() error { + m.mu.Lock() + defer m.mu.Unlock() + + ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout) + defer cancel() + + for attempt := 0; attempt < m.config.RetryAttempts; attempt++ { + m.log.Debugf("Attempt %d/%d to kill Cursor processes", attempt+1, m.config.RetryAttempts) + + if err := m.killProcess(ctx); err != nil { + m.log.Warnf("Failed to kill processes on attempt %d: %v", attempt+1, err) + time.Sleep(m.config.RetryDelay) + continue + } + return nil + } + + return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.RetryAttempts) +} + +// IsCursorRunning checks if any Cursor process is running +func (m *Manager) IsCursorRunning() bool { + m.mu.Lock() + defer m.mu.Unlock() + + processes, err := m.listCursorProcesses() + if err != nil { + m.log.Warnf("Failed to list Cursor processes: %v", err) + return false + } + + return len(processes) > 0 +} + +func (m *Manager) killProcess(ctx context.Context) error { + if runtime.GOOS == "windows" { + return m.killWindowsProcess(ctx) + } + return m.killUnixProcess(ctx) +} + +func (m *Manager) killWindowsProcess(ctx context.Context) error { + // First try graceful termination + if err := exec.CommandContext(ctx, "taskkill", "/IM", "Cursor.exe").Run(); err != nil { + m.log.Debugf("Graceful termination failed: %v", err) + } + + time.Sleep(m.config.RetryDelay) + + // Force kill if still running + if err := exec.CommandContext(ctx, "taskkill", "/F", "/IM", "Cursor.exe").Run(); err != nil { + return fmt.Errorf("failed to force kill Cursor process: %w", err) + } + + return nil +} + +func (m *Manager) killUnixProcess(ctx context.Context) error { + processes, err := m.listCursorProcesses() + if err != nil { + return fmt.Errorf("failed to list processes: %w", err) + } + + for _, pid := range processes { + if err := m.forceKillProcess(ctx, pid); err != nil { + m.log.Warnf("Failed to kill process %s: %v", pid, err) + continue + } + } + + return nil +} + +func (m *Manager) forceKillProcess(ctx context.Context, pid string) error { + // Try graceful termination first + if err := exec.CommandContext(ctx, "kill", pid).Run(); err == nil { + m.log.Debugf("Process %s terminated gracefully", pid) + time.Sleep(2 * time.Second) + return nil + } + + // 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) + } + + m.log.Debugf("Process %s force killed", pid) + return nil +} + +func (m *Manager) listCursorProcesses() ([]string, error) { + cmd := exec.Command("ps", "aux") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute ps command: %w", err) + } + + var pids []string + for _, line := range strings.Split(string(output), "\n") { + if strings.Contains(strings.ToLower(line), "apprun") { + fields := strings.Fields(line) + if len(fields) > 1 { + pids = append(pids, fields[1]) + } + } + } + + return pids, nil +} diff --git a/internal/ui/display.go b/internal/ui/display.go new file mode 100644 index 0000000..0f94f69 --- /dev/null +++ b/internal/ui/display.go @@ -0,0 +1,101 @@ +package ui + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/fatih/color" +) + +// Display handles UI display operations +type Display struct { + spinner *Spinner +} + +// NewDisplay creates a new display handler +func NewDisplay(spinner *Spinner) *Display { + if spinner == nil { + spinner = NewSpinner(nil) + } + return &Display{ + spinner: spinner, + } +} + +// ShowProgress shows a progress message with 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() +} + +// ClearScreen clears the terminal screen +func (d *Display) ClearScreen() error { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/c", "cls") + } else { + cmd = exec.Command("clear") + } + cmd.Stdout = os.Stdout + return cmd.Run() +} + +// ShowProcessStatus shows the current process status +func (d *Display) ShowProcessStatus(message string) { + fmt.Printf("\r%s", strings.Repeat(" ", 80)) // Clear line + fmt.Printf("\r%s", color.CyanString("⚡ "+message)) +} + +// ShowPrivilegeError shows the privilege error message +func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) { + red := color.New(color.FgRed, color.Bold) + yellow := color.New(color.FgYellow) + + red.Println(errorMsg) + if runtime.GOOS == "windows" { + yellow.Println(adminMsg) + } else { + yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0])) + } +} + +// ShowSuccess shows a success message +func (d *Display) ShowSuccess(successMsg, restartMsg string) { + green := color.New(color.FgGreen, color.Bold) + yellow := color.New(color.FgYellow, color.Bold) + + green.Printf("\n%s\n", successMsg) + yellow.Printf("%s\n", restartMsg) +} + +// ShowError shows an error message +func (d *Display) ShowError(message string) { + red := color.New(color.FgRed, color.Bold) + red.Printf("\n%s\n", message) +} + +// ShowWarning shows a warning message +func (d *Display) ShowWarning(message string) { + yellow := color.New(color.FgYellow, color.Bold) + yellow.Printf("\n%s\n", message) +} + +// ShowInfo shows an info message +func (d *Display) ShowInfo(message string) { + cyan := color.New(color.FgCyan) + cyan.Printf("\n%s\n", message) +} + +// ShowPrompt shows a prompt message and waits for user input +func (d *Display) ShowPrompt(message string) { + fmt.Print(message) + os.Stdout.Sync() +} diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go new file mode 100644 index 0000000..4f85fcb --- /dev/null +++ b/internal/ui/spinner.go @@ -0,0 +1,110 @@ +package ui + +import ( + "fmt" + "sync" + "time" + + "github.com/fatih/color" +) + +// SpinnerConfig defines spinner configuration +type SpinnerConfig struct { + Frames []string + Delay time.Duration +} + +// 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{}), + } +} + +// SetMessage sets the spinner message +func (s *Spinner) SetMessage(message string) { + s.mu.Lock() + defer s.mu.Unlock() + s.message = message +} + +// Start starts 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 stops 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.Println() +} + +// IsActive returns whether the spinner is currently active +func (s *Spinner) IsActive() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.active +} + +func (s *Spinner) run() { + ticker := time.NewTicker(s.config.Delay) + defer ticker.Stop() + + 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)] + message := s.message + s.current++ + s.mu.RUnlock() + + fmt.Printf("\r%s %s", color.CyanString(frame), 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..5a0058f --- /dev/null +++ b/pkg/idgen/generator.go @@ -0,0 +1,95 @@ +package idgen + +import ( + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "math/big" + "sync" +) + +// Generator handles the generation of various IDs +type Generator struct { + charsetMu sync.RWMutex + charset string +} + +// NewGenerator creates a new ID generator with default settings +func NewGenerator() *Generator { + return &Generator{ + charset: "0123456789ABCDEFGHJKLMNPQRSTVWXYZ", + } +} + +// SetCharset allows customizing the character set used for ID generation +func (g *Generator) SetCharset(charset string) { + g.charsetMu.Lock() + defer g.charsetMu.Unlock() + g.charset = charset +} + +// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id] +func (g *Generator) GenerateMachineID() (string, error) { + prefix := "auth0|user_" + + // Generate random sequence number between 0-99 + seqNum, err := cryptorand.Int(cryptorand.Reader, big.NewInt(100)) + if err != nil { + return "", fmt.Errorf("failed to generate sequence number: %w", err) + } + sequence := fmt.Sprintf("%02d", seqNum.Int64()) + + uniqueID, err := g.generateUniqueID(23) + if err != nil { + return "", fmt.Errorf("failed to generate unique ID: %w", err) + } + + fullID := prefix + sequence + uniqueID + return hex.EncodeToString([]byte(fullID)), nil +} + +// GenerateMacMachineID generates a new MAC machine ID using SHA-256 +func (g *Generator) GenerateMacMachineID() (string, error) { + data := make([]byte, 32) + if _, err := cryptorand.Read(data); err != nil { + return "", fmt.Errorf("failed to generate random data: %w", err) + } + + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]), nil +} + +// GenerateDeviceID generates a new device ID in UUID v4 format +func (g *Generator) GenerateDeviceID() (string, error) { + uuid := make([]byte, 16) + if _, err := cryptorand.Read(uuid); err != nil { + return "", fmt.Errorf("failed to generate UUID: %w", err) + } + + // Set version (4) and variant (2) bits according to RFC 4122 + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = (uuid[8] & 0x3f) | 0x80 + + return fmt.Sprintf("%x-%x-%x-%x-%x", + uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]), nil +} + +// generateUniqueID generates a random string of specified length using the configured charset +func (g *Generator) generateUniqueID(length int) (string, error) { + g.charsetMu.RLock() + defer g.charsetMu.RUnlock() + + result := make([]byte, length) + max := big.NewInt(int64(len(g.charset))) + + for i := range result { + randNum, err := cryptorand.Int(cryptorand.Reader, max) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + result[i] = g.charset[randNum.Int64()] + } + + return string(result), nil +} diff --git a/pkg/idgen/generator_test.go b/pkg/idgen/generator_test.go new file mode 100644 index 0000000..81dcea9 --- /dev/null +++ b/pkg/idgen/generator_test.go @@ -0,0 +1,26 @@ +package idgen + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewGenerator(t *testing.T) { + gen := NewGenerator() + assert.NotNil(t, gen, "Generator should not be nil") +} + +func TestGenerateMachineID(t *testing.T) { + gen := NewGenerator() + id, err := gen.GenerateMachineID() + assert.NoError(t, err, "Should not return an error") + assert.NotEmpty(t, id, "Generated machine ID should not be empty") +} + +func TestGenerateDeviceID(t *testing.T) { + gen := NewGenerator() + id, err := gen.GenerateDeviceID() + assert.NoError(t, err, "Should not return an error") + assert.NotEmpty(t, id, "Generated device ID should not be empty") +} 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 index d7e38a9..7db5e7b 100644 --- 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..747b65b 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,303 +6,123 @@ if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdenti Exit } -# Set TLS to 1.2 / 设置 TLS 为 1.2 +# Set TLS to 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -# Colors for output / 输出颜色 +# 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:" -) +# Create temporary directory +$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) +New-Item -ItemType Directory -Path $TmpDir | Out-Null -$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" +# Cleanup function +function Cleanup { + if (Test-Path $TmpDir) { + Remove-Item -Recurse -Force $TmpDir } - return "en" -} - -# Get message based on language / 根据语言获取消息 -function Get-Message($Index) { - $lang = Get-SystemLanguage - if ($lang -eq "cn") { - return $CN_MESSAGES[$Index] - } - 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" } -function Write-Error($Message) { - Write-Host "${Red}[✗]${Reset} $Message" - Exit 1 +# Error handler +trap { + Write-Host "${Red}Error: $_${Reset}" + Cleanup + exit 1 } -# 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) - } - } -} - -# 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" +# Detect system architecture +function Get-SystemArch { + if ([Environment]::Is64BitOperatingSystem) { + return "amd64" + } else { + return "386" } } -# 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 -} - -# 在文件开头添加日志函数 -function Write-Log { - param( - [string]$Message, - [string]$Level = "INFO" +# Download with progress +function Download-WithProgress { + 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 "${Red}Failed to download: $_${Reset}" + return $false } } -# 添加安装前检查函数 -function Test-Prerequisites { - Write-Log "Checking prerequisites..." "INFO" +# Main installation function +function Install-CursorModifier { + Write-Host "${Blue}Starting installation...${Reset}" - # 检查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 "${Green}Detected architecture: $arch${Reset}" + + # 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/dacrab/cursor-id-modifier/releases/latest" + $downloadUrl = $latestRelease.assets | Where-Object { $_.name -match "windows_$arch" } | Select-Object -ExpandProperty browser_download_url + + if (!$downloadUrl) { + throw "Could not find download URL for windows_$arch" } - } catch { - Write-Log "Failed to check internet connection: $_" "ERROR" - return $false + } + catch { + Write-Host "${Red}Failed to get latest release: $_${Reset}" + exit 1 } - return $true -} - -# 添加文件验证函数 -function Test-FileHash { - param( - [string]$FilePath, - [string]$ExpectedHash - ) + # Download binary + Write-Host "${Blue}Downloading latest release...${Reset}" + $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 (!(Download-WithProgress -Url $downloadUrl -OutputFile $binaryPath)) { + exit 1 + } + # Install binary + Write-Host "${Blue}Installing...${Reset}" try { - $webClient = New-Object System.Net.WebClient - $webClient.Headers.Add("User-Agent", "PowerShell Script") + Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force - $webClient.DownloadFileAsync($Url, $OutFile) - - 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 - } - finally { - if ($webClient) { - $webClient.Dispose() - } - } -} - -# 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)" + Write-Host "${Red}Failed to install: $_${Reset}" + exit 1 } -} catch { - Write-Error "$(Get-Message 6) $_" -} - -# Verify download / 验证下载 -if (-not (Test-Path $tempFile)) { - Write-Error (Get-Message 7) + + Write-Host "${Green}Installation completed successfully!${Reset}" + Write-Host "${Blue}You can now run: cursor-id-modifier${Reset}" } -# 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) $_" + Install-CursorModifier } - -# 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: $_" +finally { + Cleanup } \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh index 0aed487..2112421 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,294 +2,98 @@ 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 information +detect_system() { + local os arch -# 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 -} + case "$(uname -s)" in + Linux*) os="linux";; + Darwin*) os="darwin";; + *) echo "Unsupported OS"; exit 1;; + esac -# 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 -} + case "$(uname -m)" in + x86_64) arch="amd64";; + aarch64) arch="arm64";; + arm64) arch="arm64";; + *) echo "Unsupported architecture"; exit 1;; + esac -# 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 + echo "$os $arch" } -# 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 +# Download with progress using curl or wget +download() { + local url="$1" + local output="$2" - if [ -f "$storage_path" ]; then - cp "$storage_path" "${storage_path}.backup" - print_success "$(get_message 19) ${storage_path}.backup" - fi -} - -# Detect OS / 检测操作系统 -detect_os() { - if [[ "$OSTYPE" == "darwin"* ]]; then - echo "darwin" - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - echo "linux" + if command -v curl >/dev/null 2>&1; then + curl -#L "$url" -o "$output" + elif command -v wget >/dev/null 2>&1; then + wget --show-progress -q "$url" -O "$output" else - print_error "$(get_message 12)" + echo "Error: curl or wget is required" + exit 1 fi } -# 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/' -} - -# Get the binary name based on OS and architecture / 根据操作系统和架构获取二进制文件名 -get_binary_name() { - OS=$(detect_os) - ARCH=$(uname -m) - VERSION=$(get_latest_version) +# Check and create installation directory +setup_install_dir() { + local install_dir="$1" - case "$ARCH" in - x86_64) - echo "cursor_id_modifier_${VERSION}_${OS}_amd64" - ;; - aarch64|arm64) - echo "cursor_id_modifier_${VERSION}_${OS}_arm64" - ;; - *) - print_error "$(get_message 13) $ARCH" - ;; - esac -} - -# Add download progress display function -download_with_progress() { - local url="$1" - local output_file="$2" - - curl -L -f --progress-bar "$url" -o "$output_file" - return $? + if [ ! -d "$install_dir" ]; then + mkdir -p "$install_dir" || { + echo "Failed to create installation directory" + exit 1 + } + fi } -# 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}" - - if ! download_with_progress "$DOWNLOAD_URL" "$TMP_DIR/$BINARY_NAME"; then - rm -rf "$TMP_DIR" - print_error "$(get_message 8) $DOWNLOAD_URL" - fi +# Main installation function +main() { + echo -e "${BLUE}Starting installation...${NC}" - if [ ! -f "$TMP_DIR/$BINARY_NAME" ]; then - rm -rf "$TMP_DIR" - print_error "$(get_message 9)" - fi + # Detect system + read -r OS ARCH <<< "$(detect_system)" + echo -e "${GREEN}Detected: $OS $ARCH${NC}" - print_status "$(get_message 4)" + # Set installation directory INSTALL_DIR="/usr/local/bin" + [ "$OS" = "darwin" ] && INSTALL_DIR="/usr/local/bin" - # Create directory if it doesn't exist - mkdir -p "$INSTALL_DIR" - - # 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 - - if ! chmod +x "$INSTALL_DIR/$FINAL_BINARY_NAME"; then - rm -rf "$TMP_DIR" - print_error "Failed to set executable permissions" - fi - - # Cleanup - print_status "$(get_message 5)" - rm -rf "$TMP_DIR" + # Setup installation directory + setup_install_dir "$INSTALL_DIR" - print_success "$(get_message 6)" - printf "${GREEN}[✓]${NC} $(get_message 7)\n" "$FINAL_BINARY_NAME" + # Download latest release + LATEST_URL="https://api.github.com/repos/dacrab/cursor-id-modifier/releases/latest" + DOWNLOAD_URL=$(curl -s "$LATEST_URL" | grep "browser_download_url.*${OS}_${ARCH}" | cut -d '"' -f 4) - # 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)" + if [ -z "$DOWNLOAD_URL" ]; then + echo -e "${RED}Error: Could not find download URL for $OS $ARCH${NC}" + exit 1 fi - if ! command -v sudo >/dev/null 2>&1; then - print_error "$(get_message 11)" - fi -} - -# Main installation process / 主安装过程 -main() { - print_status "$(get_message 0)" - - # Check root privileges / 检查root权限 - check_root "$@" - - # Check required tools / 检查必需工具 - check_requirements - - # Close Cursor instances / 关闭Cursor实例 - close_cursor_instances - - # Backup storage.json / 备份storage.json - backup_storage_json + echo -e "${BLUE}Downloading latest release...${NC}" + download "$DOWNLOAD_URL" "$TMP_DIR/cursor-id-modifier" - OS=$(detect_os) - print_status "$(get_message 1) $OS" + # Install binary + echo -e "${BLUE}Installing...${NC}" + chmod +x "$TMP_DIR/cursor-id-modifier" + sudo mv "$TMP_DIR/cursor-id-modifier" "$INSTALL_DIR/" - # Install the binary / 安装二进制文件 - install_binary + echo -e "${GREEN}Installation completed successfully!${NC}" + echo -e "${BLUE}You can now run: cursor-id-modifier${NC}" } -# Run main function / 运行主函数 -main "$@" +main