├── img ├── wx_me.png ├── wx_group.png ├── wx_public.jpg └── wx_public_2.png ├── go.mod ├── .gitignore ├── internal ├── ui │ ├── logo.go │ ├── display.go │ └── spinner.go ├── config │ └── config.go ├── lang │ └── lang.go └── process │ └── manager.go ├── LICENSE ├── scripts ├── build_all.bat ├── build_all.sh ├── install.sh ├── run │ ├── cursor_win_id_modifier.ps1 │ ├── cursor_linux_id_modifier.sh │ └── cursor_mac_id_modifier.sh └── install.ps1 ├── .goreleaser.yml ├── go.sum ├── pkg └── idgen │ └── generator.go ├── cmd └── cursor-id-modifier │ └── main.go ├── .github └── workflows │ └── auto-tag-release.yml └── README.md /img/wx_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/go-cursor-help/master/img/wx_me.png -------------------------------------------------------------------------------- /img/wx_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/go-cursor-help/master/img/wx_group.png -------------------------------------------------------------------------------- /img/wx_public.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/go-cursor-help/master/img/wx_public.jpg -------------------------------------------------------------------------------- /img/wx_public_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iHunterDev/go-cursor-help/master/img/wx_public_2.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yuaotian/go-cursor-help 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/sirupsen/logrus v1.9.3 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-colorable v0.1.13 // indirect 12 | github.com/mattn/go-isatty v0.0.17 // indirect 13 | github.com/stretchr/testify v1.10.0 // indirect 14 | golang.org/x/sys v0.13.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled binary 2 | /cursor-id-modifier 3 | /cursor-id-modifier.exe 4 | 5 | # Build output directories 6 | bin/ 7 | dist/ 8 | 9 | # Go specific 10 | go.sum 11 | go/ 12 | .cache/ 13 | 14 | # IDE and editor files 15 | .vscode/ 16 | .idea/ 17 | *.swp 18 | *.swo 19 | 20 | # OS specific 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Build and release artifacts 25 | releases/ 26 | *.syso 27 | *.exe 28 | *.exe~ 29 | *.dll 30 | *.so 31 | *.dylib 32 | 33 | # Test files 34 | *.test 35 | *.out 36 | coverage.txt 37 | 38 | # Temporary files 39 | *.tmp 40 | *~ 41 | *.bak 42 | *.log -------------------------------------------------------------------------------- /internal/ui/logo.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | ) 6 | 7 | const cyberpunkLogo = ` 8 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 9 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 10 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 11 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 12 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 13 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 14 | ` 15 | 16 | // ShowLogo displays the application logo 17 | func (d *Display) ShowLogo() { 18 | cyan := color.New(color.FgCyan, color.Bold) 19 | cyan.Println(cyberpunkLogo) 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 dacrab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /scripts/build_all.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal EnableDelayedExpansion 3 | 4 | :: Build optimization flags 5 | set "OPTIMIZATION_FLAGS=-trimpath -ldflags=\"-s -w\"" 6 | set "BUILD_JOBS=4" 7 | 8 | :: Messages / 消息 9 | set "EN_MESSAGES[0]=Starting build process for version" 10 | set "EN_MESSAGES[1]=Using optimization flags:" 11 | set "EN_MESSAGES[2]=Cleaning old builds..." 12 | set "EN_MESSAGES[3]=Cleanup completed" 13 | set "EN_MESSAGES[4]=Starting builds for all platforms..." 14 | set "EN_MESSAGES[5]=Building for" 15 | set "EN_MESSAGES[6]=Build successful:" 16 | set "EN_MESSAGES[7]=All builds completed!" 17 | 18 | :: Colors 19 | set "GREEN=[32m" 20 | set "RED=[31m" 21 | set "RESET=[0m" 22 | 23 | :: Cleanup function 24 | :cleanup 25 | if exist "..\bin" ( 26 | rd /s /q "..\bin" 27 | echo %GREEN%!EN_MESSAGES[3]!%RESET% 28 | ) 29 | mkdir "..\bin" 2>nul 30 | 31 | :: Build function with optimizations 32 | :build 33 | set "os=%~1" 34 | set "arch=%~2" 35 | set "ext=" 36 | if "%os%"=="windows" set "ext=.exe" 37 | 38 | echo %GREEN%!EN_MESSAGES[5]! %os%/%arch%%RESET% 39 | 40 | set "CGO_ENABLED=0" 41 | set "GOOS=%os%" 42 | set "GOARCH=%arch%" 43 | 44 | 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" 45 | exit /b 0 46 | 47 | :: Main execution 48 | echo %GREEN%!EN_MESSAGES[0]!%RESET% 49 | echo %GREEN%!EN_MESSAGES[1]! %OPTIMIZATION_FLAGS%%RESET% 50 | 51 | call :cleanup 52 | 53 | echo %GREEN%!EN_MESSAGES[4]!%RESET% 54 | 55 | :: Start builds in parallel 56 | set "pending=0" 57 | for %%o in (windows linux darwin) do ( 58 | for %%a in (amd64 386) do ( 59 | call :build %%o %%a 60 | set /a "pending+=1" 61 | if !pending! geq %BUILD_JOBS% ( 62 | timeout /t 1 /nobreak >nul 63 | set "pending=0" 64 | ) 65 | ) 66 | ) 67 | 68 | :: Wait for all builds to complete 69 | :wait_builds 70 | timeout /t 2 /nobreak >nul 71 | tasklist /fi "IMAGENAME eq go.exe" 2>nul | find "go.exe" >nul 72 | if not errorlevel 1 goto wait_builds 73 | 74 | echo %GREEN%!EN_MESSAGES[7]!%RESET% -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go mod vendor 7 | - go mod verify 8 | 9 | builds: 10 | - id: cursor-id-modifier 11 | main: ./cmd/cursor-id-modifier/main.go 12 | binary: cursor-id-modifier 13 | env: 14 | - CGO_ENABLED=0 15 | - GO111MODULE=on 16 | goos: 17 | - linux 18 | - windows 19 | - darwin 20 | goarch: 21 | - amd64 22 | - arm64 23 | - "386" 24 | ignore: 25 | - goos: darwin 26 | goarch: "386" 27 | ldflags: 28 | - -s -w 29 | - -X 'main.version={{.Version}}' 30 | - -X 'main.commit={{.ShortCommit}}' 31 | - -X 'main.date={{.CommitDate}}' 32 | - -X 'main.builtBy=goreleaser' 33 | flags: 34 | - -trimpath 35 | mod_timestamp: '{{ .CommitTimestamp }}' 36 | 37 | archives: 38 | - id: binary 39 | format: binary 40 | name_template: >- 41 | {{- .ProjectName }}_ 42 | {{- .Version }}_ 43 | {{- .Os }}_ 44 | {{- if eq .Arch "amd64" }}x86_64 45 | {{- else if eq .Arch "386" }}i386 46 | {{- else }}{{ .Arch }}{{ end }} 47 | builds: 48 | - cursor-id-modifier 49 | allow_different_binary_count: true 50 | files: 51 | - none* 52 | 53 | checksum: 54 | name_template: 'checksums.txt' 55 | algorithm: sha256 56 | 57 | release: 58 | draft: true 59 | prerelease: auto 60 | mode: replace 61 | header: | 62 | ## Release {{.Tag}} ({{.Date}}) 63 | 64 | See [CHANGELOG.md](CHANGELOG.md) for details. 65 | footer: | 66 | **Full Changelog**: https://github.com/owner/repo/compare/{{ .PreviousTag }}...{{ .Tag }} 67 | extra_files: 68 | - glob: 'LICENSE*' 69 | - glob: 'README*' 70 | - glob: 'CHANGELOG*' 71 | 72 | changelog: 73 | sort: asc 74 | use: github 75 | filters: 76 | exclude: 77 | - '^docs:' 78 | - '^test:' 79 | - '^ci:' 80 | - Merge pull request 81 | - Merge branch 82 | groups: 83 | - title: Features 84 | regexp: "^.*feat[(\\w)]*:+.*$" 85 | order: 0 86 | - title: 'Bug fixes' 87 | regexp: "^.*fix[(\\w)]*:+.*$" 88 | order: 1 89 | - title: Others 90 | order: 999 91 | 92 | project_name: cursor-id-modifier 93 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 5 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 6 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 7 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 8 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 9 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 10 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 14 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 18 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 22 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /internal/ui/display.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | ) 12 | 13 | // Display handles UI operations for terminal output 14 | type Display struct { 15 | spinner *Spinner 16 | } 17 | 18 | // NewDisplay creates a new display instance with an optional spinner 19 | func NewDisplay(spinner *Spinner) *Display { 20 | if spinner == nil { 21 | spinner = NewSpinner(nil) 22 | } 23 | return &Display{spinner: spinner} 24 | } 25 | 26 | // Terminal Operations 27 | 28 | // ClearScreen clears the terminal screen based on OS 29 | func (d *Display) ClearScreen() error { 30 | var cmd *exec.Cmd 31 | switch runtime.GOOS { 32 | case "windows": 33 | cmd = exec.Command("cmd", "/c", "cls") 34 | default: 35 | cmd = exec.Command("clear") 36 | } 37 | cmd.Stdout = os.Stdout 38 | return cmd.Run() 39 | } 40 | 41 | // Progress Indicator 42 | 43 | // ShowProgress displays a progress message with a spinner 44 | func (d *Display) ShowProgress(message string) { 45 | d.spinner.SetMessage(message) 46 | d.spinner.Start() 47 | } 48 | 49 | // StopProgress stops the progress spinner 50 | func (d *Display) StopProgress() { 51 | d.spinner.Stop() 52 | } 53 | 54 | // Message Display 55 | 56 | // ShowSuccess displays success messages in green 57 | func (d *Display) ShowSuccess(messages ...string) { 58 | green := color.New(color.FgGreen) 59 | for _, msg := range messages { 60 | green.Println(msg) 61 | } 62 | } 63 | 64 | // ShowInfo displays an info message in cyan 65 | func (d *Display) ShowInfo(message string) { 66 | cyan := color.New(color.FgCyan) 67 | cyan.Println(message) 68 | } 69 | 70 | // ShowError displays an error message in red 71 | func (d *Display) ShowError(message string) { 72 | red := color.New(color.FgRed) 73 | red.Println(message) 74 | } 75 | 76 | // ShowPrivilegeError displays privilege error messages with instructions 77 | func (d *Display) ShowPrivilegeError(messages ...string) { 78 | red := color.New(color.FgRed, color.Bold) 79 | yellow := color.New(color.FgYellow) 80 | 81 | // Main error message 82 | red.Println(messages[0]) 83 | fmt.Println() 84 | 85 | // Additional instructions 86 | for _, msg := range messages[1:] { 87 | if strings.Contains(msg, "%s") { 88 | exe, _ := os.Executable() 89 | yellow.Printf(msg+"\n", exe) 90 | } else { 91 | yellow.Println(msg) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/ui/spinner.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // SpinnerConfig defines spinner configuration 12 | type SpinnerConfig struct { 13 | Frames []string // Animation frames for the spinner 14 | Delay time.Duration // Delay between frame updates 15 | } 16 | 17 | // DefaultSpinnerConfig returns the default spinner configuration 18 | func DefaultSpinnerConfig() *SpinnerConfig { 19 | return &SpinnerConfig{ 20 | Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, 21 | Delay: 100 * time.Millisecond, 22 | } 23 | } 24 | 25 | // Spinner represents a progress spinner 26 | type Spinner struct { 27 | config *SpinnerConfig 28 | message string 29 | current int 30 | active bool 31 | stopCh chan struct{} 32 | mu sync.RWMutex 33 | } 34 | 35 | // NewSpinner creates a new spinner with the given configuration 36 | func NewSpinner(config *SpinnerConfig) *Spinner { 37 | if config == nil { 38 | config = DefaultSpinnerConfig() 39 | } 40 | return &Spinner{ 41 | config: config, 42 | stopCh: make(chan struct{}), 43 | } 44 | } 45 | 46 | // State management 47 | 48 | // SetMessage sets the spinner message 49 | func (s *Spinner) SetMessage(message string) { 50 | s.mu.Lock() 51 | defer s.mu.Unlock() 52 | s.message = message 53 | } 54 | 55 | // IsActive returns whether the spinner is currently active 56 | func (s *Spinner) IsActive() bool { 57 | s.mu.RLock() 58 | defer s.mu.RUnlock() 59 | return s.active 60 | } 61 | 62 | // Control methods 63 | 64 | // Start begins the spinner animation 65 | func (s *Spinner) Start() { 66 | s.mu.Lock() 67 | if s.active { 68 | s.mu.Unlock() 69 | return 70 | } 71 | s.active = true 72 | s.mu.Unlock() 73 | 74 | go s.run() 75 | } 76 | 77 | // Stop halts the spinner animation 78 | func (s *Spinner) Stop() { 79 | s.mu.Lock() 80 | defer s.mu.Unlock() 81 | 82 | if !s.active { 83 | return 84 | } 85 | 86 | s.active = false 87 | close(s.stopCh) 88 | s.stopCh = make(chan struct{}) 89 | fmt.Print("\r") // Clear the spinner line 90 | } 91 | 92 | // Internal methods 93 | 94 | func (s *Spinner) run() { 95 | ticker := time.NewTicker(s.config.Delay) 96 | defer ticker.Stop() 97 | 98 | cyan := color.New(color.FgCyan, color.Bold) 99 | message := s.message 100 | 101 | // Print initial state 102 | fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message) 103 | 104 | for { 105 | select { 106 | case <-s.stopCh: 107 | return 108 | case <-ticker.C: 109 | s.mu.RLock() 110 | if !s.active { 111 | s.mu.RUnlock() 112 | return 113 | } 114 | frame := s.config.Frames[s.current%len(s.config.Frames)] 115 | s.current++ 116 | s.mu.RUnlock() 117 | 118 | fmt.Printf("\r %s", cyan.Sprint(frame)) 119 | fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /pkg/idgen/generator.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "sync" 8 | ) 9 | 10 | // Generator handles secure ID generation for machines and devices 11 | type Generator struct { 12 | bufferPool sync.Pool 13 | } 14 | 15 | // NewGenerator creates a new ID generator 16 | func NewGenerator() *Generator { 17 | return &Generator{ 18 | bufferPool: sync.Pool{ 19 | New: func() interface{} { 20 | return make([]byte, 64) 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | // Constants for ID generation 27 | const ( 28 | machineIDPrefix = "auth0|user_" 29 | uuidFormat = "%s-%s-%s-%s-%s" 30 | ) 31 | 32 | // generateRandomHex generates a random hex string of specified length 33 | func (g *Generator) generateRandomHex(length int) (string, error) { 34 | buffer := g.bufferPool.Get().([]byte) 35 | defer g.bufferPool.Put(buffer) 36 | 37 | if _, err := rand.Read(buffer[:length]); err != nil { 38 | return "", fmt.Errorf("failed to generate random bytes: %w", err) 39 | } 40 | return hex.EncodeToString(buffer[:length]), nil 41 | } 42 | 43 | // GenerateMachineID generates a new machine ID with auth0|user_ prefix 44 | func (g *Generator) GenerateMachineID() (string, error) { 45 | randomPart, err := g.generateRandomHex(32) // 生成64字符的十六进制 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | return fmt.Sprintf("%x%s", []byte(machineIDPrefix), randomPart), nil 51 | } 52 | 53 | // GenerateMacMachineID generates a new 64-byte MAC machine ID 54 | func (g *Generator) GenerateMacMachineID() (string, error) { 55 | return g.generateRandomHex(32) // 生成64字符的十六进制 56 | } 57 | 58 | // GenerateDeviceID generates a new device ID in UUID format 59 | func (g *Generator) GenerateDeviceID() (string, error) { 60 | id, err := g.generateRandomHex(16) 61 | if err != nil { 62 | return "", err 63 | } 64 | return fmt.Sprintf(uuidFormat, 65 | id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil 66 | } 67 | 68 | // GenerateSQMID generates a new SQM ID in UUID format (with braces) 69 | func (g *Generator) GenerateSQMID() (string, error) { 70 | id, err := g.GenerateDeviceID() 71 | if err != nil { 72 | return "", err 73 | } 74 | return fmt.Sprintf("{%s}", id), nil 75 | } 76 | 77 | // ValidateID validates the format of various ID types 78 | func (g *Generator) ValidateID(id string, idType string) bool { 79 | switch idType { 80 | case "machineID", "macMachineID": 81 | return len(id) == 64 && isHexString(id) 82 | case "deviceID": 83 | return isValidUUID(id) 84 | case "sqmID": 85 | if len(id) < 2 || id[0] != '{' || id[len(id)-1] != '}' { 86 | return false 87 | } 88 | return isValidUUID(id[1 : len(id)-1]) 89 | default: 90 | return false 91 | } 92 | } 93 | 94 | // Helper functions 95 | func isHexString(s string) bool { 96 | _, err := hex.DecodeString(s) 97 | return err == nil 98 | } 99 | 100 | func isValidUUID(uuid string) bool { 101 | if len(uuid) != 36 { 102 | return false 103 | } 104 | for i, r := range uuid { 105 | if i == 8 || i == 13 || i == 18 || i == 23 { 106 | if r != '-' { 107 | return false 108 | } 109 | continue 110 | } 111 | if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { 112 | return false 113 | } 114 | } 115 | return true 116 | } 117 | -------------------------------------------------------------------------------- /scripts/build_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置颜色代码 / Set color codes 4 | GREEN='\033[0;32m' 5 | RED='\033[0;31m' 6 | NC='\033[0m' # No Color / 无颜色 7 | 8 | # Build optimization flags 9 | OPTIMIZATION_FLAGS="-trimpath -ldflags=\"-s -w\"" 10 | PARALLEL_JOBS=$(nproc || echo "4") # Get number of CPU cores or default to 4 11 | 12 | # Messages / 消息 13 | EN_MESSAGES=( 14 | "Starting build process for version" 15 | "Cleaning old builds..." 16 | "Creating bin directory..." 17 | "Failed to create bin directory" 18 | "Building for" 19 | "Successfully built:" 20 | "Failed to build for" 21 | "Build Summary:" 22 | "Successful builds:" 23 | "Failed builds:" 24 | "Generated files:" 25 | ) 26 | 27 | CN_MESSAGES=( 28 | "开始构建版本" 29 | "正在清理旧的构建文件..." 30 | "正在创建bin目录..." 31 | "创建bin目录失败" 32 | "正在构建" 33 | "构建成功:" 34 | "构建失败:" 35 | "构建摘要:" 36 | "成功构建数:" 37 | "失败构建数:" 38 | "生成的文件:" 39 | "构建过程被中断" 40 | "错误:" 41 | ) 42 | 43 | # 版本信息 / Version info 44 | VERSION="1.0.0" 45 | 46 | # Detect system language / 检测系统语言 47 | detect_language() { 48 | if [[ $(locale | grep "LANG=zh_CN") ]]; then 49 | echo "cn" 50 | else 51 | echo "en" 52 | fi 53 | } 54 | 55 | # Get message based on language / 根据语言获取消息 56 | get_message() { 57 | local index=$1 58 | local lang=$(detect_language) 59 | 60 | if [[ "$lang" == "cn" ]]; then 61 | echo "${CN_MESSAGES[$index]}" 62 | else 63 | echo "${EN_MESSAGES[$index]}" 64 | fi 65 | } 66 | 67 | # 错误处理函数 / Error handling function 68 | handle_error() { 69 | echo -e "${RED}$(get_message 12) $1${NC}" 70 | exit 1 71 | } 72 | 73 | # 清理函数 / Cleanup function 74 | cleanup() { 75 | if [ -d "../bin" ]; then 76 | rm -rf ../bin 77 | echo -e "${GREEN}$(get_message 1)${NC}" 78 | fi 79 | } 80 | 81 | # Build function with optimizations 82 | build() { 83 | local os=$1 84 | local arch=$2 85 | local ext="" 86 | [ "$os" = "windows" ] && ext=".exe" 87 | 88 | echo -e "${GREEN}$(get_message 4) $os/$arch${NC}" 89 | 90 | GOOS=$os GOARCH=$arch CGO_ENABLED=0 go build \ 91 | -trimpath \ 92 | -ldflags="-s -w" \ 93 | -o "../bin/$os/$arch/cursor-id-modifier$ext" \ 94 | -a -installsuffix cgo \ 95 | -mod=readonly \ 96 | ../cmd/cursor-id-modifier & 97 | } 98 | 99 | # Parallel build execution 100 | build_all() { 101 | local builds=0 102 | local max_parallel=$PARALLEL_JOBS 103 | 104 | # Define build targets 105 | declare -A targets=( 106 | ["linux/amd64"]=1 107 | ["linux/386"]=1 108 | ["linux/arm64"]=1 109 | ["windows/amd64"]=1 110 | ["windows/386"]=1 111 | ["darwin/amd64"]=1 112 | ["darwin/arm64"]=1 113 | ) 114 | 115 | for target in "${!targets[@]}"; do 116 | IFS='/' read -r os arch <<< "$target" 117 | build "$os" "$arch" 118 | 119 | ((builds++)) 120 | 121 | if ((builds >= max_parallel)); then 122 | wait 123 | builds=0 124 | fi 125 | done 126 | 127 | # Wait for remaining builds 128 | wait 129 | } 130 | 131 | # Main execution 132 | main() { 133 | cleanup 134 | mkdir -p ../bin || { echo -e "${RED}$(get_message 3)${NC}"; exit 1; } 135 | build_all 136 | echo -e "${GREEN}Build completed successfully${NC}" 137 | } 138 | 139 | # 捕获错误信号 / Catch error signals 140 | trap 'echo -e "\n${RED}$(get_message 11)${NC}"; exit 1' INT TERM 141 | 142 | # 执行主函数 / Execute main function 143 | main -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | BLUE='\033[0;36m' 9 | YELLOW='\033[0;33m' 10 | NC='\033[0m' 11 | 12 | # Temporary directory for downloads 13 | TMP_DIR=$(mktemp -d) 14 | trap 'rm -rf "$TMP_DIR"' EXIT 15 | 16 | # Check for required commands 17 | check_requirements() { 18 | if ! command -v curl >/dev/null 2>&1; then 19 | echo -e "${RED}Error: curl is required${NC}" 20 | exit 1 21 | fi 22 | } 23 | 24 | # Detect system information 25 | detect_system() { 26 | local os arch suffix 27 | 28 | case "$(uname -s)" in 29 | Linux*) os="linux";; 30 | Darwin*) os="darwin";; 31 | *) echo -e "${RED}Unsupported OS${NC}"; exit 1;; 32 | esac 33 | 34 | case "$(uname -m)" in 35 | x86_64) 36 | arch="x86_64" 37 | ;; 38 | aarch64|arm64) 39 | arch="arm64" 40 | ;; 41 | i386|i686) 42 | arch="i386" 43 | ;; 44 | *) echo -e "${RED}Unsupported architecture${NC}"; exit 1;; 45 | esac 46 | 47 | echo "$os $arch" 48 | } 49 | 50 | # Download with progress 51 | download() { 52 | local url="$1" 53 | local output="$2" 54 | curl -#L "$url" -o "$output" 55 | } 56 | 57 | # Check and create installation directory 58 | setup_install_dir() { 59 | local install_dir="$1" 60 | 61 | if [ ! -d "$install_dir" ]; then 62 | mkdir -p "$install_dir" || { 63 | echo -e "${RED}Failed to create installation directory${NC}" 64 | exit 1 65 | } 66 | fi 67 | } 68 | 69 | # Main installation function 70 | main() { 71 | check_requirements 72 | 73 | echo -e "${BLUE}Starting installation...${NC}" 74 | 75 | # Detect system 76 | read -r OS ARCH SUFFIX <<< "$(detect_system)" 77 | echo -e "${GREEN}Detected: $OS $ARCH${NC}" 78 | 79 | # Set installation directory 80 | INSTALL_DIR="/usr/local/bin" 81 | 82 | # Setup installation directory 83 | setup_install_dir "$INSTALL_DIR" 84 | 85 | # Get latest release info 86 | echo -e "${BLUE}Fetching latest release information...${NC}" 87 | LATEST_URL="https://api.github.com/repos/yuaotian/go-cursor-help/releases/latest" 88 | 89 | # Get latest version and remove 'v' prefix 90 | VERSION=$(curl -s "$LATEST_URL" | grep "tag_name" | cut -d'"' -f4 | sed 's/^v//') 91 | 92 | # Construct binary name 93 | BINARY_NAME="cursor-id-modifier_${VERSION}_${OS}_${ARCH}" 94 | echo -e "${BLUE}Looking for asset: $BINARY_NAME${NC}" 95 | 96 | # Get download URL directly 97 | DOWNLOAD_URL=$(curl -s "$LATEST_URL" | grep -o "\"browser_download_url\": \"[^\"]*${BINARY_NAME}[^\"]*\"" | cut -d'"' -f4) 98 | 99 | if [ -z "$DOWNLOAD_URL" ]; then 100 | echo -e "${RED}Error: Could not find appropriate binary for $OS $ARCH${NC}" 101 | echo -e "${YELLOW}Available assets:${NC}" 102 | curl -s "$LATEST_URL" | grep "browser_download_url" | cut -d'"' -f4 103 | exit 1 104 | fi 105 | 106 | echo -e "${GREEN}Found matching asset: $BINARY_NAME${NC}" 107 | echo -e "${BLUE}Downloading from: $DOWNLOAD_URL${NC}" 108 | 109 | download "$DOWNLOAD_URL" "$TMP_DIR/cursor-id-modifier" 110 | 111 | # Install binary 112 | echo -e "${BLUE}Installing...${NC}" 113 | chmod +x "$TMP_DIR/cursor-id-modifier" 114 | sudo mv "$TMP_DIR/cursor-id-modifier" "$INSTALL_DIR/" 115 | 116 | echo -e "${GREEN}Installation completed successfully!${NC}" 117 | echo -e "${BLUE}Running cursor-id-modifier...${NC}" 118 | 119 | # Run the program with sudo, preserving environment variables 120 | export AUTOMATED_MODE=1 121 | if ! sudo -E cursor-id-modifier; then 122 | echo -e "${RED}Failed to run cursor-id-modifier${NC}" 123 | exit 1 124 | fi 125 | } 126 | 127 | main 128 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // StorageConfig represents the storage configuration 14 | type StorageConfig struct { 15 | TelemetryMacMachineId string `json:"telemetry.macMachineId"` 16 | TelemetryMachineId string `json:"telemetry.machineId"` 17 | TelemetryDevDeviceId string `json:"telemetry.devDeviceId"` 18 | TelemetrySqmId string `json:"telemetry.sqmId"` 19 | LastModified string `json:"lastModified"` 20 | Version string `json:"version"` 21 | } 22 | 23 | // Manager handles configuration operations 24 | type Manager struct { 25 | configPath string 26 | mu sync.RWMutex 27 | } 28 | 29 | // NewManager creates a new configuration manager 30 | func NewManager(username string) (*Manager, error) { 31 | configPath, err := getConfigPath(username) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to get config path: %w", err) 34 | } 35 | return &Manager{configPath: configPath}, nil 36 | } 37 | 38 | // ReadConfig reads the existing configuration 39 | func (m *Manager) ReadConfig() (*StorageConfig, error) { 40 | m.mu.RLock() 41 | defer m.mu.RUnlock() 42 | 43 | data, err := os.ReadFile(m.configPath) 44 | if err != nil { 45 | if os.IsNotExist(err) { 46 | return nil, nil 47 | } 48 | return nil, fmt.Errorf("failed to read config file: %w", err) 49 | } 50 | 51 | var config StorageConfig 52 | if err := json.Unmarshal(data, &config); err != nil { 53 | return nil, fmt.Errorf("failed to parse config file: %w", err) 54 | } 55 | 56 | return &config, nil 57 | } 58 | 59 | // SaveConfig saves the configuration 60 | func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { 61 | m.mu.Lock() 62 | defer m.mu.Unlock() 63 | 64 | // Ensure parent directories exist 65 | if err := os.MkdirAll(filepath.Dir(m.configPath), 0755); err != nil { 66 | return fmt.Errorf("failed to create config directory: %w", err) 67 | } 68 | 69 | // Prepare updated configuration 70 | updatedConfig := m.prepareUpdatedConfig(config) 71 | 72 | // Write configuration 73 | if err := m.writeConfigFile(updatedConfig, readOnly); err != nil { 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // prepareUpdatedConfig merges existing config with updates 81 | func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} { 82 | // Read existing config 83 | originalFile := make(map[string]interface{}) 84 | if data, err := os.ReadFile(m.configPath); err == nil { 85 | json.Unmarshal(data, &originalFile) 86 | } 87 | 88 | // Update fields 89 | originalFile["telemetry.sqmId"] = config.TelemetrySqmId 90 | originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId 91 | originalFile["telemetry.machineId"] = config.TelemetryMachineId 92 | originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId 93 | originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) 94 | // originalFile["version"] = "1.0.1" 95 | 96 | return originalFile 97 | } 98 | 99 | // writeConfigFile handles the atomic write of the config file 100 | func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error { 101 | // Marshal with indentation 102 | content, err := json.MarshalIndent(config, "", " ") 103 | if err != nil { 104 | return fmt.Errorf("failed to marshal config: %w", err) 105 | } 106 | 107 | // Write to temporary file 108 | tmpPath := m.configPath + ".tmp" 109 | if err := os.WriteFile(tmpPath, content, 0666); err != nil { 110 | return fmt.Errorf("failed to write temporary file: %w", err) 111 | } 112 | 113 | // Set final permissions 114 | fileMode := os.FileMode(0666) 115 | if readOnly { 116 | fileMode = 0444 117 | } 118 | 119 | if err := os.Chmod(tmpPath, fileMode); err != nil { 120 | os.Remove(tmpPath) 121 | return fmt.Errorf("failed to set temporary file permissions: %w", err) 122 | } 123 | 124 | // Atomic rename 125 | if err := os.Rename(tmpPath, m.configPath); err != nil { 126 | os.Remove(tmpPath) 127 | return fmt.Errorf("failed to rename file: %w", err) 128 | } 129 | 130 | // Sync directory 131 | if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { 132 | defer dir.Close() 133 | dir.Sync() 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // getConfigPath returns the path to the configuration file 140 | func getConfigPath(username string) (string, error) { 141 | var configDir string 142 | switch runtime.GOOS { 143 | case "windows": 144 | configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage") 145 | case "darwin": 146 | configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage") 147 | case "linux": 148 | configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage") 149 | default: 150 | return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 151 | } 152 | return filepath.Join(configDir, "storage.json"), nil 153 | } 154 | -------------------------------------------------------------------------------- /internal/lang/lang.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // Language represents a supported language code 11 | type Language string 12 | 13 | const ( 14 | // CN represents Chinese language 15 | CN Language = "cn" 16 | // EN represents English language 17 | EN Language = "en" 18 | ) 19 | 20 | // TextResource contains all translatable text resources 21 | type TextResource struct { 22 | // Success messages 23 | SuccessMessage string 24 | RestartMessage string 25 | 26 | // Progress messages 27 | ReadingConfig string 28 | GeneratingIds string 29 | CheckingProcesses string 30 | ClosingProcesses string 31 | ProcessesClosed string 32 | PleaseWait string 33 | 34 | // Error messages 35 | ErrorPrefix string 36 | PrivilegeError string 37 | 38 | // Instructions 39 | RunAsAdmin string 40 | RunWithSudo string 41 | SudoExample string 42 | PressEnterToExit string 43 | SetReadOnlyMessage string 44 | 45 | // Info messages 46 | ConfigLocation string 47 | } 48 | 49 | var ( 50 | currentLanguage Language 51 | currentLanguageOnce sync.Once 52 | languageMutex sync.RWMutex 53 | ) 54 | 55 | // GetCurrentLanguage returns the current language, detecting it if not already set 56 | func GetCurrentLanguage() Language { 57 | currentLanguageOnce.Do(func() { 58 | currentLanguage = detectLanguage() 59 | }) 60 | 61 | languageMutex.RLock() 62 | defer languageMutex.RUnlock() 63 | return currentLanguage 64 | } 65 | 66 | // SetLanguage sets the current language 67 | func SetLanguage(lang Language) { 68 | languageMutex.Lock() 69 | defer languageMutex.Unlock() 70 | currentLanguage = lang 71 | } 72 | 73 | // GetText returns the TextResource for the current language 74 | func GetText() TextResource { 75 | return texts[GetCurrentLanguage()] 76 | } 77 | 78 | // detectLanguage detects the system language 79 | func detectLanguage() Language { 80 | // Check environment variables first 81 | if isChineseEnvVar() { 82 | return CN 83 | } 84 | 85 | // Then check OS-specific locale 86 | if isWindows() { 87 | if isWindowsChineseLocale() { 88 | return CN 89 | } 90 | } else if isUnixChineseLocale() { 91 | return CN 92 | } 93 | 94 | return EN 95 | } 96 | 97 | func isChineseEnvVar() bool { 98 | for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { 99 | if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | 106 | func isWindows() bool { 107 | return os.Getenv("OS") == "Windows_NT" 108 | } 109 | 110 | func isWindowsChineseLocale() bool { 111 | // Check Windows UI culture 112 | cmd := exec.Command("powershell", "-Command", 113 | "[System.Globalization.CultureInfo]::CurrentUICulture.Name") 114 | output, err := cmd.Output() 115 | if err == nil && strings.HasPrefix(strings.ToLower(strings.TrimSpace(string(output))), "zh") { 116 | return true 117 | } 118 | 119 | // Check Windows locale 120 | cmd = exec.Command("wmic", "os", "get", "locale") 121 | output, err = cmd.Output() 122 | return err == nil && strings.Contains(string(output), "2052") 123 | } 124 | 125 | func isUnixChineseLocale() bool { 126 | cmd := exec.Command("locale") 127 | output, err := cmd.Output() 128 | return err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn") 129 | } 130 | 131 | // texts contains all translations 132 | var texts = map[Language]TextResource{ 133 | CN: { 134 | // Success messages 135 | SuccessMessage: "[√] 配置文件已成功更新!", 136 | RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", 137 | 138 | // Progress messages 139 | ReadingConfig: "正在读取配置文件...", 140 | GeneratingIds: "正在生成新的标识符...", 141 | CheckingProcesses: "正在检查运行中的 Cursor 实例...", 142 | ClosingProcesses: "正在关闭 Cursor 实例...", 143 | ProcessesClosed: "所有 Cursor 实例已关闭", 144 | PleaseWait: "请稍候...", 145 | 146 | // Error messages 147 | ErrorPrefix: "程序发生严重错误: %v", 148 | PrivilegeError: "\n[!] 错误:需要管理员权限", 149 | 150 | // Instructions 151 | RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」", 152 | RunWithSudo: "请使用 sudo 命令运行此程序", 153 | SudoExample: "示例: sudo %s", 154 | PressEnterToExit: "\n按回车键退出程序...", 155 | SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", 156 | 157 | // Info messages 158 | ConfigLocation: "配置文件位置:", 159 | }, 160 | EN: { 161 | // Success messages 162 | SuccessMessage: "[√] Configuration file updated successfully!", 163 | RestartMessage: "[!] Please restart Cursor manually for changes to take effect", 164 | 165 | // Progress messages 166 | ReadingConfig: "Reading configuration file...", 167 | GeneratingIds: "Generating new identifiers...", 168 | CheckingProcesses: "Checking for running Cursor instances...", 169 | ClosingProcesses: "Closing Cursor instances...", 170 | ProcessesClosed: "All Cursor instances have been closed", 171 | PleaseWait: "Please wait...", 172 | 173 | // Error messages 174 | ErrorPrefix: "Program encountered a serious error: %v", 175 | PrivilegeError: "\n[!] Error: Administrator privileges required", 176 | 177 | // Instructions 178 | RunAsAdmin: "Please right-click and select 'Run as Administrator'", 179 | RunWithSudo: "Please run this program with sudo", 180 | SudoExample: "Example: sudo %s", 181 | PressEnterToExit: "\nPress Enter to exit...", 182 | SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", 183 | 184 | // Info messages 185 | ConfigLocation: "Config file location:", 186 | }, 187 | } 188 | -------------------------------------------------------------------------------- /scripts/run/cursor_win_id_modifier.ps1: -------------------------------------------------------------------------------- 1 | # 设置输出编码为 UTF-8 2 | $OutputEncoding = [System.Text.Encoding]::UTF8 3 | [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 4 | 5 | # 颜色定义 6 | $RED = "`e[31m" 7 | $GREEN = "`e[32m" 8 | $YELLOW = "`e[33m" 9 | $BLUE = "`e[34m" 10 | $NC = "`e[0m" 11 | 12 | # 配置文件路径 13 | $STORAGE_FILE = "$env:APPDATA\Cursor\User\globalStorage\storage.json" 14 | $BACKUP_DIR = "$env:APPDATA\Cursor\User\globalStorage\backups" 15 | 16 | # 检查管理员权限 17 | function Test-Administrator { 18 | $user = [Security.Principal.WindowsIdentity]::GetCurrent() 19 | $principal = New-Object Security.Principal.WindowsPrincipal($user) 20 | return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 21 | } 22 | 23 | if (-not (Test-Administrator)) { 24 | Write-Host "$RED[错误]$NC 请以管理员身份运行此脚本" 25 | Write-Host "请右键点击脚本,选择'以管理员身份运行'" 26 | Read-Host "按回车键退出" 27 | exit 1 28 | } 29 | 30 | # 显示 Logo 31 | Clear-Host 32 | Write-Host @" 33 | 34 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 35 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 36 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 37 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 38 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 39 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 40 | 41 | "@ 42 | Write-Host "$BLUE================================$NC" 43 | Write-Host "$GREEN Cursor ID 修改工具 $NC" 44 | Write-Host "$BLUE================================$NC" 45 | Write-Host "" 46 | 47 | # 检查并关闭 Cursor 进程 48 | Write-Host "$GREEN[信息]$NC 检查 Cursor 进程..." 49 | 50 | function Get-ProcessDetails { 51 | param($processName) 52 | Write-Host "$BLUE[调试]$NC 正在获取 $processName 进程详细信息:" 53 | Get-WmiObject Win32_Process -Filter "name='$processName'" | 54 | Select-Object ProcessId, ExecutablePath, CommandLine | 55 | Format-List 56 | } 57 | 58 | # 定义最大重试次数和等待时间 59 | $MAX_RETRIES = 5 60 | $WAIT_TIME = 1 61 | 62 | # 处理进程关闭 63 | function Close-CursorProcess { 64 | param($processName) 65 | 66 | $process = Get-Process -Name $processName -ErrorAction SilentlyContinue 67 | if ($process) { 68 | Write-Host "$YELLOW[警告]$NC 发现 $processName 正在运行" 69 | Get-ProcessDetails $processName 70 | 71 | Write-Host "$YELLOW[警告]$NC 尝试关闭 $processName..." 72 | Stop-Process -Name $processName -Force 73 | 74 | $retryCount = 0 75 | while ($retryCount -lt $MAX_RETRIES) { 76 | $process = Get-Process -Name $processName -ErrorAction SilentlyContinue 77 | if (-not $process) { break } 78 | 79 | $retryCount++ 80 | if ($retryCount -ge $MAX_RETRIES) { 81 | Write-Host "$RED[错误]$NC 在 $MAX_RETRIES 次尝试后仍无法关闭 $processName" 82 | Get-ProcessDetails $processName 83 | Write-Host "$RED[错误]$NC 请手动关闭进程后重试" 84 | Read-Host "按回车键退出" 85 | exit 1 86 | } 87 | Write-Host "$YELLOW[警告]$NC 等待进程关闭,尝试 $retryCount/$MAX_RETRIES..." 88 | Start-Sleep -Seconds $WAIT_TIME 89 | } 90 | Write-Host "$GREEN[信息]$NC $processName 已成功关闭" 91 | } 92 | } 93 | 94 | # 关闭所有 Cursor 进程 95 | Close-CursorProcess "Cursor" 96 | Close-CursorProcess "cursor" 97 | 98 | # 创建备份目录 99 | if (-not (Test-Path $BACKUP_DIR)) { 100 | New-Item -ItemType Directory -Path $BACKUP_DIR | Out-Null 101 | } 102 | 103 | # 备份现有配置 104 | if (Test-Path $STORAGE_FILE) { 105 | Write-Host "$GREEN[信息]$NC 正在备份配置文件..." 106 | $backupName = "storage.json.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" 107 | Copy-Item $STORAGE_FILE "$BACKUP_DIR\$backupName" 108 | } 109 | 110 | # 生成新的 ID 111 | Write-Host "$GREEN[信息]$NC 正在生成新的 ID..." 112 | 113 | # 生成随机字节数组并转换为十六进制字符串的函数 114 | function Get-RandomHex { 115 | param ( 116 | [int]$length 117 | ) 118 | $bytes = New-Object byte[] $length 119 | $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() 120 | $rng.GetBytes($bytes) 121 | $rng.Dispose() 122 | return -join ($bytes | ForEach-Object { '{0:x2}' -f $_ }) 123 | } 124 | 125 | $UUID = [System.Guid]::NewGuid().ToString() 126 | # 将 auth0|user_ 转换为字节数组的十六进制 127 | $prefixBytes = [System.Text.Encoding]::UTF8.GetBytes("auth0|user_") 128 | $prefixHex = -join ($prefixBytes | ForEach-Object { '{0:x2}' -f $_ }) 129 | # 生成32字节(64个十六进制字符)的随机数作为 machineId 的随机部分 130 | $randomPart = Get-RandomHex -length 32 131 | $MACHINE_ID = "$prefixHex$randomPart" 132 | $MAC_MACHINE_ID = Get-RandomHex -length 32 133 | $SQM_ID = "{$([System.Guid]::NewGuid().ToString().ToUpper())}" 134 | 135 | # 创建或更新配置文件 136 | Write-Host "$GREEN[信息]$NC 正在更新配置..." 137 | 138 | $config = @{ 139 | 'telemetry.machineId' = $MACHINE_ID 140 | 'telemetry.macMachineId' = $MAC_MACHINE_ID 141 | 'telemetry.devDeviceId' = $UUID 142 | 'telemetry.sqmId' = $SQM_ID 143 | } 144 | 145 | $config | ConvertTo-Json | Set-Content $STORAGE_FILE 146 | 147 | # 设置文件权限 148 | $acl = Get-Acl $STORAGE_FILE 149 | $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( 150 | $env:USERNAME, "FullControl", "Allow" 151 | ) 152 | $acl.SetAccessRule($rule) 153 | Set-Acl $STORAGE_FILE $acl 154 | 155 | # 显示结果 156 | Write-Host "" 157 | Write-Host "$GREEN[信息]$NC 已更新配置:" 158 | Write-Host "$BLUE[调试]$NC machineId: $MACHINE_ID" 159 | Write-Host "$BLUE[调试]$NC macMachineId: $MAC_MACHINE_ID" 160 | Write-Host "$BLUE[调试]$NC devDeviceId: $UUID" 161 | Write-Host "$BLUE[调试]$NC sqmId: $SQM_ID" 162 | 163 | 164 | 165 | # 显示文件树结构 166 | Write-Host "" 167 | Write-Host "$GREEN[信息]$NC 文件结构:" 168 | Write-Host "$BLUE$env:APPDATA\Cursor\User$NC" 169 | Write-Host "├── globalStorage" 170 | Write-Host "│ ├── storage.json (已修改)" 171 | Write-Host "│ └── backups" 172 | 173 | # 列出备份文件 174 | $backupFiles = Get-ChildItem "$BACKUP_DIR\*" -ErrorAction SilentlyContinue 175 | if ($backupFiles) { 176 | foreach ($file in $backupFiles) { 177 | Write-Host "│ └── $($file.Name)" 178 | } 179 | } else { 180 | Write-Host "│ └── (空)" 181 | } 182 | Write-Host "" 183 | # 显示公众号信息 184 | Write-Host "" 185 | Write-Host "$GREEN================================$NC" 186 | Write-Host "$YELLOW 关注公众号【煎饼果子卷AI】一起交流更多Cursor技巧和AI知识 $NC" 187 | Write-Host "$GREEN================================$NC" 188 | Write-Host "" 189 | Write-Host "$GREEN[信息]$NC 请重启 Cursor 以应用新的配置" 190 | Write-Host "" 191 | 192 | Read-Host "按回车键退出" 193 | exit 0 -------------------------------------------------------------------------------- /internal/process/manager.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "strings" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Config holds process manager configuration 14 | type Config struct { 15 | MaxAttempts int // Maximum number of attempts to kill processes 16 | RetryDelay time.Duration // Delay between retry attempts 17 | ProcessPatterns []string // Process names to look for 18 | } 19 | 20 | // DefaultConfig returns the default configuration 21 | func DefaultConfig() *Config { 22 | return &Config{ 23 | MaxAttempts: 3, 24 | RetryDelay: 2 * time.Second, 25 | ProcessPatterns: []string{ 26 | "Cursor.exe", // Windows executable 27 | "Cursor ", // Linux/macOS executable with space 28 | "cursor ", // Linux/macOS executable lowercase with space 29 | "cursor", // Linux/macOS executable lowercase 30 | "Cursor", // Linux/macOS executable 31 | "*cursor*", // Any process containing cursor 32 | "*Cursor*", // Any process containing Cursor 33 | }, 34 | } 35 | } 36 | 37 | // Manager handles process-related operations 38 | type Manager struct { 39 | config *Config 40 | log *logrus.Logger 41 | } 42 | 43 | // NewManager creates a new process manager with optional config and logger 44 | func NewManager(config *Config, log *logrus.Logger) *Manager { 45 | if config == nil { 46 | config = DefaultConfig() 47 | } 48 | if log == nil { 49 | log = logrus.New() 50 | } 51 | return &Manager{ 52 | config: config, 53 | log: log, 54 | } 55 | } 56 | 57 | // IsCursorRunning checks if any Cursor process is currently running 58 | func (m *Manager) IsCursorRunning() bool { 59 | processes, err := m.getCursorProcesses() 60 | if err != nil { 61 | m.log.Warn("Failed to get Cursor processes:", err) 62 | return false 63 | } 64 | return len(processes) > 0 65 | } 66 | 67 | // KillCursorProcesses attempts to kill all running Cursor processes 68 | func (m *Manager) KillCursorProcesses() error { 69 | for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ { 70 | processes, err := m.getCursorProcesses() 71 | if err != nil { 72 | return fmt.Errorf("failed to get processes: %w", err) 73 | } 74 | 75 | if len(processes) == 0 { 76 | return nil 77 | } 78 | 79 | // Try graceful shutdown first on Windows 80 | if runtime.GOOS == "windows" { 81 | for _, pid := range processes { 82 | exec.Command("taskkill", "/PID", pid).Run() 83 | time.Sleep(500 * time.Millisecond) 84 | } 85 | } 86 | 87 | // Force kill remaining processes 88 | remainingProcesses, _ := m.getCursorProcesses() 89 | for _, pid := range remainingProcesses { 90 | m.killProcess(pid) 91 | } 92 | 93 | time.Sleep(m.config.RetryDelay) 94 | 95 | if processes, _ := m.getCursorProcesses(); len(processes) == 0 { 96 | return nil 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // getCursorProcesses returns PIDs of running Cursor processes 104 | func (m *Manager) getCursorProcesses() ([]string, error) { 105 | cmd := m.getProcessListCommand() 106 | if cmd == nil { 107 | return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 108 | } 109 | 110 | output, err := cmd.Output() 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to execute command: %w", err) 113 | } 114 | 115 | return m.parseProcessList(string(output)), nil 116 | } 117 | 118 | // getProcessListCommand returns the appropriate command to list processes based on OS 119 | func (m *Manager) getProcessListCommand() *exec.Cmd { 120 | switch runtime.GOOS { 121 | case "windows": 122 | return exec.Command("tasklist", "/FO", "CSV", "/NH") 123 | case "darwin": 124 | return exec.Command("ps", "-ax") 125 | case "linux": 126 | return exec.Command("ps", "-A") 127 | default: 128 | return nil 129 | } 130 | } 131 | 132 | // parseProcessList extracts Cursor process PIDs from process list output 133 | func (m *Manager) parseProcessList(output string) []string { 134 | var processes []string 135 | for _, line := range strings.Split(output, "\n") { 136 | lowerLine := strings.ToLower(line) 137 | 138 | if m.isOwnProcess(lowerLine) { 139 | continue 140 | } 141 | 142 | if pid := m.findCursorProcess(line, lowerLine); pid != "" { 143 | processes = append(processes, pid) 144 | } 145 | } 146 | return processes 147 | } 148 | 149 | // isOwnProcess checks if the process belongs to this application 150 | func (m *Manager) isOwnProcess(line string) bool { 151 | return strings.Contains(line, "cursor-id-modifier") || 152 | strings.Contains(line, "cursor-helper") 153 | } 154 | 155 | // findCursorProcess checks if a process line matches Cursor patterns and returns its PID 156 | func (m *Manager) findCursorProcess(line, lowerLine string) string { 157 | for _, pattern := range m.config.ProcessPatterns { 158 | if m.matchPattern(lowerLine, strings.ToLower(pattern)) { 159 | return m.extractPID(line) 160 | } 161 | } 162 | return "" 163 | } 164 | 165 | // matchPattern checks if a line matches a pattern, supporting wildcards 166 | func (m *Manager) matchPattern(line, pattern string) bool { 167 | switch { 168 | case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"): 169 | search := pattern[1 : len(pattern)-1] 170 | return strings.Contains(line, search) 171 | case strings.HasPrefix(pattern, "*"): 172 | return strings.HasSuffix(line, pattern[1:]) 173 | case strings.HasSuffix(pattern, "*"): 174 | return strings.HasPrefix(line, pattern[:len(pattern)-1]) 175 | default: 176 | return line == pattern 177 | } 178 | } 179 | 180 | // extractPID extracts process ID from a process list line based on OS format 181 | func (m *Manager) extractPID(line string) string { 182 | switch runtime.GOOS { 183 | case "windows": 184 | parts := strings.Split(line, ",") 185 | if len(parts) >= 2 { 186 | return strings.Trim(parts[1], "\"") 187 | } 188 | case "darwin", "linux": 189 | parts := strings.Fields(line) 190 | if len(parts) >= 1 { 191 | return parts[0] 192 | } 193 | } 194 | return "" 195 | } 196 | 197 | // killProcess forcefully terminates a process by PID 198 | func (m *Manager) killProcess(pid string) error { 199 | cmd := m.getKillCommand(pid) 200 | if cmd == nil { 201 | return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 202 | } 203 | return cmd.Run() 204 | } 205 | 206 | // getKillCommand returns the appropriate command to kill a process based on OS 207 | func (m *Manager) getKillCommand(pid string) *exec.Cmd { 208 | switch runtime.GOOS { 209 | case "windows": 210 | return exec.Command("taskkill", "/F", "/PID", pid) 211 | case "darwin", "linux": 212 | return exec.Command("kill", "-9", pid) 213 | default: 214 | return nil 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /scripts/run/cursor_linux_id_modifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置错误处理 4 | set -e 5 | 6 | # 颜色定义 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | BLUE='\033[0;34m' 11 | NC='\033[0m' # No Color 12 | 13 | # 日志函数 14 | log_info() { 15 | echo -e "${GREEN}[INFO]${NC} $1" 16 | } 17 | 18 | log_warn() { 19 | echo -e "${YELLOW}[WARN]${NC} $1" 20 | } 21 | 22 | log_error() { 23 | echo -e "${RED}[ERROR]${NC} $1" 24 | } 25 | 26 | log_debug() { 27 | echo -e "${BLUE}[DEBUG]${NC} $1" 28 | } 29 | 30 | # 获取当前用户 31 | get_current_user() { 32 | if [ "$EUID" -eq 0 ]; then 33 | echo "$SUDO_USER" 34 | else 35 | echo "$USER" 36 | fi 37 | } 38 | 39 | CURRENT_USER=$(get_current_user) 40 | if [ -z "$CURRENT_USER" ]; then 41 | log_error "无法获取用户名" 42 | exit 1 43 | fi 44 | 45 | # 定义配置文件路径 (Linux 路径) 46 | STORAGE_FILE="/home/$CURRENT_USER/.config/Cursor/User/globalStorage/storage.json" 47 | BACKUP_DIR="/home/$CURRENT_USER/.config/Cursor/User/globalStorage/backups" 48 | 49 | # 检查权限 50 | check_permissions() { 51 | if [ "$EUID" -ne 0 ]; then 52 | log_error "请使用 sudo 运行此脚本" 53 | echo "示例: sudo $0" 54 | exit 1 55 | fi 56 | } 57 | 58 | # 检查并关闭 Cursor 进程 59 | check_and_kill_cursor() { 60 | log_info "检查 Cursor 进程..." 61 | 62 | local attempt=1 63 | local max_attempts=5 64 | 65 | # 函数:获取进程详细信息 66 | get_process_details() { 67 | local process_name="$1" 68 | log_debug "正在获取 $process_name 进程详细信息:" 69 | ps aux | grep -i "$process_name" | grep -v grep 70 | } 71 | 72 | while [ $attempt -le $max_attempts ]; do 73 | CURSOR_PIDS=$(pgrep -i "cursor" || true) 74 | 75 | if [ -z "$CURSOR_PIDS" ]; then 76 | log_info "未发现运行中的 Cursor 进程" 77 | return 0 78 | fi 79 | 80 | log_warn "发现 Cursor 进程正在运行" 81 | get_process_details "cursor" 82 | 83 | log_warn "尝试关闭 Cursor 进程..." 84 | 85 | if [ $attempt -eq $max_attempts ]; then 86 | log_warn "尝试强制终止进程..." 87 | kill -9 $CURSOR_PIDS 2>/dev/null || true 88 | else 89 | kill $CURSOR_PIDS 2>/dev/null || true 90 | fi 91 | 92 | sleep 1 93 | 94 | if ! pgrep -i "cursor" > /dev/null; then 95 | log_info "Cursor 进程已成功关闭" 96 | return 0 97 | fi 98 | 99 | log_warn "等待进程关闭,尝试 $attempt/$max_attempts..." 100 | ((attempt++)) 101 | done 102 | 103 | log_error "在 $max_attempts 次尝试后仍无法关闭 Cursor 进程" 104 | get_process_details "cursor" 105 | log_error "请手动关闭进程后重试" 106 | exit 1 107 | } 108 | 109 | # 备份配置文件 110 | backup_config() { 111 | if [ ! -f "$STORAGE_FILE" ]; then 112 | log_warn "配置文件不存在,跳过备份" 113 | return 0 114 | fi 115 | 116 | mkdir -p "$BACKUP_DIR" 117 | local backup_file="$BACKUP_DIR/storage.json.backup_$(date +%Y%m%d_%H%M%S)" 118 | 119 | if cp "$STORAGE_FILE" "$backup_file"; then 120 | chmod 644 "$backup_file" 121 | chown "$CURRENT_USER:$CURRENT_USER" "$backup_file" 122 | log_info "配置已备份到: $backup_file" 123 | else 124 | log_error "备份失败" 125 | exit 1 126 | fi 127 | } 128 | 129 | # 生成随机 ID 130 | generate_random_id() { 131 | # Linux 可以使用 /dev/urandom 132 | head -c 32 /dev/urandom | xxd -p 133 | } 134 | 135 | # 生成随机 UUID 136 | generate_uuid() { 137 | # Linux 使用 uuidgen 命令 138 | uuidgen | tr '[:upper:]' '[:lower:]' 139 | } 140 | 141 | # 生成新的配置 142 | generate_new_config() { 143 | local machine_id="auth0|user_$(generate_random_id)" 144 | local mac_machine_id=$(generate_random_id) 145 | local device_id=$(generate_uuid | tr '[:upper:]' '[:lower:]') 146 | local sqm_id="{$(generate_uuid | tr '[:lower:]' '[:upper:]')}" 147 | 148 | if [ -f "$STORAGE_FILE" ]; then 149 | # 直接修改现有文件 (Linux sed 不需要 '') 150 | sed -i "s/\"telemetry\.machineId\":[[:space:]]*\"[^\"]*\"/\"telemetry.machineId\": \"$machine_id\"/" "$STORAGE_FILE" 151 | sed -i "s/\"telemetry\.macMachineId\":[[:space:]]*\"[^\"]*\"/\"telemetry.macMachineId\": \"$mac_machine_id\"/" "$STORAGE_FILE" 152 | sed -i "s/\"telemetry\.devDeviceId\":[[:space:]]*\"[^\"]*\"/\"telemetry.devDeviceId\": \"$device_id\"/" "$STORAGE_FILE" 153 | sed -i "s/\"telemetry\.sqmId\":[[:space:]]*\"[^\"]*\"/\"telemetry.sqmId\": \"$sqm_id\"/" "$STORAGE_FILE" 154 | else 155 | # 创建新文件 156 | echo "{" > "$STORAGE_FILE" 157 | echo " \"telemetry.machineId\": \"$machine_id\"," >> "$STORAGE_FILE" 158 | echo " \"telemetry.macMachineId\": \"$mac_machine_id\"," >> "$STORAGE_FILE" 159 | echo " \"telemetry.devDeviceId\": \"$device_id\"," >> "$STORAGE_FILE" 160 | echo " \"telemetry.sqmId\": \"$sqm_id\"" >> "$STORAGE_FILE" 161 | echo "}" >> "$STORAGE_FILE" 162 | fi 163 | 164 | chmod 644 "$STORAGE_FILE" 165 | chown "$CURRENT_USER:$CURRENT_USER" "$STORAGE_FILE" 166 | 167 | echo 168 | log_info "已更新配置:" 169 | log_debug "machineId: $machine_id" 170 | log_debug "macMachineId: $mac_machine_id" 171 | log_debug "devDeviceId: $device_id" 172 | log_debug "sqmId: $sqm_id" 173 | } 174 | 175 | # 显示文件树结构 176 | show_file_tree() { 177 | local base_dir=$(dirname "$STORAGE_FILE") 178 | echo 179 | log_info "文件结构:" 180 | echo -e "${BLUE}$base_dir${NC}" 181 | echo "├── globalStorage" 182 | echo "│ ├── storage.json (已修改)" 183 | echo "│ └── backups" 184 | 185 | # 列出备份文件 186 | if [ -d "$BACKUP_DIR" ]; then 187 | local backup_files=("$BACKUP_DIR"/*) 188 | if [ ${#backup_files[@]} -gt 0 ]; then 189 | for file in "${backup_files[@]}"; do 190 | if [ -f "$file" ]; then 191 | echo "│ └── $(basename "$file")" 192 | fi 193 | done 194 | else 195 | echo "│ └── (空)" 196 | fi 197 | fi 198 | echo 199 | } 200 | 201 | # 显示公众号信息 202 | show_follow_info() { 203 | echo 204 | echo -e "${GREEN}================================${NC}" 205 | echo -e "${YELLOW} 关注公众号【煎饼果子AI】一起交流更多Cursor技巧和AI知识 ${NC}" 206 | echo -e "${GREEN}================================${NC}" 207 | echo 208 | } 209 | 210 | # 主函数 211 | main() { 212 | clear 213 | # 显示 CURSOR Logo 214 | echo -e " 215 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 216 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 217 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 218 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 219 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 220 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 221 | " 222 | echo -e "${BLUE}================================${NC}" 223 | echo -e "${GREEN} Cursor ID 修改工具${NC}" 224 | echo -e "${BLUE}================================${NC}" 225 | echo 226 | 227 | check_permissions 228 | check_and_kill_cursor 229 | backup_config 230 | generate_new_config 231 | 232 | echo 233 | log_info "操作完成!" 234 | show_follow_info 235 | show_file_tree 236 | log_info "请重启 Cursor 以应用新的配置" 237 | echo 238 | } 239 | 240 | # 执行主函数 241 | main 242 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | # Check for admin rights and handle elevation 2 | $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") 3 | if (-NOT $isAdmin) { 4 | # Detect PowerShell version and path 5 | $pwshPath = if (Get-Command "pwsh" -ErrorAction SilentlyContinue) { 6 | (Get-Command "pwsh").Source # PowerShell 7+ 7 | } elseif (Test-Path "$env:ProgramFiles\PowerShell\7\pwsh.exe") { 8 | "$env:ProgramFiles\PowerShell\7\pwsh.exe" 9 | } else { 10 | "powershell.exe" # Windows PowerShell 11 | } 12 | 13 | try { 14 | Write-Host "`nRequesting administrator privileges..." -ForegroundColor Cyan 15 | $scriptPath = $MyInvocation.MyCommand.Path 16 | $argList = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" 17 | Start-Process -FilePath $pwshPath -Verb RunAs -ArgumentList $argList -Wait 18 | exit 19 | } 20 | catch { 21 | Write-Host "`nError: Administrator privileges required" -ForegroundColor Red 22 | Write-Host "Please run this script from an Administrator PowerShell window" -ForegroundColor Yellow 23 | Write-Host "`nTo do this:" -ForegroundColor Cyan 24 | Write-Host "1. Press Win + X" -ForegroundColor White 25 | Write-Host "2. Click 'Windows Terminal (Admin)' or 'PowerShell (Admin)'" -ForegroundColor White 26 | Write-Host "3. Run the installation command again" -ForegroundColor White 27 | Write-Host "`nPress enter to exit..." 28 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') 29 | exit 1 30 | } 31 | } 32 | 33 | # Set TLS to 1.2 34 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 35 | 36 | # Create temporary directory 37 | $TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) 38 | New-Item -ItemType Directory -Path $TmpDir | Out-Null 39 | 40 | # Cleanup function 41 | function Cleanup { 42 | if (Test-Path $TmpDir) { 43 | Remove-Item -Recurse -Force $TmpDir 44 | } 45 | } 46 | 47 | # Error handler 48 | trap { 49 | Write-Host "Error: $_" -ForegroundColor Red 50 | Cleanup 51 | Write-Host "Press enter to exit..." 52 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') 53 | exit 1 54 | } 55 | 56 | # Detect system architecture 57 | function Get-SystemArch { 58 | if ([Environment]::Is64BitOperatingSystem) { 59 | return "x86_64" 60 | } else { 61 | return "i386" 62 | } 63 | } 64 | 65 | # Download with progress 66 | function Get-FileWithProgress { 67 | param ( 68 | [string]$Url, 69 | [string]$OutputFile 70 | ) 71 | 72 | try { 73 | $webClient = New-Object System.Net.WebClient 74 | $webClient.Headers.Add("User-Agent", "PowerShell Script") 75 | 76 | $webClient.DownloadFile($Url, $OutputFile) 77 | return $true 78 | } 79 | catch { 80 | Write-Host "Failed to download: $_" -ForegroundColor Red 81 | return $false 82 | } 83 | } 84 | 85 | # Main installation function 86 | function Install-CursorModifier { 87 | Write-Host "Starting installation..." -ForegroundColor Cyan 88 | 89 | # Detect architecture 90 | $arch = Get-SystemArch 91 | Write-Host "Detected architecture: $arch" -ForegroundColor Green 92 | 93 | # Set installation directory 94 | $InstallDir = "$env:ProgramFiles\CursorModifier" 95 | if (!(Test-Path $InstallDir)) { 96 | New-Item -ItemType Directory -Path $InstallDir | Out-Null 97 | } 98 | 99 | # Get latest release 100 | try { 101 | $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/yuaotian/go-cursor-help/releases/latest" 102 | Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan 103 | 104 | # Look for Windows binary with our architecture 105 | $version = $latestRelease.tag_name.TrimStart('v') 106 | Write-Host "Version: $version" -ForegroundColor Cyan 107 | $possibleNames = @( 108 | "cursor-id-modifier_${version}_windows_x86_64.exe", 109 | "cursor-id-modifier_${version}_windows_$($arch).exe" 110 | ) 111 | 112 | $asset = $null 113 | foreach ($name in $possibleNames) { 114 | Write-Host "Checking for asset: $name" -ForegroundColor Cyan 115 | $asset = $latestRelease.assets | Where-Object { $_.name -eq $name } 116 | if ($asset) { 117 | Write-Host "Found matching asset: $($asset.name)" -ForegroundColor Green 118 | break 119 | } 120 | } 121 | 122 | if (!$asset) { 123 | Write-Host "`nAvailable assets:" -ForegroundColor Yellow 124 | $latestRelease.assets | ForEach-Object { Write-Host "- $($_.name)" } 125 | throw "Could not find appropriate Windows binary for $arch architecture" 126 | } 127 | 128 | $downloadUrl = $asset.browser_download_url 129 | } 130 | catch { 131 | Write-Host "Failed to get latest release: $_" -ForegroundColor Red 132 | exit 1 133 | } 134 | 135 | # Download binary 136 | Write-Host "`nDownloading latest release..." -ForegroundColor Cyan 137 | $binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe" 138 | 139 | if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) { 140 | exit 1 141 | } 142 | 143 | # Install binary 144 | Write-Host "Installing..." -ForegroundColor Cyan 145 | try { 146 | Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force 147 | 148 | # Add to PATH if not already present 149 | $currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine") 150 | if ($currentPath -notlike "*$InstallDir*") { 151 | [Environment]::SetEnvironmentVariable("Path", "$currentPath;$InstallDir", "Machine") 152 | } 153 | } 154 | catch { 155 | Write-Host "Failed to install: $_" -ForegroundColor Red 156 | exit 1 157 | } 158 | 159 | Write-Host "Installation completed successfully!" -ForegroundColor Green 160 | Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan 161 | 162 | # Run the program 163 | try { 164 | & "$InstallDir\cursor-id-modifier.exe" 165 | if ($LASTEXITCODE -ne 0) { 166 | Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red 167 | exit 1 168 | } 169 | } 170 | catch { 171 | Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red 172 | exit 1 173 | } 174 | } 175 | 176 | # Run installation 177 | try { 178 | Install-CursorModifier 179 | } 180 | catch { 181 | Write-Host "Installation failed: $_" -ForegroundColor Red 182 | Cleanup 183 | Write-Host "Press enter to exit..." 184 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') 185 | exit 1 186 | } 187 | finally { 188 | Cleanup 189 | if ($LASTEXITCODE -ne 0) { 190 | Write-Host "Press enter to exit..." -ForegroundColor Green 191 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') 192 | } 193 | } -------------------------------------------------------------------------------- /scripts/run/cursor_mac_id_modifier.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置错误处理 4 | set -e 5 | 6 | # 颜色定义 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | BLUE='\033[0;34m' 11 | NC='\033[0m' # No Color 12 | 13 | # 日志函数 14 | log_info() { 15 | echo -e "${GREEN}[INFO]${NC} $1" 16 | } 17 | 18 | log_warn() { 19 | echo -e "${YELLOW}[WARN]${NC} $1" 20 | } 21 | 22 | log_error() { 23 | echo -e "${RED}[ERROR]${NC} $1" 24 | } 25 | 26 | log_debug() { 27 | echo -e "${BLUE}[DEBUG]${NC} $1" 28 | } 29 | 30 | # 获取当前用户 31 | get_current_user() { 32 | if [ "$EUID" -eq 0 ]; then 33 | echo "$SUDO_USER" 34 | else 35 | echo "$USER" 36 | fi 37 | } 38 | 39 | CURRENT_USER=$(get_current_user) 40 | if [ -z "$CURRENT_USER" ]; then 41 | log_error "无法获取用户名" 42 | exit 1 43 | fi 44 | 45 | # 定义配置文件路径 46 | STORAGE_FILE="$HOME/Library/Application Support/Cursor/User/globalStorage/storage.json" 47 | BACKUP_DIR="$HOME/Library/Application Support/Cursor/User/globalStorage/backups" 48 | 49 | # 检查权限 50 | check_permissions() { 51 | if [ "$EUID" -ne 0 ]; then 52 | log_error "请使用 sudo 运行此脚本" 53 | echo "示例: sudo $0" 54 | exit 1 55 | fi 56 | } 57 | 58 | # 检查并关闭 Cursor 进程 59 | check_and_kill_cursor() { 60 | log_info "检查 Cursor 进程..." 61 | 62 | local attempt=1 63 | local max_attempts=5 64 | 65 | # 函数:获取进程详细信息 66 | get_process_details() { 67 | local process_name="$1" 68 | log_debug "正在获取 $process_name 进程详细信息:" 69 | ps aux | grep -i "$process_name" | grep -v grep 70 | } 71 | 72 | while [ $attempt -le $max_attempts ]; do 73 | CURSOR_PIDS=$(pgrep -i "cursor" || true) 74 | 75 | if [ -z "$CURSOR_PIDS" ]; then 76 | log_info "未发现运行中的 Cursor 进程" 77 | return 0 78 | fi 79 | 80 | log_warn "发现 Cursor 进程正在运行" 81 | get_process_details "cursor" 82 | 83 | log_warn "尝试关闭 Cursor 进程..." 84 | 85 | if [ $attempt -eq $max_attempts ]; then 86 | log_warn "尝试强制终止进程..." 87 | kill -9 $CURSOR_PIDS 2>/dev/null || true 88 | else 89 | kill $CURSOR_PIDS 2>/dev/null || true 90 | fi 91 | 92 | sleep 1 93 | 94 | if ! pgrep -i "cursor" > /dev/null; then 95 | log_info "Cursor 进程已成功关闭" 96 | return 0 97 | fi 98 | 99 | log_warn "等待进程关闭,尝试 $attempt/$max_attempts..." 100 | ((attempt++)) 101 | done 102 | 103 | log_error "在 $max_attempts 次尝试后仍无法关闭 Cursor 进程" 104 | get_process_details "cursor" 105 | log_error "请手动关闭进程后重试" 106 | exit 1 107 | } 108 | 109 | # 备份配置文件 110 | backup_config() { 111 | if [ ! -f "$STORAGE_FILE" ]; then 112 | log_warn "配置文件不存在,跳过备份" 113 | return 0 114 | fi 115 | 116 | mkdir -p "$BACKUP_DIR" 117 | local backup_file="$BACKUP_DIR/storage.json.backup_$(date +%Y%m%d_%H%M%S)" 118 | 119 | if cp "$STORAGE_FILE" "$backup_file"; then 120 | chmod 644 "$backup_file" 121 | chown "$CURRENT_USER" "$backup_file" 122 | log_info "配置已备份到: $backup_file" 123 | else 124 | log_error "备份失败" 125 | exit 1 126 | fi 127 | } 128 | 129 | # 生成随机 ID 130 | generate_random_id() { 131 | # 生成32字节(64个十六进制字符)的随机数 132 | openssl rand -hex 32 133 | } 134 | 135 | # 生成随机 UUID 136 | generate_uuid() { 137 | uuidgen | tr '[:upper:]' '[:lower:]' 138 | } 139 | 140 | # 生成新的配置 141 | generate_new_config() { 142 | # 将 auth0|user_ 转换为字节数组的十六进制 143 | local prefix_hex=$(echo -n "auth0|user_" | xxd -p) 144 | # 生成随机部分 145 | local random_part=$(generate_random_id) 146 | # 拼接前缀的十六进制和随机部分 147 | local machine_id="${prefix_hex}${random_part}" 148 | 149 | local mac_machine_id=$(generate_random_id) 150 | local device_id=$(generate_uuid | tr '[:upper:]' '[:lower:]') 151 | local sqm_id="{$(generate_uuid | tr '[:lower:]' '[:upper:]')}" 152 | 153 | if [ -f "$STORAGE_FILE" ]; then 154 | # 直接修改现有文件 155 | sed -i '' -e "s/\"telemetry\.machineId\":[[:space:]]*\"[^\"]*\"/\"telemetry.machineId\": \"$machine_id\"/" "$STORAGE_FILE" 156 | sed -i '' -e "s/\"telemetry\.macMachineId\":[[:space:]]*\"[^\"]*\"/\"telemetry.macMachineId\": \"$mac_machine_id\"/" "$STORAGE_FILE" 157 | sed -i '' -e "s/\"telemetry\.devDeviceId\":[[:space:]]*\"[^\"]*\"/\"telemetry.devDeviceId\": \"$device_id\"/" "$STORAGE_FILE" 158 | sed -i '' -e "s/\"telemetry\.sqmId\":[[:space:]]*\"[^\"]*\"/\"telemetry.sqmId\": \"$sqm_id\"/" "$STORAGE_FILE" 159 | else 160 | # 创建新文件 161 | echo "{" > "$STORAGE_FILE" 162 | echo " \"telemetry.machineId\": \"$machine_id\"," >> "$STORAGE_FILE" 163 | echo " \"telemetry.macMachineId\": \"$mac_machine_id\"," >> "$STORAGE_FILE" 164 | echo " \"telemetry.devDeviceId\": \"$device_id\"," >> "$STORAGE_FILE" 165 | echo " \"telemetry.sqmId\": \"$sqm_id\"" >> "$STORAGE_FILE" 166 | echo "}" >> "$STORAGE_FILE" 167 | fi 168 | 169 | chmod 644 "$STORAGE_FILE" 170 | chown "$CURRENT_USER" "$STORAGE_FILE" 171 | 172 | echo 173 | log_info "已更新配置:" 174 | log_debug "machineId: $machine_id" 175 | log_debug "macMachineId: $mac_machine_id" 176 | log_debug "devDeviceId: $device_id" 177 | log_debug "sqmId: $sqm_id" 178 | } 179 | 180 | # 显示文件树结构 181 | show_file_tree() { 182 | local base_dir=$(dirname "$STORAGE_FILE") 183 | echo 184 | log_info "文件结构:" 185 | echo -e "${BLUE}$base_dir${NC}" 186 | echo "├── globalStorage" 187 | echo "│ ├── storage.json (已修改)" 188 | echo "│ └── backups" 189 | 190 | # 列出备份文件 191 | if [ -d "$BACKUP_DIR" ]; then 192 | local backup_files=("$BACKUP_DIR"/*) 193 | if [ ${#backup_files[@]} -gt 0 ]; then 194 | for file in "${backup_files[@]}"; do 195 | if [ -f "$file" ]; then 196 | echo "│ └── $(basename "$file")" 197 | fi 198 | done 199 | else 200 | echo "│ └── (空)" 201 | fi 202 | fi 203 | echo 204 | } 205 | 206 | # 显示公众号信息 207 | show_follow_info() { 208 | echo 209 | echo -e "${GREEN}================================${NC}" 210 | echo -e "${YELLOW} 关注公众号【煎饼果子卷AI】一起交流更多Cursor技巧和AI知识 ${NC}" 211 | echo -e "${GREEN}================================${NC}" 212 | echo 213 | } 214 | 215 | # 主函数 216 | main() { 217 | clear 218 | # 显示 CURSOR Logo 219 | echo -e " 220 | ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ 221 | ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ 222 | ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ 223 | ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ 224 | ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ 225 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ 226 | " 227 | echo -e "${BLUE}================================${NC}" 228 | echo -e "${GREEN} Cursor ID 修改工具 ${NC}" 229 | echo -e "${BLUE}================================${NC}" 230 | echo 231 | 232 | check_permissions 233 | check_and_kill_cursor 234 | backup_config 235 | generate_new_config 236 | 237 | echo 238 | log_info "操作完成!" 239 | show_file_tree 240 | show_follow_info 241 | log_info "请重启 Cursor 以应用新的配置" 242 | echo 243 | } 244 | 245 | # 执行主函数 246 | main 247 | -------------------------------------------------------------------------------- /cmd/cursor-id-modifier/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "runtime" 11 | "runtime/debug" 12 | "strings" 13 | 14 | "github.com/yuaotian/go-cursor-help/internal/config" 15 | "github.com/yuaotian/go-cursor-help/internal/lang" 16 | "github.com/yuaotian/go-cursor-help/internal/process" 17 | "github.com/yuaotian/go-cursor-help/internal/ui" 18 | "github.com/yuaotian/go-cursor-help/pkg/idgen" 19 | 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // Global variables 24 | var ( 25 | version = "dev" 26 | setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode") 27 | showVersion = flag.Bool("v", false, "show version information") 28 | log = logrus.New() 29 | ) 30 | 31 | func main() { 32 | setupErrorRecovery() 33 | handleFlags() 34 | setupLogger() 35 | 36 | username := getCurrentUser() 37 | log.Debug("Running as user:", username) 38 | 39 | // Initialize components 40 | display := ui.NewDisplay(nil) 41 | configManager := initConfigManager(username) 42 | generator := idgen.NewGenerator() 43 | processManager := process.NewManager(nil, log) 44 | 45 | // Check and handle privileges 46 | if err := handlePrivileges(display); err != nil { 47 | return 48 | } 49 | 50 | // Setup display 51 | setupDisplay(display) 52 | 53 | text := lang.GetText() 54 | 55 | // Handle Cursor processes 56 | if err := handleCursorProcesses(display, processManager); err != nil { 57 | return 58 | } 59 | 60 | // Handle configuration 61 | oldConfig := readExistingConfig(display, configManager, text) 62 | newConfig := generateNewConfig(display, generator, oldConfig, text) 63 | 64 | if err := saveConfiguration(display, configManager, newConfig); err != nil { 65 | return 66 | } 67 | 68 | // Show completion messages 69 | showCompletionMessages(display) 70 | 71 | if os.Getenv("AUTOMATED_MODE") != "1" { 72 | waitExit() 73 | } 74 | } 75 | 76 | func setupErrorRecovery() { 77 | defer func() { 78 | if r := recover(); r != nil { 79 | log.Errorf("Panic recovered: %v\n", r) 80 | debug.PrintStack() 81 | waitExit() 82 | } 83 | }() 84 | } 85 | 86 | func handleFlags() { 87 | flag.Parse() 88 | if *showVersion { 89 | fmt.Printf("Cursor ID Modifier v%s\n", version) 90 | os.Exit(0) 91 | } 92 | } 93 | 94 | func setupLogger() { 95 | log.SetFormatter(&logrus.TextFormatter{ 96 | FullTimestamp: true, 97 | DisableLevelTruncation: true, 98 | PadLevelText: true, 99 | }) 100 | log.SetLevel(logrus.InfoLevel) 101 | } 102 | 103 | func getCurrentUser() string { 104 | if username := os.Getenv("SUDO_USER"); username != "" { 105 | return username 106 | } 107 | 108 | user, err := user.Current() 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | return user.Username 113 | } 114 | 115 | func initConfigManager(username string) *config.Manager { 116 | configManager, err := config.NewManager(username) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | return configManager 121 | } 122 | 123 | func handlePrivileges(display *ui.Display) error { 124 | isAdmin, err := checkAdminPrivileges() 125 | if err != nil { 126 | log.Error(err) 127 | waitExit() 128 | return err 129 | } 130 | 131 | if !isAdmin { 132 | if runtime.GOOS == "windows" { 133 | return handleWindowsPrivileges(display) 134 | } 135 | display.ShowPrivilegeError( 136 | lang.GetText().PrivilegeError, 137 | lang.GetText().RunWithSudo, 138 | lang.GetText().SudoExample, 139 | ) 140 | waitExit() 141 | return fmt.Errorf("insufficient privileges") 142 | } 143 | return nil 144 | } 145 | 146 | func handleWindowsPrivileges(display *ui.Display) error { 147 | message := "\nRequesting administrator privileges..." 148 | if lang.GetCurrentLanguage() == lang.CN { 149 | message = "\n请求管理员权限..." 150 | } 151 | fmt.Println(message) 152 | 153 | if err := selfElevate(); err != nil { 154 | log.Error(err) 155 | display.ShowPrivilegeError( 156 | lang.GetText().PrivilegeError, 157 | lang.GetText().RunAsAdmin, 158 | lang.GetText().RunWithSudo, 159 | lang.GetText().SudoExample, 160 | ) 161 | waitExit() 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | func setupDisplay(display *ui.Display) { 168 | if err := display.ClearScreen(); err != nil { 169 | log.Warn("Failed to clear screen:", err) 170 | } 171 | display.ShowLogo() 172 | fmt.Println() 173 | } 174 | 175 | func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error { 176 | if os.Getenv("AUTOMATED_MODE") == "1" { 177 | log.Debug("Running in automated mode, skipping Cursor process closing") 178 | return nil 179 | } 180 | 181 | display.ShowProgress("Closing Cursor...") 182 | log.Debug("Attempting to close Cursor processes") 183 | 184 | if err := processManager.KillCursorProcesses(); err != nil { 185 | log.Error("Failed to close Cursor:", err) 186 | display.StopProgress() 187 | display.ShowError("Failed to close Cursor. Please close it manually and try again.") 188 | waitExit() 189 | return err 190 | } 191 | 192 | if processManager.IsCursorRunning() { 193 | log.Error("Cursor processes still detected after closing") 194 | display.StopProgress() 195 | display.ShowError("Failed to close Cursor completely. Please close it manually and try again.") 196 | waitExit() 197 | return fmt.Errorf("cursor still running") 198 | } 199 | 200 | log.Debug("Successfully closed all Cursor processes") 201 | display.StopProgress() 202 | fmt.Println() 203 | return nil 204 | } 205 | 206 | func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig { 207 | fmt.Println() 208 | display.ShowProgress(text.ReadingConfig) 209 | oldConfig, err := configManager.ReadConfig() 210 | if err != nil { 211 | log.Warn("Failed to read existing config:", err) 212 | oldConfig = nil 213 | } 214 | display.StopProgress() 215 | fmt.Println() 216 | return oldConfig 217 | } 218 | 219 | func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig { 220 | display.ShowProgress(text.GeneratingIds) 221 | newConfig := &config.StorageConfig{} 222 | 223 | if machineID, err := generator.GenerateMachineID(); err != nil { 224 | log.Fatal("Failed to generate machine ID:", err) 225 | } else { 226 | newConfig.TelemetryMachineId = machineID 227 | } 228 | 229 | if macMachineID, err := generator.GenerateMacMachineID(); err != nil { 230 | log.Fatal("Failed to generate MAC machine ID:", err) 231 | } else { 232 | newConfig.TelemetryMacMachineId = macMachineID 233 | } 234 | 235 | if deviceID, err := generator.GenerateDeviceID(); err != nil { 236 | log.Fatal("Failed to generate device ID:", err) 237 | } else { 238 | newConfig.TelemetryDevDeviceId = deviceID 239 | } 240 | 241 | if oldConfig != nil && oldConfig.TelemetrySqmId != "" { 242 | newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId 243 | } else if sqmID, err := generator.GenerateSQMID(); err != nil { 244 | log.Fatal("Failed to generate SQM ID:", err) 245 | } else { 246 | newConfig.TelemetrySqmId = sqmID 247 | } 248 | 249 | display.StopProgress() 250 | fmt.Println() 251 | return newConfig 252 | } 253 | 254 | func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error { 255 | display.ShowProgress("Saving configuration...") 256 | if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil { 257 | log.Error(err) 258 | waitExit() 259 | return err 260 | } 261 | display.StopProgress() 262 | fmt.Println() 263 | return nil 264 | } 265 | 266 | func showCompletionMessages(display *ui.Display) { 267 | display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage) 268 | fmt.Println() 269 | 270 | message := "Operation completed!" 271 | if lang.GetCurrentLanguage() == lang.CN { 272 | message = "操作完成!" 273 | } 274 | display.ShowInfo(message) 275 | } 276 | 277 | func waitExit() { 278 | fmt.Print(lang.GetText().PressEnterToExit) 279 | os.Stdout.Sync() 280 | bufio.NewReader(os.Stdin).ReadString('\n') 281 | } 282 | 283 | func checkAdminPrivileges() (bool, error) { 284 | switch runtime.GOOS { 285 | case "windows": 286 | cmd := exec.Command("net", "session") 287 | return cmd.Run() == nil, nil 288 | 289 | case "darwin", "linux": 290 | currentUser, err := user.Current() 291 | if err != nil { 292 | return false, fmt.Errorf("failed to get current user: %w", err) 293 | } 294 | return currentUser.Uid == "0", nil 295 | 296 | default: 297 | return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 298 | } 299 | } 300 | 301 | func selfElevate() error { 302 | os.Setenv("AUTOMATED_MODE", "1") 303 | 304 | switch runtime.GOOS { 305 | case "windows": 306 | verb := "runas" 307 | exe, _ := os.Executable() 308 | cwd, _ := os.Getwd() 309 | args := strings.Join(os.Args[1:], " ") 310 | 311 | cmd := exec.Command("cmd", "/C", "start", verb, exe, args) 312 | cmd.Dir = cwd 313 | return cmd.Run() 314 | 315 | case "darwin", "linux": 316 | exe, err := os.Executable() 317 | if err != nil { 318 | return err 319 | } 320 | 321 | cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...) 322 | cmd.Stdin = os.Stdin 323 | cmd.Stdout = os.Stdout 324 | cmd.Stderr = os.Stderr 325 | return cmd.Run() 326 | 327 | default: 328 | return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /.github/workflows/auto-tag-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow requires Ubuntu 22.04 or 24.04 2 | 3 | name: Auto Tag & Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - main 10 | tags: 11 | - "v*" 12 | paths-ignore: 13 | - "**.md" 14 | - "LICENSE" 15 | - ".gitignore" 16 | workflow_call: {} 17 | 18 | permissions: 19 | contents: write 20 | packages: write 21 | actions: write 22 | 23 | jobs: 24 | pre_job: 25 | runs-on: ubuntu-22.04 26 | outputs: 27 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 28 | steps: 29 | - id: skip_check 30 | uses: fkirc/skip-duplicate-actions@v5.3.0 31 | with: 32 | cancel_others: "true" 33 | concurrent_skipping: "same_content" 34 | 35 | auto-tag-release: 36 | needs: pre_job 37 | if: | 38 | needs.pre_job.outputs.should_skip != 'true' || 39 | startsWith(github.ref, 'refs/tags/v') 40 | runs-on: ubuntu-22.04 41 | timeout-minutes: 15 42 | outputs: 43 | version: ${{ steps.get_latest_tag.outputs.version }} 44 | concurrency: 45 | group: ${{ github.workflow }}-${{ github.ref }} 46 | cancel-in-progress: true 47 | 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v3 51 | with: 52 | fetch-depth: 0 53 | lfs: true 54 | submodules: recursive 55 | 56 | - name: Setup Go 57 | uses: actions/setup-go@v3 58 | with: 59 | go-version: "1.21" 60 | check-latest: true 61 | cache: true 62 | 63 | - name: Cache 64 | uses: actions/cache@v3 65 | with: 66 | path: | 67 | ~/.cache/go-build 68 | ~/go/pkg/mod 69 | ~/.cache/git 70 | key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }} 71 | restore-keys: | 72 | ${{ runner.os }}-build- 73 | ${{ runner.os }}- 74 | 75 | # 只在非tag推送时执行自动打tag 76 | - name: Get latest tag 77 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 78 | id: get_latest_tag 79 | run: | 80 | set -euo pipefail 81 | git fetch --tags --force || { 82 | echo "::error::Failed to fetch tags" 83 | exit 1 84 | } 85 | latest_tag=$(git tag -l 'v*' --sort=-v:refname | head -n 1) 86 | if [ -z "$latest_tag" ]; then 87 | new_version="v0.1.0" 88 | else 89 | major=$(echo $latest_tag | cut -d. -f1) 90 | minor=$(echo $latest_tag | cut -d. -f2) 91 | patch=$(echo $latest_tag | cut -d. -f3) 92 | new_patch=$((patch + 1)) 93 | new_version="$major.$minor.$new_patch" 94 | fi 95 | echo "version=$new_version" >> "$GITHUB_OUTPUT" 96 | echo "Generated version: $new_version" 97 | 98 | - name: Validate version 99 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 100 | run: | 101 | set -euo pipefail 102 | new_tag="${{ steps.get_latest_tag.outputs.version }}" 103 | echo "Validating version: $new_tag" 104 | if [[ ! $new_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 105 | echo "::error::Invalid version format: $new_tag" 106 | exit 1 107 | fi 108 | major=$(echo $new_tag | cut -d. -f1 | tr -d 'v') 109 | minor=$(echo $new_tag | cut -d. -f2) 110 | patch=$(echo $new_tag | cut -d. -f3) 111 | if [[ $major -gt 99 || $minor -gt 99 || $patch -gt 999 ]]; then 112 | echo "::error::Version numbers out of valid range" 113 | exit 1 114 | fi 115 | echo "Version validation passed" 116 | 117 | - name: Create new tag 118 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | run: | 122 | new_tag=${{ steps.get_latest_tag.outputs.version }} 123 | git config --global user.name 'github-actions[bot]' 124 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 125 | git tag -a $new_tag -m "Release $new_tag" 126 | git push origin $new_tag 127 | 128 | # 在 Run GoReleaser 之前添加配置检查步骤 129 | - name: Check GoReleaser config 130 | run: | 131 | if [ ! -f ".goreleaser.yml" ] && [ ! -f ".goreleaser.yaml" ]; then 132 | echo "::error::GoReleaser configuration file not found" 133 | exit 1 134 | fi 135 | 136 | # 添加依赖检查步骤 137 | - name: Check Dependencies 138 | run: | 139 | go mod verify 140 | go mod download 141 | # 如果使用 vendor 模式,则执行以下命令 142 | if [ -d "vendor" ]; then 143 | go mod vendor 144 | fi 145 | 146 | # 添加构建环境准备步骤 147 | - name: Prepare Build Environment 148 | run: | 149 | echo "Building version: ${VERSION:-development}" 150 | echo "GOOS=${GOOS:-$(go env GOOS)}" >> $GITHUB_ENV 151 | echo "GOARCH=${GOARCH:-$(go env GOARCH)}" >> $GITHUB_ENV 152 | echo "GO111MODULE=on" >> $GITHUB_ENV 153 | 154 | # 添加清理步骤 155 | - name: Cleanup workspace 156 | run: | 157 | rm -rf /tmp/go/ 158 | rm -rf .cache/ 159 | rm -rf dist/ 160 | git clean -fdx 161 | git status 162 | 163 | # 修改 GoReleaser 步骤 164 | - name: Run GoReleaser 165 | if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} 166 | uses: goreleaser/goreleaser-action@v3 167 | with: 168 | distribution: goreleaser 169 | version: latest 170 | args: release --clean --timeout 60m 171 | env: 172 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 173 | VERSION: ${{ steps.get_latest_tag.outputs.version }} 174 | CGO_ENABLED: 0 175 | GOPATH: /tmp/go 176 | GOROOT: ${{ env.GOROOT }} 177 | GOCACHE: /tmp/.cache/go-build 178 | GOMODCACHE: /tmp/go/pkg/mod 179 | GORELEASER_DEBUG: 1 180 | GORELEASER_CURRENT_TAG: ${{ steps.get_latest_tag.outputs.version }} 181 | # 添加额外的构建信息 182 | BUILD_TIME: ${{ steps.get_latest_tag.outputs.version }} 183 | BUILD_COMMIT: ${{ github.sha }} 184 | 185 | # 优化 vendor 同步步骤 186 | - name: Sync vendor directory 187 | run: | 188 | echo "Syncing vendor directory..." 189 | go mod tidy 190 | go mod vendor 191 | go mod verify 192 | # 验证 vendor 目录 193 | if [ -d "vendor" ]; then 194 | echo "Verifying vendor directory..." 195 | go mod verify 196 | # 检查是否有未跟踪的文件 197 | if [ -n "$(git status --porcelain vendor/)" ]; then 198 | echo "Warning: Vendor directory has uncommitted changes" 199 | git status vendor/ 200 | fi 201 | fi 202 | 203 | # 添加错误检查步骤 204 | - name: Check GoReleaser Output 205 | if: failure() 206 | run: | 207 | echo "::group::GoReleaser Debug Info" 208 | cat dist/artifacts.json || true 209 | echo "::endgroup::" 210 | 211 | echo "::group::GoReleaser Config" 212 | cat .goreleaser.yml 213 | echo "::endgroup::" 214 | 215 | echo "::group::Environment Info" 216 | go version 217 | go env 218 | echo "::endgroup::" 219 | 220 | - name: Set Release Version 221 | if: startsWith(github.ref, 'refs/tags/v') 222 | run: | 223 | echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 224 | 225 | # 改进验证步骤 226 | - name: Verify Release 227 | if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} 228 | run: | 229 | echo "Verifying release artifacts..." 230 | if [ ! -d "dist" ]; then 231 | echo "::error::Release artifacts not found" 232 | exit 1 233 | fi 234 | # 验证生成的二进制文件 235 | for file in dist/cursor-id-modifier_*; do 236 | if [ -f "$file" ]; then 237 | echo "Verifying: $file" 238 | if [[ "$file" == *.exe ]]; then 239 | # Windows 二进制文件检查 240 | if ! [ -x "$file" ]; then 241 | echo "::error::$file is not executable" 242 | exit 1 243 | fi 244 | else 245 | # Unix 二进制文件检查 246 | if ! [ -x "$file" ]; then 247 | echo "::error::$file is not executable" 248 | exit 1 249 | fi 250 | fi 251 | fi 252 | done 253 | 254 | - name: Notify on failure 255 | if: failure() 256 | run: | 257 | echo "::error::Release process failed" 258 | 259 | # 修改构建摘要步骤 260 | - name: Build Summary 261 | if: always() 262 | run: | 263 | echo "## Build Summary" >> $GITHUB_STEP_SUMMARY 264 | echo "- Go Version: $(go version)" >> $GITHUB_STEP_SUMMARY 265 | echo "- Release Version: ${VERSION:-N/A}" >> $GITHUB_STEP_SUMMARY 266 | echo "- GPG Signing: Disabled" >> $GITHUB_STEP_SUMMARY 267 | echo "- Build Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY 268 | 269 | if [ -d "dist" ]; then 270 | echo "### Generated Artifacts" >> $GITHUB_STEP_SUMMARY 271 | ls -lh dist/ | awk '{print "- "$9" ("$5")"}' >> $GITHUB_STEP_SUMMARY 272 | fi 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Cursor Free Trial Reset Tool 2 | 3 |
12 |
13 | | 43 | 44 | **Windows** ✅ 45 | - x64 (64-bit) 46 | - x86 (32-bit) 47 | 48 | | 49 |50 | 51 | **macOS** ✅ 52 | - Intel (x64) 53 | - Apple Silicon (M1/M2) 54 | 55 | | 56 |57 | 58 | **Linux** ✅ 59 | - x64 (64-bit) 60 | - x86 (32-bit) 61 | - ARM64 62 | 63 | | 64 |
|
192 | 个人微信 193 | ![]() 194 | JavaRookie666 195 | |
196 |
197 | 微信交流群 198 | ![]() 199 | 7天内(1月15日前)有效 200 | |
201 |
202 | 公众号 203 | ![]() 204 | 获取更多AI开发资源 205 | |
206 |
| 215 | 216 | **Windows** ✅ 217 | - x64 & x86 218 | 219 | | 220 |221 | 222 | **macOS** ✅ 223 | - Intel & M-series 224 | 225 | | 226 |227 | 228 | **Linux** ✅ 229 | - x64 & ARM64 230 | 231 | | 232 |