mirror of
https://github.com/yuaotian/go-cursor-help.git
synced 2025-06-08 12:32:06 +08:00
commit
778f48e57a
53
.github/workflows/auto-tag.yml
vendored
Normal file
53
.github/workflows/auto-tag.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
name: Auto Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '.gitignore'
|
||||
|
||||
jobs:
|
||||
auto-tag:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
new_tag: ${{ steps.get_latest_tag.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get latest tag
|
||||
id: get_latest_tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
latest_tag=$(git tag -l 'v*' --sort=-v:refname | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "version=v0.1.0" >> $GITHUB_OUTPUT
|
||||
else
|
||||
major=$(echo $latest_tag | cut -d. -f1)
|
||||
minor=$(echo $latest_tag | cut -d. -f2)
|
||||
patch=$(echo $latest_tag | cut -d. -f3)
|
||||
new_patch=$((patch + 1))
|
||||
echo "version=$major.$minor.$new_patch" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create new tag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
new_tag=${{ steps.get_latest_tag.outputs.version }}
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git tag -a $new_tag -m "Release $new_tag"
|
||||
git push origin $new_tag
|
||||
|
||||
release:
|
||||
needs: auto-tag
|
||||
uses: ./.github/workflows/release.yml
|
||||
secrets: inherit
|
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@ -1,12 +1,14 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@ -16,16 +18,29 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
submodules: recursive
|
||||
|
||||
- name: Debug Files
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
echo "Current directory contents:"
|
||||
ls -R
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
check-latest: true
|
||||
go-version: '1.21'
|
||||
cache: true
|
||||
|
||||
- name: Set Repository Variables
|
||||
run: |
|
||||
echo "GITHUB_REPOSITORY_OWNER=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
|
||||
echo "GITHUB_REPOSITORY_NAME=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
if: ${{ env.GPG_PRIVATE_KEY != '' }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
@ -35,3 +50,4 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
|
||||
|
45
.gitignore
vendored
45
.gitignore
vendored
@ -1,23 +1,40 @@
|
||||
# Binary files
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Build directories
|
||||
releases/
|
||||
cursor-id-modifier
|
||||
# Compiled binary
|
||||
/cursor-id-modifier
|
||||
/cursor-id-modifier.exe
|
||||
|
||||
# Build output directories
|
||||
bin/
|
||||
dist/
|
||||
|
||||
# Go specific
|
||||
go.sum
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*~
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# System files
|
||||
# OS specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.vscode
|
||||
/.idea
|
||||
# Build and release artifacts
|
||||
releases/
|
||||
*.syso
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test files
|
||||
*.test
|
||||
*.out
|
||||
coverage.txt
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*~
|
||||
*.bak
|
||||
*.log
|
@ -1,14 +1,13 @@
|
||||
project_name: cursor-id-modifier
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- id: cursor-id-modifier
|
||||
main: ./cmd/cursor-id-modifier/main.go
|
||||
binary: cursor-id-modifier
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
@ -16,24 +15,41 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- "386"
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X 'main.version={{.Version}}'
|
||||
flags:
|
||||
- -trimpath
|
||||
binary: cursor_id_modifier_{{ .Version }}_{{ .Os }}_{{ .Arch }}
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
|
||||
archives:
|
||||
- format: binary
|
||||
name_template: "{{ .Binary }}"
|
||||
allow_different_binary_count: true
|
||||
- id: binary
|
||||
format: binary
|
||||
name_template: >-
|
||||
{{ .Binary }}_
|
||||
{{- .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x64{{ end }}
|
||||
{{- if eq .Arch "386" }}x86{{ end }}
|
||||
{{- if eq .Arch "arm64" }}arm64{{ end }}
|
||||
{{- if and (eq .Os "darwin") (eq .Arch "amd64") }}_intel{{ end }}
|
||||
{{- if and (eq .Os "darwin") (eq .Arch "arm64") }}_apple_silicon{{ end }}
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
use: github
|
||||
sort: asc
|
||||
use: github
|
||||
groups:
|
||||
- title: Features
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
order: 0
|
||||
- title: 'Bug Fixes'
|
||||
- title: 'Bug fixes'
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: Others
|
||||
@ -43,14 +59,39 @@ changelog:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^ci:'
|
||||
- '^chore:'
|
||||
- Merge pull request
|
||||
- Merge branch
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: '{{ .Env.GITHUB_REPOSITORY_OWNER }}'
|
||||
name: '{{ .Env.GITHUB_REPOSITORY_NAME }}'
|
||||
owner: dacrab
|
||||
name: go-cursor-help
|
||||
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/go-cursor-help/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
## Quick Installation
|
||||
|
||||
**Linux/macOS**:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
**Windows** (PowerShell Admin):
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
53
CHANGELOG.md
Normal file
53
CHANGELOG.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 📝 Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.23] - 2024-12-29 🚀
|
||||
|
||||
### ✨ Features
|
||||
- **Initial Release**: First public release of Cursor ID Modifier
|
||||
- **Multi-Platform Support**:
|
||||
- 🪟 Windows (x64, x86)
|
||||
- 🍎 macOS (Intel & Apple Silicon)
|
||||
- 🐧 Linux (x64, x86, ARM64)
|
||||
- **Installation**:
|
||||
- Automated installation scripts for all platforms
|
||||
- One-line installation commands
|
||||
- Secure download and verification
|
||||
- **Core Functionality**:
|
||||
- Telemetry ID modification for Cursor IDE
|
||||
- Automatic process management
|
||||
- Secure configuration handling
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- **Installation**:
|
||||
- Fixed environment variable preservation in sudo operations
|
||||
- Enhanced error handling during installation
|
||||
- Improved binary download reliability
|
||||
- **Process Management**:
|
||||
- Improved Cursor process detection accuracy
|
||||
- Enhanced process termination reliability
|
||||
- Better handling of edge cases
|
||||
- **User Experience**:
|
||||
- Enhanced error messages and user feedback
|
||||
- Improved progress indicators
|
||||
- Better handling of system permissions
|
||||
|
||||
### 🔧 Technical Improvements
|
||||
- Optimized binary size with proper build flags
|
||||
- Enhanced cross-platform compatibility
|
||||
- Improved error handling and logging
|
||||
- Better system resource management
|
||||
|
||||
### 📚 Documentation
|
||||
- Added comprehensive installation instructions
|
||||
- Included platform-specific guidelines
|
||||
- Enhanced error troubleshooting guide
|
||||
|
||||
---
|
||||
*For more details about the changes, please refer to the [commit history](https://github.com/dacrab/go-cursor-help/commits/main).*
|
||||
|
||||
[0.1.22]: https://github.com/dacrab/go-cursor-help/releases/tag/v0.1.23
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 dacrab
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
290
README.md
290
README.md
@ -2,23 +2,25 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/yuaotian/go-cursor-help/releases/latest)
|
||||
[](https://github.com/yuaotian/go-cursor-help/blob/main/LICENSE)
|
||||
[](https://github.com/yuaotian/go-cursor-help/stargazers)
|
||||
[](https://github.com/dacrab/go-cursor-help/releases/latest)
|
||||
[](https://github.com/dacrab/go-cursor-help/blob/master/LICENSE)
|
||||
[](https://github.com/dacrab/go-cursor-help/stargazers)
|
||||
|
||||
[English](#-english) | [中文](#-chinese)
|
||||
[🌟 English](#english) | [🌏 中文](#chinese)
|
||||
|
||||
<img src="https://ai-cursor.com/wp-content/uploads/2024/09/logo-cursor-ai-png.webp" alt="Cursor Logo" width="120"/>
|
||||
|
||||
</div>
|
||||
|
||||
# 🌟 English
|
||||
---
|
||||
|
||||
## 🌟 English
|
||||
|
||||
### 📝 Description
|
||||
|
||||
Resets Cursor's free trial limitation when you see:
|
||||
> Resets Cursor's free trial limitation when you see:
|
||||
|
||||
```
|
||||
```text
|
||||
Too many free trial accounts used on this machine.
|
||||
Please upgrade to pro. We have this limit in place
|
||||
to prevent abuse. Please let us know if you believe
|
||||
@ -27,96 +29,120 @@ this is a mistake.
|
||||
|
||||
### 💻 System Support
|
||||
|
||||
**Windows** ✅ x64
|
||||
**macOS** ✅ Intel & M-series
|
||||
**Linux** ✅ x64 & ARM64
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
### 📥 Installation
|
||||
**Windows** ✅
|
||||
- x64 (64-bit)
|
||||
- x86 (32-bit)
|
||||
|
||||
#### Automatic Installation (Recommended)
|
||||
</td>
|
||||
<td>
|
||||
|
||||
**Linux/macOS**
|
||||
**macOS** ✅
|
||||
- Intel (x64)
|
||||
- Apple Silicon (M1/M2)
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
**Linux** ✅
|
||||
- x64 (64-bit)
|
||||
- x86 (32-bit)
|
||||
- ARM64
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🚀 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/go-cursor-help/master/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
**Windows** (Run PowerShell as Admin)
|
||||
**Windows**: Copy and paste in PowerShell
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
The installation script will automatically:
|
||||
- Request necessary privileges (sudo/admin)
|
||||
- Close any running Cursor instances
|
||||
- Backup existing configuration
|
||||
- Install the tool
|
||||
- Add it to system PATH
|
||||
- Clean up temporary files
|
||||
#### Windows Installation Features:
|
||||
- 🔍 Automatically detects and uses PowerShell 7 if available
|
||||
- 🛡️ Requests administrator privileges via UAC prompt
|
||||
- 📝 Falls back to Windows PowerShell if PS7 isn't found
|
||||
- 💡 Provides manual instructions if elevation fails
|
||||
|
||||
#### Manual Installation
|
||||
That's it! The script will:
|
||||
1. ✨ Install the tool automatically
|
||||
2. 🔄 Reset your Cursor trial immediately
|
||||
|
||||
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_*
|
||||
### 📦 Manual Installation
|
||||
|
||||
# Windows (PowerShell Admin)
|
||||
.\cursor_id_modifier_*.exe
|
||||
```
|
||||
> Download the appropriate file for your system from [releases](https://github.com/dacrab/go-cursor-help/releases/latest)
|
||||
|
||||
#### Manual Configuration Method
|
||||
<details>
|
||||
<summary>Windows Packages</summary>
|
||||
|
||||
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
|
||||
- 64-bit: `cursor-id-modifier_windows_x64.exe`
|
||||
- 32-bit: `cursor-id-modifier_windows_x86.exe`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>macOS Packages</summary>
|
||||
|
||||
- Intel: `cursor-id-modifier_darwin_x64_intel`
|
||||
- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux Packages</summary>
|
||||
|
||||
- 64-bit: `cursor-id-modifier_linux_x64`
|
||||
- 32-bit: `cursor-id-modifier_linux_x86`
|
||||
- ARM64: `cursor-id-modifier_linux_arm64`
|
||||
</details>
|
||||
|
||||
### 🔧 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/`
|
||||
<details>
|
||||
<summary><b>Configuration Files</b></summary>
|
||||
|
||||
The program modifies Cursor's `storage.json` config file located at:
|
||||
|
||||
- Windows: `%APPDATA%\Cursor\User\globalStorage\storage.json`
|
||||
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/storage.json`
|
||||
- Linux: `~/.config/Cursor/User/globalStorage/storage.json`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Modified Fields</b></summary>
|
||||
|
||||
#### Modified Fields
|
||||
The tool generates new unique identifiers for:
|
||||
- `telemetry.machineId`
|
||||
- `telemetry.macMachineId`
|
||||
- `telemetry.devDeviceId`
|
||||
- `telemetry.sqmId`
|
||||
</details>
|
||||
|
||||
#### Safety Features
|
||||
- Automatic backup of existing configuration
|
||||
- Safe process termination
|
||||
- Atomic file operations
|
||||
- Error handling and rollback
|
||||
<details>
|
||||
<summary><b>Safety Features</b></summary>
|
||||
|
||||
- ✅ Safe process termination
|
||||
- ✅ Atomic file operations
|
||||
- ✅ Error handling and recovery
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
# 🌏 Chinese
|
||||
## 🌏 Chinese
|
||||
|
||||
### 📝 问题描述
|
||||
|
||||
当看到以下提示时重置Cursor试用期:
|
||||
> 当看到以下提示时重置Cursor试用期:
|
||||
|
||||
```
|
||||
```text
|
||||
Too many free trial accounts used on this machine.
|
||||
Please upgrade to pro. We have this limit in place
|
||||
to prevent abuse. Please let us know if you believe
|
||||
@ -125,86 +151,101 @@ this is a mistake.
|
||||
|
||||
### 💻 系统支持
|
||||
|
||||
**Windows** ✅ x64
|
||||
**macOS** ✅ Intel和M系列
|
||||
**Linux** ✅ x64和ARM64
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
### 📥 安装方法
|
||||
**Windows** ✅
|
||||
- x64 & x86
|
||||
|
||||
#### 自动安装(推荐)
|
||||
</td>
|
||||
<td>
|
||||
|
||||
**Linux/macOS**
|
||||
**macOS** ✅
|
||||
- Intel & M-series
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
**Linux** ✅
|
||||
- x64 & ARM64
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🚀 一键解决
|
||||
|
||||
**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/go-cursor-help/master/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
**Windows** (以管理员身份运行PowerShell)
|
||||
**Windows**: 在PowerShell中复制粘贴
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
安装脚本会自动:
|
||||
- 请求必要的权限(sudo/管理员)
|
||||
- 关闭所有运行中的Cursor实例
|
||||
- 备份现有配置
|
||||
- 安装工具
|
||||
- 添加到系统PATH
|
||||
- 清理临时文件
|
||||
#### Windows 安装特性:
|
||||
- 🔍 自动检测并使用 PowerShell 7(如果可用)
|
||||
- 🛡️ 通过 UAC 提示请求管理员权限
|
||||
- 📝 如果没有 PS7 则使用 Windows PowerShell
|
||||
- 💡 如果提权失败会提供手动说明
|
||||
|
||||
#### 手动安装
|
||||
That's it! The script will:
|
||||
1. ✨ 自动安装工具
|
||||
2. 🔄 立即重置Cursor试用期
|
||||
|
||||
1. 从[发布页面](https://github.com/yuaotian/go-cursor-help/releases)下载适合您系统的最新版本
|
||||
2. 解压并以管理员/root权限运行:
|
||||
```bash
|
||||
# Linux/macOS
|
||||
chmod +x ./cursor_id_modifier_* # 添加执行权限
|
||||
sudo ./cursor_id_modifier_*
|
||||
### 📦 Manual Installation
|
||||
|
||||
# Windows (PowerShell 管理员)
|
||||
.\cursor_id_modifier_*.exe
|
||||
```
|
||||
> Download the appropriate file for your system from [releases](https://github.com/dacrab/go-cursor-help/releases/latest)
|
||||
|
||||
#### 手动配置方法
|
||||
<details>
|
||||
<summary>Windows Packages</summary>
|
||||
|
||||
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
|
||||
- 64-bit: `cursor-id-modifier_windows_x64.exe`
|
||||
- 32-bit: `cursor-id-modifier_windows_x86.exe`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>macOS Packages</summary>
|
||||
|
||||
- Intel: `cursor-id-modifier_darwin_x64_intel`
|
||||
- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux Packages</summary>
|
||||
|
||||
- 64-bit: `cursor-id-modifier_linux_x64`
|
||||
- 32-bit: `cursor-id-modifier_linux_x86`
|
||||
- ARM64: `cursor-id-modifier_linux_arm64`
|
||||
</details>
|
||||
|
||||
### 🔧 技术细节
|
||||
|
||||
#### 配置文件
|
||||
<details>
|
||||
<summary><b>配置文件</b></summary>
|
||||
|
||||
程序修改Cursor的`storage.json`配置文件,位于:
|
||||
|
||||
- Windows: `%APPDATA%\Cursor\User\globalStorage\`
|
||||
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/`
|
||||
- Linux: `~/.config/Cursor/User/globalStorage/`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>修改字段</b></summary>
|
||||
|
||||
#### 修改字段
|
||||
工具会生成新的唯一标识符:
|
||||
- `telemetry.machineId`
|
||||
- `telemetry.macMachineId`
|
||||
- `telemetry.devDeviceId`
|
||||
- `telemetry.sqmId`
|
||||
</details>
|
||||
|
||||
#### 安全特性
|
||||
- 自动备份现有配置
|
||||
- 安全的进程终止
|
||||
- 原子文件操作
|
||||
- 错误处理和回滚
|
||||
<details>
|
||||
<summary><b>安全特性</b></summary>
|
||||
|
||||
## 🔔 关注公众号
|
||||
#### 获取更多精彩内容
|
||||
@ -214,18 +255,23 @@ irm https://raw.githubusercontent.com/yuaotian/go-cursor-help/master/scripts/ins
|
||||
- 更多AI工具和开发资源
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ⭐ Star History or Repobeats
|
||||
## ⭐ Project Stats
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://star-history.com/#yuaotian/go-cursor-help&Date)
|
||||
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
<details>
|
||||
<summary><b>MIT License</b></summary>
|
||||
|
||||
Copyright (c) 2024
|
||||
|
||||
@ -238,4 +284,4 @@ furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
</details>
|
||||
|
330
cmd/cursor-id-modifier/main.go
Normal file
330
cmd/cursor-id-modifier/main.go
Normal file
@ -0,0 +1,330 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/dacrab/go-cursor-help/internal/config"
|
||||
"github.com/dacrab/go-cursor-help/internal/lang"
|
||||
"github.com/dacrab/go-cursor-help/internal/process"
|
||||
"github.com/dacrab/go-cursor-help/internal/ui"
|
||||
"github.com/dacrab/go-cursor-help/pkg/idgen"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Global variables
|
||||
var (
|
||||
version = "dev"
|
||||
setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode")
|
||||
showVersion = flag.Bool("v", false, "show version information")
|
||||
log = logrus.New()
|
||||
)
|
||||
|
||||
func main() {
|
||||
setupErrorRecovery()
|
||||
handleFlags()
|
||||
setupLogger()
|
||||
|
||||
username := getCurrentUser()
|
||||
log.Debug("Running as user:", username)
|
||||
|
||||
// Initialize components
|
||||
display := ui.NewDisplay(nil)
|
||||
configManager := initConfigManager(username)
|
||||
generator := idgen.NewGenerator()
|
||||
processManager := process.NewManager(nil, log)
|
||||
|
||||
// Check and handle privileges
|
||||
if err := handlePrivileges(display); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Setup display
|
||||
setupDisplay(display)
|
||||
|
||||
text := lang.GetText()
|
||||
|
||||
// Handle Cursor processes
|
||||
if err := handleCursorProcesses(display, processManager); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle configuration
|
||||
oldConfig := readExistingConfig(display, configManager, text)
|
||||
newConfig := generateNewConfig(display, generator, oldConfig, text)
|
||||
|
||||
if err := saveConfiguration(display, configManager, newConfig); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Show completion messages
|
||||
showCompletionMessages(display)
|
||||
|
||||
if os.Getenv("AUTOMATED_MODE") != "1" {
|
||||
waitExit()
|
||||
}
|
||||
}
|
||||
|
||||
func setupErrorRecovery() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Panic recovered: %v\n", r)
|
||||
debug.PrintStack()
|
||||
waitExit()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handleFlags() {
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
fmt.Printf("Cursor ID Modifier v%s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger() {
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
DisableLevelTruncation: true,
|
||||
PadLevelText: true,
|
||||
})
|
||||
log.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
func getCurrentUser() string {
|
||||
if username := os.Getenv("SUDO_USER"); username != "" {
|
||||
return username
|
||||
}
|
||||
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
func initConfigManager(username string) *config.Manager {
|
||||
configManager, err := config.NewManager(username)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return configManager
|
||||
}
|
||||
|
||||
func handlePrivileges(display *ui.Display) error {
|
||||
isAdmin, err := checkAdminPrivileges()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
waitExit()
|
||||
return err
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
if runtime.GOOS == "windows" {
|
||||
return handleWindowsPrivileges(display)
|
||||
}
|
||||
display.ShowPrivilegeError(
|
||||
lang.GetText().PrivilegeError,
|
||||
lang.GetText().RunWithSudo,
|
||||
lang.GetText().SudoExample,
|
||||
)
|
||||
waitExit()
|
||||
return fmt.Errorf("insufficient privileges")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleWindowsPrivileges(display *ui.Display) error {
|
||||
message := "\nRequesting administrator privileges..."
|
||||
if lang.GetCurrentLanguage() == lang.CN {
|
||||
message = "\n请求管理员权限..."
|
||||
}
|
||||
fmt.Println(message)
|
||||
|
||||
if err := selfElevate(); err != nil {
|
||||
log.Error(err)
|
||||
display.ShowPrivilegeError(
|
||||
lang.GetText().PrivilegeError,
|
||||
lang.GetText().RunAsAdmin,
|
||||
lang.GetText().RunWithSudo,
|
||||
lang.GetText().SudoExample,
|
||||
)
|
||||
waitExit()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupDisplay(display *ui.Display) {
|
||||
if err := display.ClearScreen(); err != nil {
|
||||
log.Warn("Failed to clear screen:", err)
|
||||
}
|
||||
display.ShowLogo()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error {
|
||||
if os.Getenv("AUTOMATED_MODE") == "1" {
|
||||
log.Debug("Running in automated mode, skipping Cursor process closing")
|
||||
return nil
|
||||
}
|
||||
|
||||
display.ShowProgress("Closing Cursor...")
|
||||
log.Debug("Attempting to close Cursor processes")
|
||||
|
||||
if err := processManager.KillCursorProcesses(); err != nil {
|
||||
log.Error("Failed to close Cursor:", err)
|
||||
display.StopProgress()
|
||||
display.ShowError("Failed to close Cursor. Please close it manually and try again.")
|
||||
waitExit()
|
||||
return err
|
||||
}
|
||||
|
||||
if processManager.IsCursorRunning() {
|
||||
log.Error("Cursor processes still detected after closing")
|
||||
display.StopProgress()
|
||||
display.ShowError("Failed to close Cursor completely. Please close it manually and try again.")
|
||||
waitExit()
|
||||
return fmt.Errorf("cursor still running")
|
||||
}
|
||||
|
||||
log.Debug("Successfully closed all Cursor processes")
|
||||
display.StopProgress()
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig {
|
||||
fmt.Println()
|
||||
display.ShowProgress(text.ReadingConfig)
|
||||
oldConfig, err := configManager.ReadConfig()
|
||||
if err != nil {
|
||||
log.Warn("Failed to read existing config:", err)
|
||||
oldConfig = nil
|
||||
}
|
||||
display.StopProgress()
|
||||
fmt.Println()
|
||||
return oldConfig
|
||||
}
|
||||
|
||||
func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig {
|
||||
display.ShowProgress(text.GeneratingIds)
|
||||
newConfig := &config.StorageConfig{}
|
||||
|
||||
if machineID, err := generator.GenerateMachineID(); err != nil {
|
||||
log.Fatal("Failed to generate machine ID:", err)
|
||||
} else {
|
||||
newConfig.TelemetryMachineId = machineID
|
||||
}
|
||||
|
||||
if macMachineID, err := generator.GenerateMacMachineID(); err != nil {
|
||||
log.Fatal("Failed to generate MAC machine ID:", err)
|
||||
} else {
|
||||
newConfig.TelemetryMacMachineId = macMachineID
|
||||
}
|
||||
|
||||
if deviceID, err := generator.GenerateDeviceID(); err != nil {
|
||||
log.Fatal("Failed to generate device ID:", err)
|
||||
} else {
|
||||
newConfig.TelemetryDevDeviceId = deviceID
|
||||
}
|
||||
|
||||
if oldConfig != nil && oldConfig.TelemetrySqmId != "" {
|
||||
newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId
|
||||
} else if sqmID, err := generator.GenerateMacMachineID(); err != nil {
|
||||
log.Fatal("Failed to generate SQM ID:", err)
|
||||
} else {
|
||||
newConfig.TelemetrySqmId = sqmID
|
||||
}
|
||||
|
||||
display.StopProgress()
|
||||
fmt.Println()
|
||||
return newConfig
|
||||
}
|
||||
|
||||
func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error {
|
||||
display.ShowProgress("Saving configuration...")
|
||||
if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil {
|
||||
log.Error(err)
|
||||
waitExit()
|
||||
return err
|
||||
}
|
||||
display.StopProgress()
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
func showCompletionMessages(display *ui.Display) {
|
||||
display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage)
|
||||
fmt.Println()
|
||||
|
||||
message := "Operation completed!"
|
||||
if lang.GetCurrentLanguage() == lang.CN {
|
||||
message = "操作完成!"
|
||||
}
|
||||
display.ShowInfo(message)
|
||||
}
|
||||
|
||||
func waitExit() {
|
||||
fmt.Print(lang.GetText().PressEnterToExit)
|
||||
os.Stdout.Sync()
|
||||
bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
}
|
||||
|
||||
func checkAdminPrivileges() (bool, error) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd := exec.Command("net", "session")
|
||||
return cmd.Run() == nil, nil
|
||||
|
||||
case "darwin", "linux":
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
return currentUser.Uid == "0", nil
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func selfElevate() error {
|
||||
os.Setenv("AUTOMATED_MODE", "1")
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
verb := "runas"
|
||||
exe, _ := os.Executable()
|
||||
cwd, _ := os.Getwd()
|
||||
args := strings.Join(os.Args[1:], " ")
|
||||
|
||||
cmd := exec.Command("cmd", "/C", "start", verb, exe, args)
|
||||
cmd.Dir = cwd
|
||||
return cmd.Run()
|
||||
|
||||
case "darwin", "linux":
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
8
go.mod
8
go.mod
@ -1,11 +1,15 @@
|
||||
module cursor-id-modifier
|
||||
module github.com/dacrab/go-cursor-help
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/fatih/color v1.15.0
|
||||
require (
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
)
|
||||
|
16
go.sum
16
go.sum
@ -1,3 +1,6 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@ -5,6 +8,19 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
153
internal/config/config.go
Normal file
153
internal/config/config.go
Normal file
@ -0,0 +1,153 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StorageConfig represents the storage configuration
|
||||
type StorageConfig struct {
|
||||
TelemetryMacMachineId string `json:"telemetry.macMachineId"`
|
||||
TelemetryMachineId string `json:"telemetry.machineId"`
|
||||
TelemetryDevDeviceId string `json:"telemetry.devDeviceId"`
|
||||
TelemetrySqmId string `json:"telemetry.sqmId"`
|
||||
LastModified string `json:"lastModified"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Manager handles configuration operations
|
||||
type Manager struct {
|
||||
configPath string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewManager creates a new configuration manager
|
||||
func NewManager(username string) (*Manager, error) {
|
||||
configPath, err := getConfigPath(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config path: %w", err)
|
||||
}
|
||||
return &Manager{configPath: configPath}, nil
|
||||
}
|
||||
|
||||
// ReadConfig reads the existing configuration
|
||||
func (m *Manager) ReadConfig() (*StorageConfig, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
data, err := os.ReadFile(m.configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var config StorageConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves the configuration
|
||||
func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Ensure parent directories exist
|
||||
if err := os.MkdirAll(filepath.Dir(m.configPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// Prepare updated configuration
|
||||
updatedConfig := m.prepareUpdatedConfig(config)
|
||||
|
||||
// Write configuration
|
||||
if err := m.writeConfigFile(updatedConfig, readOnly); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareUpdatedConfig merges existing config with updates
|
||||
func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} {
|
||||
// Read existing config
|
||||
originalFile := make(map[string]interface{})
|
||||
if data, err := os.ReadFile(m.configPath); err == nil {
|
||||
json.Unmarshal(data, &originalFile)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
originalFile["telemetry.sqmId"] = config.TelemetrySqmId
|
||||
originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId
|
||||
originalFile["telemetry.machineId"] = config.TelemetryMachineId
|
||||
originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId
|
||||
originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339)
|
||||
originalFile["version"] = "1.0.1"
|
||||
|
||||
return originalFile
|
||||
}
|
||||
|
||||
// writeConfigFile handles the atomic write of the config file
|
||||
func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error {
|
||||
// Marshal with indentation
|
||||
content, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
// Write to temporary file
|
||||
tmpPath := m.configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, content, 0666); err != nil {
|
||||
return fmt.Errorf("failed to write temporary file: %w", err)
|
||||
}
|
||||
|
||||
// Set final permissions
|
||||
fileMode := os.FileMode(0666)
|
||||
if readOnly {
|
||||
fileMode = 0444
|
||||
}
|
||||
|
||||
if err := os.Chmod(tmpPath, fileMode); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to set temporary file permissions: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
if err := os.Rename(tmpPath, m.configPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to rename file: %w", err)
|
||||
}
|
||||
|
||||
// Sync directory
|
||||
if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil {
|
||||
defer dir.Close()
|
||||
dir.Sync()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getConfigPath returns the path to the configuration file
|
||||
func getConfigPath(username string) (string, error) {
|
||||
var configDir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage")
|
||||
case "darwin":
|
||||
configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage")
|
||||
case "linux":
|
||||
configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
return filepath.Join(configDir, "storage.json"), nil
|
||||
}
|
187
internal/lang/lang.go
Normal file
187
internal/lang/lang.go
Normal file
@ -0,0 +1,187 @@
|
||||
package lang
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Language represents a supported language code
|
||||
type Language string
|
||||
|
||||
const (
|
||||
// CN represents Chinese language
|
||||
CN Language = "cn"
|
||||
// EN represents English language
|
||||
EN Language = "en"
|
||||
)
|
||||
|
||||
// TextResource contains all translatable text resources
|
||||
type TextResource struct {
|
||||
// Success messages
|
||||
SuccessMessage string
|
||||
RestartMessage string
|
||||
|
||||
// Progress messages
|
||||
ReadingConfig string
|
||||
GeneratingIds string
|
||||
CheckingProcesses string
|
||||
ClosingProcesses string
|
||||
ProcessesClosed string
|
||||
PleaseWait string
|
||||
|
||||
// Error messages
|
||||
ErrorPrefix string
|
||||
PrivilegeError string
|
||||
|
||||
// Instructions
|
||||
RunAsAdmin string
|
||||
RunWithSudo string
|
||||
SudoExample string
|
||||
PressEnterToExit string
|
||||
SetReadOnlyMessage string
|
||||
|
||||
// Info messages
|
||||
ConfigLocation string
|
||||
}
|
||||
|
||||
var (
|
||||
currentLanguage Language
|
||||
currentLanguageOnce sync.Once
|
||||
languageMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// GetCurrentLanguage returns the current language, detecting it if not already set
|
||||
func GetCurrentLanguage() Language {
|
||||
currentLanguageOnce.Do(func() {
|
||||
currentLanguage = detectLanguage()
|
||||
})
|
||||
|
||||
languageMutex.RLock()
|
||||
defer languageMutex.RUnlock()
|
||||
return currentLanguage
|
||||
}
|
||||
|
||||
// SetLanguage sets the current language
|
||||
func SetLanguage(lang Language) {
|
||||
languageMutex.Lock()
|
||||
defer languageMutex.Unlock()
|
||||
currentLanguage = lang
|
||||
}
|
||||
|
||||
// GetText returns the TextResource for the current language
|
||||
func GetText() TextResource {
|
||||
return texts[GetCurrentLanguage()]
|
||||
}
|
||||
|
||||
// detectLanguage detects the system language
|
||||
func detectLanguage() Language {
|
||||
// Check environment variables first
|
||||
if isChineseEnvVar() {
|
||||
return CN
|
||||
}
|
||||
|
||||
// Then check OS-specific locale
|
||||
if isWindows() {
|
||||
if isWindowsChineseLocale() {
|
||||
return CN
|
||||
}
|
||||
} else if isUnixChineseLocale() {
|
||||
return CN
|
||||
}
|
||||
|
||||
return EN
|
||||
}
|
||||
|
||||
func isChineseEnvVar() bool {
|
||||
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} {
|
||||
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWindows() bool {
|
||||
return os.Getenv("OS") == "Windows_NT"
|
||||
}
|
||||
|
||||
func isWindowsChineseLocale() bool {
|
||||
// Check Windows UI culture
|
||||
cmd := exec.Command("powershell", "-Command",
|
||||
"[System.Globalization.CultureInfo]::CurrentUICulture.Name")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && strings.HasPrefix(strings.ToLower(strings.TrimSpace(string(output))), "zh") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check Windows locale
|
||||
cmd = exec.Command("wmic", "os", "get", "locale")
|
||||
output, err = cmd.Output()
|
||||
return err == nil && strings.Contains(string(output), "2052")
|
||||
}
|
||||
|
||||
func isUnixChineseLocale() bool {
|
||||
cmd := exec.Command("locale")
|
||||
output, err := cmd.Output()
|
||||
return err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn")
|
||||
}
|
||||
|
||||
// texts contains all translations
|
||||
var texts = map[Language]TextResource{
|
||||
CN: {
|
||||
// Success messages
|
||||
SuccessMessage: "[√] 配置文件已成功更新!",
|
||||
RestartMessage: "[!] 请手动重启 Cursor 以使更新生效",
|
||||
|
||||
// Progress messages
|
||||
ReadingConfig: "正在读取配置文件...",
|
||||
GeneratingIds: "正在生成新的标识符...",
|
||||
CheckingProcesses: "正在检查运行中的 Cursor 实例...",
|
||||
ClosingProcesses: "正在关闭 Cursor 实例...",
|
||||
ProcessesClosed: "所有 Cursor 实例已关闭",
|
||||
PleaseWait: "请稍候...",
|
||||
|
||||
// Error messages
|
||||
ErrorPrefix: "程序发生严重错误: %v",
|
||||
PrivilegeError: "\n[!] 错误:需要管理员权限",
|
||||
|
||||
// Instructions
|
||||
RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」",
|
||||
RunWithSudo: "请使用 sudo 命令运行此程序",
|
||||
SudoExample: "示例: sudo %s",
|
||||
PressEnterToExit: "\n按回车键退出程序...",
|
||||
SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题",
|
||||
|
||||
// Info messages
|
||||
ConfigLocation: "配置文件位置:",
|
||||
},
|
||||
EN: {
|
||||
// Success messages
|
||||
SuccessMessage: "[√] Configuration file updated successfully!",
|
||||
RestartMessage: "[!] Please restart Cursor manually for changes to take effect",
|
||||
|
||||
// Progress messages
|
||||
ReadingConfig: "Reading configuration file...",
|
||||
GeneratingIds: "Generating new identifiers...",
|
||||
CheckingProcesses: "Checking for running Cursor instances...",
|
||||
ClosingProcesses: "Closing Cursor instances...",
|
||||
ProcessesClosed: "All Cursor instances have been closed",
|
||||
PleaseWait: "Please wait...",
|
||||
|
||||
// Error messages
|
||||
ErrorPrefix: "Program encountered a serious error: %v",
|
||||
PrivilegeError: "\n[!] Error: Administrator privileges required",
|
||||
|
||||
// Instructions
|
||||
RunAsAdmin: "Please right-click and select 'Run as Administrator'",
|
||||
RunWithSudo: "Please run this program with sudo",
|
||||
SudoExample: "Example: sudo %s",
|
||||
PressEnterToExit: "\nPress Enter to exit...",
|
||||
SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records",
|
||||
|
||||
// Info messages
|
||||
ConfigLocation: "Config file location:",
|
||||
},
|
||||
}
|
216
internal/process/manager.go
Normal file
216
internal/process/manager.go
Normal file
@ -0,0 +1,216 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config holds process manager configuration
|
||||
type Config struct {
|
||||
MaxAttempts int // Maximum number of attempts to kill processes
|
||||
RetryDelay time.Duration // Delay between retry attempts
|
||||
ProcessPatterns []string // Process names to look for
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
MaxAttempts: 3,
|
||||
RetryDelay: 2 * time.Second,
|
||||
ProcessPatterns: []string{
|
||||
"Cursor.exe", // Windows executable
|
||||
"Cursor ", // Linux/macOS executable with space
|
||||
"cursor ", // Linux/macOS executable lowercase with space
|
||||
"cursor", // Linux/macOS executable lowercase
|
||||
"Cursor", // Linux/macOS executable
|
||||
"*cursor*", // Any process containing cursor
|
||||
"*Cursor*", // Any process containing Cursor
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Manager handles process-related operations
|
||||
type Manager struct {
|
||||
config *Config
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// NewManager creates a new process manager with optional config and logger
|
||||
func NewManager(config *Config, log *logrus.Logger) *Manager {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
if log == nil {
|
||||
log = logrus.New()
|
||||
}
|
||||
return &Manager{
|
||||
config: config,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// IsCursorRunning checks if any Cursor process is currently running
|
||||
func (m *Manager) IsCursorRunning() bool {
|
||||
processes, err := m.getCursorProcesses()
|
||||
if err != nil {
|
||||
m.log.Warn("Failed to get Cursor processes:", err)
|
||||
return false
|
||||
}
|
||||
return len(processes) > 0
|
||||
}
|
||||
|
||||
// KillCursorProcesses attempts to kill all running Cursor processes
|
||||
func (m *Manager) KillCursorProcesses() error {
|
||||
for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ {
|
||||
processes, err := m.getCursorProcesses()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get processes: %w", err)
|
||||
}
|
||||
|
||||
if len(processes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try graceful shutdown first on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
for _, pid := range processes {
|
||||
exec.Command("taskkill", "/PID", pid).Run()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Force kill remaining processes
|
||||
remainingProcesses, _ := m.getCursorProcesses()
|
||||
for _, pid := range remainingProcesses {
|
||||
m.killProcess(pid)
|
||||
}
|
||||
|
||||
time.Sleep(m.config.RetryDelay)
|
||||
|
||||
if processes, _ := m.getCursorProcesses(); len(processes) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCursorProcesses returns PIDs of running Cursor processes
|
||||
func (m *Manager) getCursorProcesses() ([]string, error) {
|
||||
cmd := m.getProcessListCommand()
|
||||
if cmd == nil {
|
||||
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute command: %w", err)
|
||||
}
|
||||
|
||||
return m.parseProcessList(string(output)), nil
|
||||
}
|
||||
|
||||
// getProcessListCommand returns the appropriate command to list processes based on OS
|
||||
func (m *Manager) getProcessListCommand() *exec.Cmd {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return exec.Command("tasklist", "/FO", "CSV", "/NH")
|
||||
case "darwin":
|
||||
return exec.Command("ps", "-ax")
|
||||
case "linux":
|
||||
return exec.Command("ps", "-A")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseProcessList extracts Cursor process PIDs from process list output
|
||||
func (m *Manager) parseProcessList(output string) []string {
|
||||
var processes []string
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
lowerLine := strings.ToLower(line)
|
||||
|
||||
if m.isOwnProcess(lowerLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
if pid := m.findCursorProcess(line, lowerLine); pid != "" {
|
||||
processes = append(processes, pid)
|
||||
}
|
||||
}
|
||||
return processes
|
||||
}
|
||||
|
||||
// isOwnProcess checks if the process belongs to this application
|
||||
func (m *Manager) isOwnProcess(line string) bool {
|
||||
return strings.Contains(line, "cursor-id-modifier") ||
|
||||
strings.Contains(line, "cursor-helper")
|
||||
}
|
||||
|
||||
// findCursorProcess checks if a process line matches Cursor patterns and returns its PID
|
||||
func (m *Manager) findCursorProcess(line, lowerLine string) string {
|
||||
for _, pattern := range m.config.ProcessPatterns {
|
||||
if m.matchPattern(lowerLine, strings.ToLower(pattern)) {
|
||||
return m.extractPID(line)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// matchPattern checks if a line matches a pattern, supporting wildcards
|
||||
func (m *Manager) matchPattern(line, pattern string) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"):
|
||||
search := pattern[1 : len(pattern)-1]
|
||||
return strings.Contains(line, search)
|
||||
case strings.HasPrefix(pattern, "*"):
|
||||
return strings.HasSuffix(line, pattern[1:])
|
||||
case strings.HasSuffix(pattern, "*"):
|
||||
return strings.HasPrefix(line, pattern[:len(pattern)-1])
|
||||
default:
|
||||
return line == pattern
|
||||
}
|
||||
}
|
||||
|
||||
// extractPID extracts process ID from a process list line based on OS format
|
||||
func (m *Manager) extractPID(line string) string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) >= 2 {
|
||||
return strings.Trim(parts[1], "\"")
|
||||
}
|
||||
case "darwin", "linux":
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 1 {
|
||||
return parts[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// killProcess forcefully terminates a process by PID
|
||||
func (m *Manager) killProcess(pid string) error {
|
||||
cmd := m.getKillCommand(pid)
|
||||
if cmd == nil {
|
||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// getKillCommand returns the appropriate command to kill a process based on OS
|
||||
func (m *Manager) getKillCommand(pid string) *exec.Cmd {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return exec.Command("taskkill", "/F", "/PID", pid)
|
||||
case "darwin", "linux":
|
||||
return exec.Command("kill", "-9", pid)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
94
internal/ui/display.go
Normal file
94
internal/ui/display.go
Normal file
@ -0,0 +1,94 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Display handles UI operations for terminal output
|
||||
type Display struct {
|
||||
spinner *Spinner
|
||||
}
|
||||
|
||||
// NewDisplay creates a new display instance with an optional spinner
|
||||
func NewDisplay(spinner *Spinner) *Display {
|
||||
if spinner == nil {
|
||||
spinner = NewSpinner(nil)
|
||||
}
|
||||
return &Display{spinner: spinner}
|
||||
}
|
||||
|
||||
// Terminal Operations
|
||||
|
||||
// ClearScreen clears the terminal screen based on OS
|
||||
func (d *Display) ClearScreen() error {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "cls")
|
||||
default:
|
||||
cmd = exec.Command("clear")
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Progress Indicator
|
||||
|
||||
// ShowProgress displays a progress message with a spinner
|
||||
func (d *Display) ShowProgress(message string) {
|
||||
d.spinner.SetMessage(message)
|
||||
d.spinner.Start()
|
||||
}
|
||||
|
||||
// StopProgress stops the progress spinner
|
||||
func (d *Display) StopProgress() {
|
||||
d.spinner.Stop()
|
||||
}
|
||||
|
||||
// Message Display
|
||||
|
||||
// ShowSuccess displays success messages in green
|
||||
func (d *Display) ShowSuccess(messages ...string) {
|
||||
green := color.New(color.FgGreen)
|
||||
for _, msg := range messages {
|
||||
green.Println(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowInfo displays an info message in cyan
|
||||
func (d *Display) ShowInfo(message string) {
|
||||
cyan := color.New(color.FgCyan)
|
||||
cyan.Println(message)
|
||||
}
|
||||
|
||||
// ShowError displays an error message in red
|
||||
func (d *Display) ShowError(message string) {
|
||||
red := color.New(color.FgRed)
|
||||
red.Println(message)
|
||||
}
|
||||
|
||||
// ShowPrivilegeError displays privilege error messages with instructions
|
||||
func (d *Display) ShowPrivilegeError(messages ...string) {
|
||||
red := color.New(color.FgRed, color.Bold)
|
||||
yellow := color.New(color.FgYellow)
|
||||
|
||||
// Main error message
|
||||
red.Println(messages[0])
|
||||
fmt.Println()
|
||||
|
||||
// Additional instructions
|
||||
for _, msg := range messages[1:] {
|
||||
if strings.Contains(msg, "%s") {
|
||||
exe, _ := os.Executable()
|
||||
yellow.Printf(msg+"\n", exe)
|
||||
} else {
|
||||
yellow.Println(msg)
|
||||
}
|
||||
}
|
||||
}
|
20
internal/ui/logo.go
Normal file
20
internal/ui/logo.go
Normal file
@ -0,0 +1,20 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const cyberpunkLogo = `
|
||||
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗
|
||||
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗
|
||||
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝
|
||||
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗
|
||||
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║
|
||||
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝
|
||||
`
|
||||
|
||||
// ShowLogo displays the application logo
|
||||
func (d *Display) ShowLogo() {
|
||||
cyan := color.New(color.FgCyan, color.Bold)
|
||||
cyan.Println(cyberpunkLogo)
|
||||
}
|
122
internal/ui/spinner.go
Normal file
122
internal/ui/spinner.go
Normal file
@ -0,0 +1,122 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// SpinnerConfig defines spinner configuration
|
||||
type SpinnerConfig struct {
|
||||
Frames []string // Animation frames for the spinner
|
||||
Delay time.Duration // Delay between frame updates
|
||||
}
|
||||
|
||||
// DefaultSpinnerConfig returns the default spinner configuration
|
||||
func DefaultSpinnerConfig() *SpinnerConfig {
|
||||
return &SpinnerConfig{
|
||||
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
||||
Delay: 100 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// Spinner represents a progress spinner
|
||||
type Spinner struct {
|
||||
config *SpinnerConfig
|
||||
message string
|
||||
current int
|
||||
active bool
|
||||
stopCh chan struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewSpinner creates a new spinner with the given configuration
|
||||
func NewSpinner(config *SpinnerConfig) *Spinner {
|
||||
if config == nil {
|
||||
config = DefaultSpinnerConfig()
|
||||
}
|
||||
return &Spinner{
|
||||
config: config,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// State management
|
||||
|
||||
// SetMessage sets the spinner message
|
||||
func (s *Spinner) SetMessage(message string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.message = message
|
||||
}
|
||||
|
||||
// IsActive returns whether the spinner is currently active
|
||||
func (s *Spinner) IsActive() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.active
|
||||
}
|
||||
|
||||
// Control methods
|
||||
|
||||
// Start begins the spinner animation
|
||||
func (s *Spinner) Start() {
|
||||
s.mu.Lock()
|
||||
if s.active {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.active = true
|
||||
s.mu.Unlock()
|
||||
|
||||
go s.run()
|
||||
}
|
||||
|
||||
// Stop halts the spinner animation
|
||||
func (s *Spinner) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.active {
|
||||
return
|
||||
}
|
||||
|
||||
s.active = false
|
||||
close(s.stopCh)
|
||||
s.stopCh = make(chan struct{})
|
||||
fmt.Print("\r") // Clear the spinner line
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
func (s *Spinner) run() {
|
||||
ticker := time.NewTicker(s.config.Delay)
|
||||
defer ticker.Stop()
|
||||
|
||||
cyan := color.New(color.FgCyan, color.Bold)
|
||||
message := s.message
|
||||
|
||||
// Print initial state
|
||||
fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.mu.RLock()
|
||||
if !s.active {
|
||||
s.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
frame := s.config.Frames[s.current%len(s.config.Frames)]
|
||||
s.current++
|
||||
s.mu.RUnlock()
|
||||
|
||||
fmt.Printf("\r %s", cyan.Sprint(frame))
|
||||
fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message
|
||||
}
|
||||
}
|
||||
}
|
59
pkg/idgen/generator.go
Normal file
59
pkg/idgen/generator.go
Normal file
@ -0,0 +1,59 @@
|
||||
package idgen
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Generator handles secure ID generation for machines and devices
|
||||
type Generator struct{}
|
||||
|
||||
// NewGenerator creates a new ID generator
|
||||
func NewGenerator() *Generator {
|
||||
return &Generator{}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
// -------------
|
||||
|
||||
// simulateWork adds a small delay to make progress visible
|
||||
func (g *Generator) simulateWork() {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
}
|
||||
|
||||
// generateRandomHex generates a random hex string of specified length
|
||||
func (g *Generator) generateRandomHex(length int) (string, error) {
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// Public methods
|
||||
// -------------
|
||||
|
||||
// GenerateMachineID generates a new 32-byte machine ID
|
||||
func (g *Generator) GenerateMachineID() (string, error) {
|
||||
g.simulateWork()
|
||||
return g.generateRandomHex(32)
|
||||
}
|
||||
|
||||
// GenerateMacMachineID generates a new 64-byte MAC machine ID
|
||||
func (g *Generator) GenerateMacMachineID() (string, error) {
|
||||
g.simulateWork()
|
||||
return g.generateRandomHex(64)
|
||||
}
|
||||
|
||||
// GenerateDeviceID generates a new device ID in UUID format
|
||||
func (g *Generator) GenerateDeviceID() (string, error) {
|
||||
g.simulateWork()
|
||||
id, err := g.generateRandomHex(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s",
|
||||
id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil
|
||||
}
|
@ -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 "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 "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 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%)
|
||||
:: 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
|
||||
|
||||
echo.
|
||||
echo %GREEN%!%LANG%_MESSAGES[9]! %duration% !%LANG%_MESSAGES[10]!%RESET%
|
||||
if exist "..\bin" dir /b "..\bin"
|
||||
|
||||
pause
|
||||
endlocal
|
||||
echo %GREEN%!EN_MESSAGES[7]!%RESET%
|
108
scripts/build_all.sh
Normal file → Executable file
108
scripts/build_all.sh
Normal file → Executable file
@ -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
|
||||
if [ -d "../bin" ]; then
|
||||
rm -rf ../bin
|
||||
echo -e "${GREEN}$(get_message 1)${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 创建输出目录 / Create output directory
|
||||
create_output_dir() {
|
||||
echo "$(get_message 2)"
|
||||
mkdir -p ../bin || handle_error "$(get_message 3)"
|
||||
}
|
||||
|
||||
# 构建函数 / Build function
|
||||
# Build function with optimizations
|
||||
build() {
|
||||
local os=$1
|
||||
local arch=$2
|
||||
local suffix=$3
|
||||
local ext=""
|
||||
[ "$os" = "windows" ] && ext=".exe"
|
||||
|
||||
echo -e "\n$(get_message 4) $os ($arch)..."
|
||||
echo -e "${GREEN}$(get_message 4) $os/$arch${NC}"
|
||||
|
||||
output_name="../bin/cursor_id_modifier_v${VERSION}_${os}_${arch}${suffix}"
|
||||
|
||||
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}"
|
||||
# Parallel build execution
|
||||
build_all() {
|
||||
local builds=0
|
||||
local max_parallel=$PARALLEL_JOBS
|
||||
|
||||
# 清理旧文件 / Clean old files
|
||||
cleanup
|
||||
|
||||
# 创建输出目录 / Create output directory
|
||||
create_output_dir
|
||||
|
||||
# 定义构建目标 / 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
|
||||
|
@ -1,308 +1,193 @@
|
||||
# Auto-elevate to admin rights if not already running as admin
|
||||
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
Write-Host "Requesting administrator privileges..."
|
||||
$arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -ExecutionFromElevated"
|
||||
Start-Process powershell.exe -ArgumentList $arguments -Verb RunAs
|
||||
Exit
|
||||
# Check for admin rights and handle elevation
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
|
||||
if (-NOT $isAdmin) {
|
||||
# Detect PowerShell version and path
|
||||
$pwshPath = if (Get-Command "pwsh" -ErrorAction SilentlyContinue) {
|
||||
(Get-Command "pwsh").Source # PowerShell 7+
|
||||
} elseif (Test-Path "$env:ProgramFiles\PowerShell\7\pwsh.exe") {
|
||||
"$env:ProgramFiles\PowerShell\7\pwsh.exe"
|
||||
} else {
|
||||
"powershell.exe" # Windows PowerShell
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "`nRequesting administrator privileges..." -ForegroundColor Cyan
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
$argList = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
|
||||
Start-Process -FilePath $pwshPath -Verb RunAs -ArgumentList $argList -Wait
|
||||
exit
|
||||
}
|
||||
catch {
|
||||
Write-Host "`nError: Administrator privileges required" -ForegroundColor Red
|
||||
Write-Host "Please run this script from an Administrator PowerShell window" -ForegroundColor Yellow
|
||||
Write-Host "`nTo do this:" -ForegroundColor Cyan
|
||||
Write-Host "1. Press Win + X" -ForegroundColor White
|
||||
Write-Host "2. Click 'Windows Terminal (Admin)' or 'PowerShell (Admin)'" -ForegroundColor White
|
||||
Write-Host "3. Run the installation command again" -ForegroundColor White
|
||||
Write-Host "`nPress enter to exit..."
|
||||
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Set TLS to 1.2 / 设置 TLS 为 1.2
|
||||
# Set TLS to 1.2
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Colors for output / 输出颜色
|
||||
$Red = "`e[31m"
|
||||
$Green = "`e[32m"
|
||||
$Blue = "`e[36m"
|
||||
$Yellow = "`e[33m"
|
||||
$Reset = "`e[0m"
|
||||
# Create temporary directory
|
||||
$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())
|
||||
New-Item -ItemType Directory -Path $TmpDir | Out-Null
|
||||
|
||||
# Messages / 消息
|
||||
$EN_MESSAGES = @(
|
||||
"Starting installation...",
|
||||
"Detected architecture:",
|
||||
"Only 64-bit Windows is supported",
|
||||
"Latest version:",
|
||||
"Creating installation directory...",
|
||||
"Downloading latest release from:",
|
||||
"Failed to download binary:",
|
||||
"Downloaded file not found",
|
||||
"Installing binary...",
|
||||
"Failed to install binary:",
|
||||
"Adding to PATH...",
|
||||
"Cleaning up...",
|
||||
"Installation completed successfully!",
|
||||
"You can now use 'cursor-id-modifier' directly",
|
||||
"Checking for running Cursor instances...",
|
||||
"Found running Cursor processes. Attempting to close them...",
|
||||
"Successfully closed all Cursor instances",
|
||||
"Failed to close Cursor instances. Please close them manually",
|
||||
"Backing up storage.json...",
|
||||
"Backup created at:"
|
||||
)
|
||||
|
||||
$CN_MESSAGES = @(
|
||||
"开始安装...",
|
||||
"检测到架构:",
|
||||
"仅支持64位Windows系统",
|
||||
"最新版本:",
|
||||
"正在创建安装目录...",
|
||||
"正在从以下地址下载最新版本:",
|
||||
"下载二进制文件失败:",
|
||||
"未找到下载的文件",
|
||||
"正在安装程序...",
|
||||
"安装二进制文件失败:",
|
||||
"正在添加到PATH...",
|
||||
"正在清理...",
|
||||
"安装成功完成!",
|
||||
"现在可以直接使用 'cursor-id-modifier' 了",
|
||||
"正在检查运行中的Cursor进程...",
|
||||
"发现正在运行的Cursor进程,尝试关闭...",
|
||||
"成功关闭所有Cursor实例",
|
||||
"无法关闭Cursor实例,请手动关闭",
|
||||
"正在备份storage.json...",
|
||||
"备份已创建于:"
|
||||
)
|
||||
|
||||
# Detect system language / 检测系统语言
|
||||
function Get-SystemLanguage {
|
||||
if ((Get-Culture).Name -like "zh-CN") {
|
||||
return "cn"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
# Close Cursor instances / 关闭Cursor实例
|
||||
function Close-CursorInstances {
|
||||
Write-Status (Get-Message 14)
|
||||
$cursorProcesses = Get-Process "Cursor" -ErrorAction SilentlyContinue
|
||||
|
||||
if ($cursorProcesses) {
|
||||
Write-Status (Get-Message 15)
|
||||
try {
|
||||
$cursorProcesses | ForEach-Object { $_.CloseMainWindow() | Out-Null }
|
||||
Start-Sleep -Seconds 2
|
||||
$cursorProcesses | Where-Object { !$_.HasExited } | Stop-Process -Force
|
||||
Write-Success (Get-Message 16)
|
||||
} catch {
|
||||
Write-Error (Get-Message 17)
|
||||
}
|
||||
# Cleanup function
|
||||
function Cleanup {
|
||||
if (Test-Path $TmpDir) {
|
||||
Remove-Item -Recurse -Force $TmpDir
|
||||
}
|
||||
}
|
||||
|
||||
# Backup storage.json / 备份storage.json
|
||||
function Backup-StorageJson {
|
||||
Write-Status (Get-Message 18)
|
||||
$storageJsonPath = "$env:APPDATA\Cursor\User\globalStorage\storage.json"
|
||||
if (Test-Path $storageJsonPath) {
|
||||
$backupPath = "$storageJsonPath.backup"
|
||||
Copy-Item -Path $storageJsonPath -Destination $backupPath -Force
|
||||
Write-Success "$(Get-Message 19) $backupPath"
|
||||
# Error handler
|
||||
trap {
|
||||
Write-Host "Error: $_" -ForegroundColor Red
|
||||
Cleanup
|
||||
Write-Host "Press enter to exit..."
|
||||
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Detect system architecture
|
||||
function Get-SystemArch {
|
||||
if ([Environment]::Is64BitOperatingSystem) {
|
||||
return "x64"
|
||||
} else {
|
||||
return "x86"
|
||||
}
|
||||
}
|
||||
|
||||
# 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"
|
||||
)
|
||||
$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 }
|
||||
}
|
||||
}
|
||||
|
||||
# 添加安装前检查函数
|
||||
function Test-Prerequisites {
|
||||
Write-Log "Checking prerequisites..." "INFO"
|
||||
|
||||
# 检查PowerShell版本
|
||||
if ($PSVersionTable.PSVersion.Major -lt 5) {
|
||||
Write-Log "PowerShell 5.0 or higher is required" "ERROR"
|
||||
return $false
|
||||
}
|
||||
|
||||
# 检查网络连接
|
||||
try {
|
||||
$testConnection = Test-Connection -ComputerName "github.com" -Count 1 -Quiet
|
||||
if (-not $testConnection) {
|
||||
Write-Log "No internet connection available" "ERROR"
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Failed to check internet connection: $_" "ERROR"
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
# 添加文件验证函数
|
||||
function Test-FileHash {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string]$ExpectedHash
|
||||
)
|
||||
|
||||
$actualHash = Get-FileHash -Path $FilePath -Algorithm SHA256
|
||||
return $actualHash.Hash -eq $ExpectedHash
|
||||
}
|
||||
|
||||
# 修改下载函数,添加进度条
|
||||
function Download-File {
|
||||
param(
|
||||
# Download with progress
|
||||
function Get-FileWithProgress {
|
||||
param (
|
||||
[string]$Url,
|
||||
[string]$OutFile
|
||||
[string]$OutputFile
|
||||
)
|
||||
|
||||
try {
|
||||
$webClient = New-Object System.Net.WebClient
|
||||
$webClient.Headers.Add("User-Agent", "PowerShell Script")
|
||||
|
||||
$webClient.DownloadFileAsync($Url, $OutFile)
|
||||
|
||||
while ($webClient.IsBusy) {
|
||||
Write-Progress -Activity "Downloading..." -Status "Progress:" -PercentComplete -1
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
|
||||
Write-Progress -Activity "Downloading..." -Completed
|
||||
$webClient.DownloadFile($Url, $OutputFile)
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
Write-Log "Download failed: $_" "ERROR"
|
||||
Write-Host "Failed to download: $_" -ForegroundColor Red
|
||||
return $false
|
||||
}
|
||||
finally {
|
||||
if ($webClient) {
|
||||
$webClient.Dispose()
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
function Install-CursorModifier {
|
||||
Write-Host "Starting installation..." -ForegroundColor Cyan
|
||||
|
||||
# Detect architecture
|
||||
$arch = Get-SystemArch
|
||||
Write-Host "Detected architecture: $arch" -ForegroundColor Green
|
||||
|
||||
# Set installation directory
|
||||
$InstallDir = "$env:ProgramFiles\CursorModifier"
|
||||
if (!(Test-Path $InstallDir)) {
|
||||
New-Item -ItemType Directory -Path $InstallDir | Out-Null
|
||||
}
|
||||
|
||||
# Get latest release
|
||||
try {
|
||||
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/dacrab/go-cursor-help/releases/latest"
|
||||
Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan
|
||||
|
||||
# Look for Windows binary with our architecture
|
||||
$possibleNames = @(
|
||||
"cursor-id-modifier_windows_$($arch).exe",
|
||||
"cursor-id-modifier_windows_$($arch).exe",
|
||||
"cursor-id-modifier_Windows_$($arch).exe",
|
||||
"cursor-id-modifier_Windows_$($arch).exe"
|
||||
)
|
||||
|
||||
$asset = $null
|
||||
foreach ($name in $possibleNames) {
|
||||
Write-Host "Checking for asset: $name" -ForegroundColor Cyan
|
||||
$asset = $latestRelease.assets | Where-Object { $_.name -eq $name }
|
||||
if ($asset) {
|
||||
Write-Host "Found matching asset: $($asset.name)" -ForegroundColor Green
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!$asset) {
|
||||
Write-Host "`nAvailable assets:" -ForegroundColor Yellow
|
||||
$latestRelease.assets | ForEach-Object { Write-Host "- $($_.name)" }
|
||||
throw "Could not find appropriate Windows binary for $arch architecture"
|
||||
}
|
||||
|
||||
$downloadUrl = $asset.browser_download_url
|
||||
}
|
||||
catch {
|
||||
Write-Host "Failed to get latest release: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download binary
|
||||
Write-Host "`nDownloading latest release..." -ForegroundColor Cyan
|
||||
$binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe"
|
||||
|
||||
if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Install binary
|
||||
Write-Host "Installing..." -ForegroundColor Cyan
|
||||
try {
|
||||
Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force
|
||||
|
||||
# Add to PATH if not already present
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if ($currentPath -notlike "*$InstallDir*") {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$InstallDir", "Machine")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Main installation process / 主安装过程
|
||||
Write-Status (Get-Message 0)
|
||||
|
||||
# Close any running Cursor instances
|
||||
Close-CursorInstances
|
||||
|
||||
# Backup storage.json
|
||||
Backup-StorageJson
|
||||
|
||||
# Get system architecture / 获取系统架构
|
||||
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
|
||||
Write-Status "$(Get-Message 1) $arch"
|
||||
|
||||
if ($arch -ne "amd64") {
|
||||
Write-Error (Get-Message 2)
|
||||
}
|
||||
|
||||
# Get latest version / 获取最新版本
|
||||
$version = Get-LatestVersion
|
||||
Write-Status "$(Get-Message 3) $version"
|
||||
|
||||
# Set up paths / 设置路径
|
||||
$installDir = "$env:ProgramFiles\cursor-id-modifier"
|
||||
$versionWithoutV = $version.TrimStart('v') # 移除版本号前面的 'v' 字符
|
||||
$binaryName = "cursor_id_modifier_${versionWithoutV}_windows_amd64.exe"
|
||||
$downloadUrl = "https://github.com/yuaotian/go-cursor-help/releases/download/$version/$binaryName"
|
||||
$tempFile = "$env:TEMP\$binaryName"
|
||||
|
||||
# Create installation directory / 创建安装目录
|
||||
Write-Status (Get-Message 4)
|
||||
if (-not (Test-Path $installDir)) {
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Download binary / 下载二进制文件
|
||||
Write-Status "$(Get-Message 5) $downloadUrl"
|
||||
try {
|
||||
if (-not (Download-File -Url $downloadUrl -OutFile $tempFile)) {
|
||||
Write-Error "$(Get-Message 6)"
|
||||
catch {
|
||||
Write-Host "Failed to install: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Installation completed successfully!" -ForegroundColor Green
|
||||
Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan
|
||||
|
||||
# Run the program
|
||||
try {
|
||||
& "$InstallDir\cursor-id-modifier.exe"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Error "$(Get-Message 6) $_"
|
||||
}
|
||||
|
||||
# Verify download / 验证下载
|
||||
if (-not (Test-Path $tempFile)) {
|
||||
Write-Error (Get-Message 7)
|
||||
}
|
||||
|
||||
# Install binary / 安装二进制文件
|
||||
Write-Status (Get-Message 8)
|
||||
# Run installation
|
||||
try {
|
||||
Move-Item -Force $tempFile "$installDir\cursor-id-modifier.exe"
|
||||
} catch {
|
||||
Write-Error "$(Get-Message 9) $_"
|
||||
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"
|
||||
)
|
||||
catch {
|
||||
Write-Host "Installation failed: $_" -ForegroundColor Red
|
||||
Cleanup
|
||||
Write-Host "Press enter to exit..."
|
||||
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Press enter to exit..." -ForegroundColor Green
|
||||
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
||||
}
|
||||
}
|
@ -2,294 +2,124 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output / 输出颜色
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;36m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color / 无颜色
|
||||
NC='\033[0m'
|
||||
|
||||
# Messages / 消息
|
||||
EN_MESSAGES=(
|
||||
"Starting installation..."
|
||||
"Detected OS:"
|
||||
"Downloading latest release..."
|
||||
"URL:"
|
||||
"Installing binary..."
|
||||
"Cleaning up..."
|
||||
"Installation completed successfully!"
|
||||
"You can now use 'sudo %s' from your terminal"
|
||||
"Failed to download binary from:"
|
||||
"Failed to download the binary"
|
||||
"curl is required but not installed. Please install curl first."
|
||||
"sudo is required but not installed. Please install sudo first."
|
||||
"Unsupported operating system"
|
||||
"Unsupported architecture:"
|
||||
"Checking for running Cursor instances..."
|
||||
"Found running Cursor processes. Attempting to close them..."
|
||||
"Successfully closed all Cursor instances"
|
||||
"Failed to close Cursor instances. Please close them manually"
|
||||
"Backing up storage.json..."
|
||||
"Backup created at:"
|
||||
"This script requires root privileges. Requesting sudo access..."
|
||||
)
|
||||
# Temporary directory for downloads
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
CN_MESSAGES=(
|
||||
"开始安装..."
|
||||
"检测到操作系统:"
|
||||
"正在下载最新版本..."
|
||||
"下载地址:"
|
||||
"正在安装程序..."
|
||||
"正在清理..."
|
||||
"安装成功完成!"
|
||||
"现在可以在终端中使用 'sudo %s' 了"
|
||||
"从以下地址下载二进制文件失败:"
|
||||
"下载二进制文件失败"
|
||||
"需要 curl 但未安装。请先安装 curl。"
|
||||
"需要 sudo 但未安装。请先安装 sudo。"
|
||||
"不支持的操作系统"
|
||||
"不支持的架构:"
|
||||
"正在检查运行中的Cursor进程..."
|
||||
"发现正在运行的Cursor进程,尝试关闭..."
|
||||
"成功关闭所有Cursor实例"
|
||||
"无法关闭Cursor实例,请手动关闭"
|
||||
"正在备份storage.json..."
|
||||
"备份已创建于:"
|
||||
"此脚本需要root权限。正在请求sudo访问..."
|
||||
)
|
||||
|
||||
# Detect system language / 检测系统语言
|
||||
detect_language() {
|
||||
if [[ $(locale | grep "LANG=zh_CN") ]]; then
|
||||
echo "cn"
|
||||
else
|
||||
echo "en"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get message based on language / 根据语言获取消息
|
||||
get_message() {
|
||||
local index=$1
|
||||
local lang=$(detect_language)
|
||||
|
||||
if [[ "$lang" == "cn" ]]; then
|
||||
echo "${CN_MESSAGES[$index]}"
|
||||
else
|
||||
echo "${EN_MESSAGES[$index]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print with color / 带颜色打印
|
||||
print_status() {
|
||||
echo -e "${BLUE}[*]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[✓]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[!]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[✗]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check and request root privileges / 检查并请求root权限
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_status "$(get_message 20)"
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
exec sudo bash "$0" "$@"
|
||||
else
|
||||
print_error "$(get_message 11)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Close Cursor instances / 关闭Cursor实例
|
||||
close_cursor_instances() {
|
||||
print_status "$(get_message 14)"
|
||||
|
||||
if pgrep -x "Cursor" >/dev/null; then
|
||||
print_status "$(get_message 15)"
|
||||
if pkill -x "Cursor" 2>/dev/null; then
|
||||
sleep 2
|
||||
print_success "$(get_message 16)"
|
||||
else
|
||||
print_error "$(get_message 17)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Backup storage.json / 备份storage.json
|
||||
backup_storage_json() {
|
||||
print_status "$(get_message 18)"
|
||||
local storage_path
|
||||
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
storage_path="$HOME/Library/Application Support/Cursor/User/globalStorage/storage.json"
|
||||
else
|
||||
storage_path="$HOME/.config/Cursor/User/globalStorage/storage.json"
|
||||
fi
|
||||
|
||||
if [ -f "$storage_path" ]; then
|
||||
cp "$storage_path" "${storage_path}.backup"
|
||||
print_success "$(get_message 19) ${storage_path}.backup"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect OS / 检测操作系统
|
||||
detect_os() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "darwin"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "linux"
|
||||
else
|
||||
print_error "$(get_message 12)"
|
||||
fi
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
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 $?
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
if [ ! -f "$TMP_DIR/$BINARY_NAME" ]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
print_error "$(get_message 9)"
|
||||
fi
|
||||
|
||||
print_status "$(get_message 4)"
|
||||
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"
|
||||
|
||||
print_success "$(get_message 6)"
|
||||
printf "${GREEN}[✓]${NC} $(get_message 7)\n" "$FINAL_BINARY_NAME"
|
||||
|
||||
# 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 for required commands
|
||||
check_requirements() {
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
print_error "$(get_message 10)"
|
||||
fi
|
||||
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
print_error "$(get_message 11)"
|
||||
echo -e "${RED}Error: curl is required${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation process / 主安装过程
|
||||
# Detect system information
|
||||
detect_system() {
|
||||
local os arch suffix
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) os="linux";;
|
||||
Darwin*) os="darwin";;
|
||||
*) echo -e "${RED}Unsupported OS${NC}"; exit 1;;
|
||||
esac
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64)
|
||||
arch="x64"
|
||||
[ "$os" = "darwin" ] && suffix="_intel"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
arch="arm64"
|
||||
[ "$os" = "darwin" ] && suffix="_apple_silicon"
|
||||
;;
|
||||
i386|i686)
|
||||
arch="x86"
|
||||
[ "$os" = "darwin" ] && { echo -e "${RED}32-bit not supported on macOS${NC}"; exit 1; }
|
||||
;;
|
||||
*) echo -e "${RED}Unsupported architecture${NC}"; exit 1;;
|
||||
esac
|
||||
|
||||
echo "$os $arch $suffix"
|
||||
}
|
||||
|
||||
# Download with progress
|
||||
download() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
curl -#L "$url" -o "$output"
|
||||
}
|
||||
|
||||
# Check and create installation directory
|
||||
setup_install_dir() {
|
||||
local install_dir="$1"
|
||||
|
||||
if [ ! -d "$install_dir" ]; then
|
||||
mkdir -p "$install_dir" || {
|
||||
echo -e "${RED}Failed to create installation directory${NC}"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
print_status "$(get_message 0)"
|
||||
|
||||
# Check root privileges / 检查root权限
|
||||
check_root "$@"
|
||||
|
||||
# Check required tools / 检查必需工具
|
||||
check_requirements
|
||||
|
||||
# Close Cursor instances / 关闭Cursor实例
|
||||
close_cursor_instances
|
||||
echo -e "${BLUE}Starting installation...${NC}"
|
||||
|
||||
# Backup storage.json / 备份storage.json
|
||||
backup_storage_json
|
||||
# Detect system
|
||||
read -r OS ARCH SUFFIX <<< "$(detect_system)"
|
||||
echo -e "${GREEN}Detected: $OS $ARCH${NC}"
|
||||
|
||||
OS=$(detect_os)
|
||||
print_status "$(get_message 1) $OS"
|
||||
# Set installation directory
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
|
||||
# Install the binary / 安装二进制文件
|
||||
install_binary
|
||||
# Setup installation directory
|
||||
setup_install_dir "$INSTALL_DIR"
|
||||
|
||||
# Get latest release info
|
||||
echo -e "${BLUE}Fetching latest release information...${NC}"
|
||||
LATEST_URL="https://api.github.com/repos/dacrab/go-cursor-help/releases/latest"
|
||||
|
||||
# Construct binary name
|
||||
BINARY_NAME="cursor-id-modifier_${OS}_${ARCH}${SUFFIX}"
|
||||
echo -e "${BLUE}Looking for asset: $BINARY_NAME${NC}"
|
||||
|
||||
# Get download URL directly
|
||||
DOWNLOAD_URL=$(curl -s "$LATEST_URL" | tr -d '\n' | grep -o "\"browser_download_url\": \"[^\"]*${BINARY_NAME}[^\"]*\"" | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$DOWNLOAD_URL" ]; then
|
||||
echo -e "${RED}Error: Could not find appropriate binary for $OS $ARCH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Found matching asset: $BINARY_NAME${NC}"
|
||||
echo -e "${BLUE}Downloading from: $DOWNLOAD_URL${NC}"
|
||||
|
||||
download "$DOWNLOAD_URL" "$TMP_DIR/cursor-id-modifier"
|
||||
|
||||
# Install binary
|
||||
echo -e "${BLUE}Installing...${NC}"
|
||||
chmod +x "$TMP_DIR/cursor-id-modifier"
|
||||
sudo mv "$TMP_DIR/cursor-id-modifier" "$INSTALL_DIR/"
|
||||
|
||||
echo -e "${GREEN}Installation completed successfully!${NC}"
|
||||
echo -e "${BLUE}Running cursor-id-modifier...${NC}"
|
||||
|
||||
# Run the program with sudo, preserving environment variables
|
||||
export AUTOMATED_MODE=1
|
||||
if ! sudo -E cursor-id-modifier; then
|
||||
echo -e "${RED}Failed to run cursor-id-modifier${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function / 运行主函数
|
||||
main "$@"
|
||||
main
|
||||
|
Loading…
x
Reference in New Issue
Block a user