Merge pull request #85 from dacrab/master

commit request
This commit is contained in:
煎饼果子卷鲨鱼辣椒 2024-12-30 17:27:59 +08:00 committed by GitHub
commit 778f48e57a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1963 additions and 1916 deletions

53
.github/workflows/auto-tag.yml vendored Normal file
View 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

View File

@ -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
View File

@ -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

View File

@ -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
View 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
View 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
View File

@ -2,23 +2,25 @@
<div align="center">
[![Release](https://img.shields.io/github/v/release/yuaotian/go-cursor-help?style=flat-square&logo=github&color=blue)](https://github.com/yuaotian/go-cursor-help/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=bookstack)](https://github.com/yuaotian/go-cursor-help/blob/main/LICENSE)
[![Stars](https://img.shields.io/github/stars/yuaotian/go-cursor-help?style=flat-square&logo=github)](https://github.com/yuaotian/go-cursor-help/stargazers)
[![Release](https://img.shields.io/github/v/release/dacrab/go-cursor-help?style=flat-square&logo=github&color=blue)](https://github.com/dacrab/go-cursor-help/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square&logo=bookstack)](https://github.com/dacrab/go-cursor-help/blob/master/LICENSE)
[![Stars](https://img.shields.io/github/stars/dacrab/go-cursor-help?style=flat-square&logo=github)](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工具和开发资源
![微信公众号二维码](img/wx_public_2.png)
---
## ⭐ Star History or Repobeats
## ⭐ Project Stats
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=yuaotian/go-cursor-help&type=Date)](https://star-history.com/#yuaotian/go-cursor-help&Date)
![Repobeats analytics image](https://repobeats.axiom.co/api/embed/ddaa9df9a94b0029ec3fad399e1c1c4e75755477.svg "Repobeats analytics image")
</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>

View 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
View File

@ -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
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
}
}
}

1050
main.go

File diff suppressed because it is too large Load Diff

59
pkg/idgen/generator.go Normal file
View 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
}

View File

@ -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
View 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

View File

@ -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')
}
}

View File

@ -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