├── img
├── etc.png
├── alipay.png
├── pwsh_1.png
├── pwsh_2.png
├── qun-16.png
├── qun-17.png
├── qun-18.jpg
├── wx_me.png
├── wx_zsm.jpg
├── wx_group7.jpg
├── wx_public.jpg
├── wx_zsm2.png
├── run_success.png
├── wx_public_2.png
├── zanzhu
│ └── twillot.png
└── alipay_scan_pay.jpg
├── scripts
├── git-actions.sh
├── build_all.bat
├── build_all.sh
├── install.sh
├── install.ps1
├── cursor_id_modifier.pot
└── run
│ ├── cursor_mac_id_modifier_new.sh
│ └── cursor_win_id_modifier_old.ps1
├── go.mod
├── .gitignore
├── internal
├── ui
│ ├── logo.go
│ ├── display.go
│ └── spinner.go
├── config
│ └── config.go
├── lang
│ └── lang.go
└── process
│ └── manager.go
├── LICENSE
├── .goreleaser.yml
├── go.sum
├── pkg
└── idgen
│ └── generator.go
├── cmd
└── cursor-id-modifier
│ └── main.go
├── .github
└── workflows
│ └── auto-tag-release.yml
├── process_cursor_links.py
├── README_CN.md
├── README_JP.md
└── README.md
/img/etc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/etc.png
--------------------------------------------------------------------------------
/img/alipay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/alipay.png
--------------------------------------------------------------------------------
/img/pwsh_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/pwsh_1.png
--------------------------------------------------------------------------------
/img/pwsh_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/pwsh_2.png
--------------------------------------------------------------------------------
/img/qun-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/qun-16.png
--------------------------------------------------------------------------------
/img/qun-17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/qun-17.png
--------------------------------------------------------------------------------
/img/qun-18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/qun-18.jpg
--------------------------------------------------------------------------------
/img/wx_me.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_me.png
--------------------------------------------------------------------------------
/img/wx_zsm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_zsm.jpg
--------------------------------------------------------------------------------
/img/wx_group7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_group7.jpg
--------------------------------------------------------------------------------
/img/wx_public.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_public.jpg
--------------------------------------------------------------------------------
/img/wx_zsm2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_zsm2.png
--------------------------------------------------------------------------------
/img/run_success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/run_success.png
--------------------------------------------------------------------------------
/img/wx_public_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/wx_public_2.png
--------------------------------------------------------------------------------
/img/zanzhu/twillot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/zanzhu/twillot.png
--------------------------------------------------------------------------------
/img/alipay_scan_pay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dext7r/go-cursor-help/master/img/alipay_scan_pay.jpg
--------------------------------------------------------------------------------
/scripts/git-actions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | REPO_DIR="$PWD"
3 | LOCALES_DIR="$REPO_DIR/locales"
4 | msginit -i cursor_id_modifier.pot -o $LOCALES_DIR/en_US/LC_MESSAGES/cursor_id_modifier.po -l en_US
5 | for lang in en_US zh_CN; do
6 | cd $LOCALES_DIR/$lang/LC_MESSAGES
7 | msgfmt -o cursor_id_modifier.mo cursor_id_modifier.po
8 | done
9 |
10 |
--------------------------------------------------------------------------------
/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
43 |
44 | .cunzhi*/
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/cursor_id_modifier.pot:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: cursor_id_modifier\n"
4 | "POT-Creation-Date: 2025-04-25 12:00+0000\n"
5 | "PO-Revision-Date: 2025-04-25 12:00+0000\n"
6 | "Language-Team: None\n"
7 | "MIME-Version: 1.0\n"
8 | "Content-Type: text/plain; charset=UTF-8\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 |
11 | msgid "Error: No translation file found for domain 'cursor_id_modifier' in {}/zh_CN/LC_MESSAGES/"
12 | msgstr ""
13 |
14 | msgid "========== Cursor ID modification tool log start {} =========="
15 | msgstr ""
16 |
17 | msgid "[INFO] {} {}"
18 | msgstr ""
19 |
20 | msgid "[WARN] {} {}"
21 | msgstr ""
22 |
23 | msgid "[ERROR] {} {}"
24 | msgstr ""
25 |
26 | msgid "[DEBUG] {} {}"
27 | msgstr ""
28 |
29 | msgid "[CMD] {} Executing command: {}"
30 | msgstr ""
31 |
32 | msgid "[CMD] {}:"
33 | msgstr ""
34 |
35 | msgid "Unable to get username"
36 | msgstr ""
37 |
38 | msgid "Finding Cursor installation path..."
39 | msgstr ""
40 |
41 | msgid "Found Cursor installation path: {}"
42 | msgstr ""
43 |
44 | msgid "Found Cursor via which: {}"
45 | msgstr ""
46 |
47 | msgid "Cursor executable not found, will try using config directory"
48 | msgstr ""
49 |
50 | msgid "Found Cursor via search: {}"
51 | msgstr ""
52 |
53 | msgid "Finding Cursor resource directory..."
54 | msgstr ""
55 |
56 | msgid "Found Cursor resource directory: {}"
57 | msgstr ""
58 |
59 | msgid "Found resource directory via binary path: {}"
60 | msgstr ""
61 |
62 | msgid "Cursor resource directory not found"
63 | msgstr ""
64 |
65 | msgid "Please run this script with sudo"
66 | msgstr ""
67 |
68 | msgid "Example: sudo {}"
69 | msgstr ""
70 |
71 | msgid "Checking Cursor processes..."
72 | msgstr ""
73 |
74 | msgid "Getting process details for {}:"
75 | msgstr ""
76 |
77 | msgid "No running Cursor processes found"
78 | msgstr ""
79 |
80 | msgid "Found running Cursor processes"
81 | msgstr ""
82 |
83 | msgid "Attempting to terminate Cursor processes..."
84 | msgstr ""
85 |
86 | msgid "Attempting to forcefully terminate processes..."
87 | msgstr ""
88 |
89 | msgid "Waiting for processes to terminate, attempt {}/{}..."
90 | msgstr ""
91 |
92 | msgid "Cursor processes successfully terminated"
93 | msgstr ""
94 |
95 | msgid "Unable to terminate Cursor processes after {} attempts"
96 | msgstr ""
97 |
98 | msgid "Please manually terminate the processes and try again"
99 | msgstr ""
100 |
101 | msgid "Configuration file does not exist, skipping backup"
102 | msgstr ""
103 |
104 | msgid "Configuration backed up to: {}"
105 | msgstr ""
106 |
107 | msgid "Backup failed"
108 | msgstr ""
109 |
110 | msgid "File does not exist: {}"
111 | msgstr ""
112 |
113 | msgid "Unable to modify file permissions: {}"
114 | msgstr ""
115 |
116 | msgid "Generated temporary file is empty"
117 | msgstr ""
118 |
119 | msgid "Unable to write to file: {}"
120 | msgstr ""
121 |
122 | msgid "Machine code reset options"
123 | msgstr ""
124 |
125 | msgid "Do you need to reset the machine code? (Usually, modifying JS files is sufficient):"
126 | msgstr ""
127 |
128 | msgid "Don't reset - only modify JS files"
129 | msgstr ""
130 |
131 | msgid "Reset - modify both config file and machine code"
132 | msgstr ""
133 |
134 | msgid "[INPUT_DEBUG] Machine code reset option selected: {}"
135 | msgstr ""
136 |
137 | msgid "You chose to reset the machine code"
138 | msgstr ""
139 |
140 | msgid "Found existing configuration file: {}"
141 | msgstr ""
142 |
143 | msgid "Setting new device and machine IDs..."
144 | msgstr ""
145 |
146 | msgid "New device ID: {}"
147 | msgstr ""
148 |
149 | msgid "New machine ID: {}"
150 | msgstr ""
151 |
152 | msgid "Configuration file modified successfully"
153 | msgstr ""
154 |
155 | msgid "Configuration file modification failed"
156 | msgstr ""
157 |
158 | msgid "Configuration file not found, this is normal, skipping ID modification"
159 | msgstr ""
160 |
161 | msgid "You chose not to reset the machine code, will only modify JS files"
162 | msgstr ""
163 |
164 | msgid "Configuration processing completed"
165 | msgstr ""
166 |
167 | msgid "Finding Cursor's JS files..."
168 | msgstr ""
169 |
170 | msgid "Searching for JS files in resource directory: {}"
171 | msgstr ""
172 |
173 | msgid "Found JS file: {}"
174 | msgstr ""
175 |
176 | msgid "No JS files found in resource directory, trying other directories..."
177 | msgstr ""
178 |
179 | msgid "Searching directory: {}"
180 | msgstr ""
181 |
182 | msgid "No modifiable JS files found"
183 | msgstr ""
184 |
185 | msgid "Found {} JS files to modify"
186 | msgstr ""
187 |
188 | msgid "Starting to modify Cursor's JS files..."
189 | msgstr ""
190 |
191 | msgid "Unable to find modifiable JS files"
192 | msgstr ""
193 |
194 | msgid "Processing file: {}"
195 | msgstr ""
196 |
197 | msgid "Unable to create backup for file: {}"
198 | msgstr ""
199 |
200 | msgid "Found x-cursor-checksum setting code"
201 | msgstr ""
202 |
203 | msgid "Successfully modified x-cursor-checksum setting code"
204 | msgstr ""
205 |
206 | msgid "Failed to modify x-cursor-checksum setting code"
207 | msgstr ""
208 |
209 | msgid "Found IOPlatformUUID keyword"
210 | msgstr ""
211 |
212 | msgid "Successfully injected randomUUID call into a$ function"
213 | msgstr ""
214 |
215 | msgid "Failed to modify a$ function"
216 | msgstr ""
217 |
218 | msgid "Successfully injected randomUUID call into v5 function"
219 | msgstr ""
220 |
221 | msgid "Failed to modify v5 function"
222 | msgstr ""
223 |
224 | msgid "Completed universal modification"
225 | msgstr ""
226 |
227 | msgid "File already contains custom injection code, skipping modification"
228 | msgstr ""
229 |
230 | msgid "Completed most universal injection"
231 | msgstr ""
232 |
233 | msgid "File has already been modified, skipping modification"
234 | msgstr ""
235 |
236 | msgid "Failed to modify any JS files"
237 | msgstr ""
238 |
239 | msgid "Successfully modified {} JS files"
240 | msgstr ""
241 |
242 | msgid "Disabling Cursor auto-update..."
243 | msgstr ""
244 |
245 | msgid "Found update configuration file: {}"
246 | msgstr ""
247 |
248 | msgid "Disabled update configuration file: {}"
249 | msgstr ""
250 |
251 | msgid "Found updater: {}"
252 | msgstr ""
253 |
254 | msgid "Disabled updater: {}"
255 | msgstr ""
256 |
257 | msgid "No update configuration files or updaters found"
258 | msgstr ""
259 |
260 | msgid "Successfully disabled auto-update"
261 | msgstr ""
262 |
263 | msgid "You selected: {}"
264 | msgstr ""
265 |
266 | msgid "This script only supports Linux systems"
267 | msgstr ""
268 |
269 | msgid "Script started..."
270 | msgstr ""
271 |
272 | msgid "System information: {}"
273 | msgstr ""
274 |
275 | msgid "Current user: {}"
276 | msgstr ""
277 |
278 | msgid "System version information"
279 | msgstr ""
280 |
281 | msgid "Cursor Linux startup tool"
282 | msgstr ""
283 |
284 | msgid "Important notice"
285 | msgstr ""
286 |
287 | msgid "This tool prioritizes modifying JS files, which is safer and more reliable"
288 | msgstr ""
289 |
290 | msgid "Modifying Cursor JS files..."
291 | msgstr ""
292 |
293 | msgid "JS files modified successfully!"
294 | msgstr ""
295 |
296 | msgid "JS file modification failed, but configuration file modification may have succeeded"
297 | msgstr ""
298 |
299 | msgid "If Cursor still indicates the device is disabled after restarting, please rerun this script"
300 | msgstr ""
301 |
302 | msgid "Please restart Cursor to apply the new configuration"
303 | msgstr ""
304 |
305 | msgid "Follow the WeChat public account [Pancake AI] to discuss more Cursor tips and AI knowledge (script is free, join the group via the public account for more tips and experts)"
306 | msgstr ""
307 |
308 | msgid "Script execution completed"
309 | msgstr ""
310 |
311 | msgid "========== Cursor ID modification tool log end {} =========="
312 | msgstr ""
313 |
314 | msgid "Detailed log saved to: {}"
315 | msgstr ""
316 |
317 | msgid "If you encounter issues, please provide this log file to the developer for troubleshooting"
318 | msgstr ""
319 |
--------------------------------------------------------------------------------
/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/sirupsen/logrus"
15 |
16 | "github.com/yuaotian/go-cursor-help/internal/config"
17 | "github.com/yuaotian/go-cursor-help/internal/lang"
18 | "github.com/yuaotian/go-cursor-help/internal/process"
19 | "github.com/yuaotian/go-cursor-help/internal/ui"
20 | "github.com/yuaotian/go-cursor-help/pkg/idgen"
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 | // Place defer at the beginning of main to ensure it can catch panics from all subsequent function calls
33 | defer func() {
34 | if r := recover(); r != nil {
35 | log.Errorf("Panic recovered: %v\n", r)
36 | debug.PrintStack()
37 | waitExit()
38 | }
39 | }()
40 |
41 | handleFlags()
42 | setupLogger()
43 |
44 | username := getCurrentUser()
45 | log.Debug("Running as user:", username)
46 |
47 | // Initialize components
48 | display := ui.NewDisplay(nil)
49 | configManager := initConfigManager(username)
50 | generator := idgen.NewGenerator()
51 | processManager := process.NewManager(nil, log)
52 |
53 | // Check and handle privileges
54 | if err := handlePrivileges(display); err != nil {
55 | return
56 | }
57 |
58 | // Setup display
59 | setupDisplay(display)
60 |
61 | text := lang.GetText()
62 |
63 | // Handle Cursor processes
64 | if err := handleCursorProcesses(display, processManager); err != nil {
65 | return
66 | }
67 |
68 | // Handle configuration
69 | oldConfig := readExistingConfig(display, configManager, text)
70 | newConfig := generateNewConfig(display, generator, oldConfig, text)
71 |
72 | if err := saveConfiguration(display, configManager, newConfig); err != nil {
73 | return
74 | }
75 |
76 | // Show completion messages
77 | showCompletionMessages(display)
78 |
79 | if os.Getenv("AUTOMATED_MODE") != "1" {
80 | waitExit()
81 | }
82 | }
83 |
84 | func handleFlags() {
85 | flag.Parse()
86 | if *showVersion {
87 | fmt.Printf("Cursor ID Modifier v%s\n", version)
88 | os.Exit(0)
89 | }
90 | }
91 |
92 | func setupLogger() {
93 | log.SetFormatter(&logrus.TextFormatter{
94 | FullTimestamp: true,
95 | DisableLevelTruncation: true,
96 | PadLevelText: true,
97 | })
98 | log.SetLevel(logrus.InfoLevel)
99 | }
100 |
101 | func getCurrentUser() string {
102 | if username := os.Getenv("SUDO_USER"); username != "" {
103 | return username
104 | }
105 |
106 | user, err := user.Current()
107 | if err != nil {
108 | log.Fatal(err)
109 | }
110 | return user.Username
111 | }
112 |
113 | func initConfigManager(username string) *config.Manager {
114 | configManager, err := config.NewManager(username)
115 | if err != nil {
116 | log.Fatal(err)
117 | }
118 | return configManager
119 | }
120 |
121 | func handlePrivileges(display *ui.Display) error {
122 | isAdmin, err := checkAdminPrivileges()
123 | if err != nil {
124 | log.Error(err)
125 | waitExit()
126 | return err
127 | }
128 |
129 | if !isAdmin {
130 | if runtime.GOOS == "windows" {
131 | return handleWindowsPrivileges(display)
132 | }
133 | display.ShowPrivilegeError(
134 | lang.GetText().PrivilegeError,
135 | lang.GetText().RunWithSudo,
136 | lang.GetText().SudoExample,
137 | )
138 | waitExit()
139 | return fmt.Errorf("insufficient privileges")
140 | }
141 | return nil
142 | }
143 |
144 | func handleWindowsPrivileges(display *ui.Display) error {
145 | message := "\nRequesting administrator privileges..."
146 | if lang.GetCurrentLanguage() == lang.CN {
147 | message = "\n请求管理员权限..."
148 | }
149 | fmt.Println(message)
150 |
151 | if err := selfElevate(); err != nil {
152 | log.Error(err)
153 | display.ShowPrivilegeError(
154 | lang.GetText().PrivilegeError,
155 | lang.GetText().RunAsAdmin,
156 | lang.GetText().RunWithSudo,
157 | lang.GetText().SudoExample,
158 | )
159 | waitExit()
160 | return err
161 | }
162 | return nil
163 | }
164 |
165 | func setupDisplay(display *ui.Display) {
166 | if err := display.ClearScreen(); err != nil {
167 | log.Warn("Failed to clear screen:", err)
168 | }
169 | display.ShowLogo()
170 | fmt.Println()
171 | }
172 |
173 | func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error {
174 | if os.Getenv("AUTOMATED_MODE") == "1" {
175 | log.Debug("Running in automated mode, skipping Cursor process closing")
176 | return nil
177 | }
178 |
179 | display.ShowProgress("Closing Cursor...")
180 | log.Debug("Attempting to close Cursor processes")
181 |
182 | if err := processManager.KillCursorProcesses(); err != nil {
183 | log.Error("Failed to close Cursor:", err)
184 | display.StopProgress()
185 | display.ShowError("Failed to close Cursor. Please close it manually and try again.")
186 | waitExit()
187 | return err
188 | }
189 |
190 | if processManager.IsCursorRunning() {
191 | log.Error("Cursor processes still detected after closing")
192 | display.StopProgress()
193 | display.ShowError("Failed to close Cursor completely. Please close it manually and try again.")
194 | waitExit()
195 | return fmt.Errorf("cursor still running")
196 | }
197 |
198 | log.Debug("Successfully closed all Cursor processes")
199 | display.StopProgress()
200 | fmt.Println()
201 | return nil
202 | }
203 |
204 | func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig {
205 | fmt.Println()
206 | display.ShowProgress(text.ReadingConfig)
207 | oldConfig, err := configManager.ReadConfig()
208 | if err != nil {
209 | log.Warn("Failed to read existing config:", err)
210 | oldConfig = nil
211 | }
212 | display.StopProgress()
213 | fmt.Println()
214 | return oldConfig
215 | }
216 |
217 | func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig {
218 | display.ShowProgress(text.GeneratingIds)
219 | newConfig := &config.StorageConfig{}
220 |
221 | if machineID, err := generator.GenerateMachineID(); err != nil {
222 | log.Fatal("Failed to generate machine ID:", err)
223 | } else {
224 | newConfig.TelemetryMachineId = machineID
225 | }
226 |
227 | if macMachineID, err := generator.GenerateMacMachineID(); err != nil {
228 | log.Fatal("Failed to generate MAC machine ID:", err)
229 | } else {
230 | newConfig.TelemetryMacMachineId = macMachineID
231 | }
232 |
233 | if deviceID, err := generator.GenerateDeviceID(); err != nil {
234 | log.Fatal("Failed to generate device ID:", err)
235 | } else {
236 | newConfig.TelemetryDevDeviceId = deviceID
237 | }
238 |
239 | if oldConfig != nil && oldConfig.TelemetrySqmId != "" {
240 | newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId
241 | } else if sqmID, err := generator.GenerateSQMID(); err != nil {
242 | log.Fatal("Failed to generate SQM ID:", err)
243 | } else {
244 | newConfig.TelemetrySqmId = sqmID
245 | }
246 |
247 | display.StopProgress()
248 | fmt.Println()
249 | return newConfig
250 | }
251 |
252 | func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error {
253 | display.ShowProgress("Saving configuration...")
254 | if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil {
255 | log.Error(err)
256 | waitExit()
257 | return err
258 | }
259 | display.StopProgress()
260 | fmt.Println()
261 | return nil
262 | }
263 |
264 | func showCompletionMessages(display *ui.Display) {
265 | display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage)
266 | fmt.Println()
267 |
268 | message := "Operation completed!"
269 | if lang.GetCurrentLanguage() == lang.CN {
270 | message = "操作完成!"
271 | }
272 | display.ShowInfo(message)
273 | }
274 |
275 | func waitExit() {
276 | fmt.Print(lang.GetText().PressEnterToExit)
277 | os.Stdout.Sync()
278 | bufio.NewReader(os.Stdin).ReadString('\n')
279 | }
280 |
281 | func checkAdminPrivileges() (bool, error) {
282 | switch runtime.GOOS {
283 | case "windows":
284 | cmd := exec.Command("net", "session")
285 | return cmd.Run() == nil, nil
286 |
287 | case "darwin", "linux":
288 | currentUser, err := user.Current()
289 | if err != nil {
290 | return false, fmt.Errorf("failed to get current user: %w", err)
291 | }
292 | return currentUser.Uid == "0", nil
293 |
294 | default:
295 | return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
296 | }
297 | }
298 |
299 | func selfElevate() error {
300 | os.Setenv("AUTOMATED_MODE", "1")
301 |
302 | switch runtime.GOOS {
303 | case "windows":
304 | verb := "runas"
305 | exe, _ := os.Executable()
306 | cwd, _ := os.Getwd()
307 | args := strings.Join(os.Args[1:], " ")
308 |
309 | cmd := exec.Command("cmd", "/C", "start", verb, exe, args)
310 | cmd.Dir = cwd
311 | return cmd.Run()
312 |
313 | case "darwin", "linux":
314 | exe, err := os.Executable()
315 | if err != nil {
316 | return err
317 | }
318 |
319 | cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...)
320 | cmd.Stdin = os.Stdin
321 | cmd.Stdout = os.Stdout
322 | cmd.Stderr = os.Stderr
323 | return cmd.Run()
324 |
325 | default:
326 | return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
327 | }
328 | }
329 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/process_cursor_links.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from dataclasses import dataclass
3 | from typing import List
4 | import json
5 |
6 | @dataclass
7 | class CursorVersion:
8 | version: str
9 | build_id: str
10 |
11 | def get_download_links(self) -> dict:
12 | base_url = f"https://downloader.cursor.sh/builds/{self.build_id}"
13 | return {
14 | "windows": {
15 | "x64": f"{base_url}/windows/nsis/x64",
16 | "arm64": f"{base_url}/windows/nsis/arm64"
17 | },
18 | "mac": {
19 | "universal": f"{base_url}/mac/installer/universal",
20 | "arm64": f"{base_url}/mac/installer/arm64",
21 | "x64": f"{base_url}/mac/installer/x64"
22 | },
23 | "linux": {
24 | "x64": f"{base_url}/linux/appImage/x64"
25 | }
26 | }
27 |
28 | def parse_versions(data: str) -> List[CursorVersion]:
29 | versions = []
30 | for line in data.strip().split('\n'):
31 | if not line:
32 | continue
33 | version, build_id = line.strip().split(',')
34 | versions.append(CursorVersion(version, build_id))
35 | return versions
36 |
37 | def generate_markdown(versions: List[CursorVersion]) -> str:
38 | md = """# 🖥️ Windows
39 |
40 | ## x64
41 | 📦 Windows x64 安装包
43 |
44 | | 版本 | 下载链接 |
45 | |------|----------|
46 | """
47 |
48 | # Windows x64
49 | for version in versions:
50 | links = version.get_download_links()
51 | md += f"| {version.version} | [下载]({links['windows']['x64']}) |\n"
52 |
53 | md += """
54 | 📱 Windows ARM64 安装包
59 |
60 | | 版本 | 下载链接 |
61 | |------|----------|
62 | """
63 |
64 | # Windows ARM64
65 | for version in versions:
66 | links = version.get_download_links()
67 | md += f"| {version.version} | [下载]({links['windows']['arm64']}) |\n"
68 |
69 | md += """
70 | 🎯 macOS Universal 安装包
77 |
78 | | 版本 | 下载链接 |
79 | |------|----------|
80 | """
81 |
82 | # macOS Universal
83 | for version in versions:
84 | links = version.get_download_links()
85 | md += f"| {version.version} | [下载]({links['mac']['universal']}) |\n"
86 |
87 | md += """
88 | 💪 macOS ARM64 安装包
93 |
94 | | 版本 | 下载链接 |
95 | |------|----------|
96 | """
97 |
98 | # macOS ARM64
99 | for version in versions:
100 | links = version.get_download_links()
101 | md += f"| {version.version} | [下载]({links['mac']['arm64']}) |\n"
102 |
103 | md += """
104 | 💻 macOS Intel 安装包
109 |
110 | | 版本 | 下载链接 |
111 | |------|----------|
112 | """
113 |
114 | # macOS Intel
115 | for version in versions:
116 | links = version.get_download_links()
117 | md += f"| {version.version} | [下载]({links['mac']['x64']}) |\n"
118 |
119 | md += """
120 | 🎮 Linux x64 AppImage
127 |
128 | | 版本 | 下载链接 |
129 | |------|----------|
130 | """
131 |
132 | # Linux x64
133 | for version in versions:
134 | links = version.get_download_links()
135 | md += f"| {version.version} | [下载]({links['linux']['x64']}) |\n"
136 |
137 | md += """
138 |
12 |
13 |
14 |
15 | > ⚠️ **重要提示**
16 | >
17 | > 本工具当前支持版本:
18 | > - ✅ Windows: 最新的 1.0.x 版本(已支持)
19 | > - ✅ Mac/Linux: 最新的 1.0.x 版本(已支持,欢迎测试并反馈问题)
20 |
21 | > 使用前请确认您的 Cursor 版本。
22 |
23 | | 158 | 159 | **Windows** ✅ 160 | 161 | - x64 & x86 162 | 163 | | 164 |165 | 166 | **macOS** ✅ 167 | 168 | - Intel & M-series 169 | 170 | | 171 |172 | 173 | **Linux** ✅ 174 | 175 | - x64 & ARM64 176 | 177 | | 178 |
216 | |
458 | 个人微信 459 | ![]() 460 | 微信:JavaRookie666 461 | |
462 |
463 | 微信交流群 464 | ![]() 465 | 二维码7天内(8月28日前)有效,过期请加微信 466 | |
467 |
468 | 公众号 469 | ![]() 470 | 获取更多AI开发资源 471 | |
472 |
473 | 微信赞赏 474 | ![]() 475 | 要到饭咧?啊咧?啊咧?不给也没事~ 请随意打赏 476 | |
477 |
478 | 支付宝赞赏 479 | ![]() 480 | 如果觉得有帮助,来包辣条犒劳一下吧~ 481 | |
482 |
12 |
13 |
14 |
15 | | 212 | 213 | **Windows** ✅ 214 | 215 | - x64 (64ビット) 216 | - x86 (32ビット) 217 | 218 | | 219 |220 | 221 | **macOS** ✅ 222 | 223 | - Intel (x64) 224 | - Apple Silicon (M1/M2) 225 | 226 | | 227 |228 | 229 | **Linux** ✅ 230 | 231 | - x64 (64ビット) 232 | - x86 (32ビット) 233 | - ARM64 234 | 235 | | 236 |
274 | |
515 | WeChat Pay 516 | ![]() 517 | 要到饭咧?啊咧?啊咧?不给也没事~ 请随意打赏 518 | |
519 |
520 | Alipay 521 | ![]() 522 | 如果觉得有帮助,来包辣条犒劳一下吧~ 523 | |
524 |
525 | Alipay 526 | ![]() 527 | 1 Latiao = 1 AI thought cycle 528 | |
529 |
530 | WeChat 531 | ![]() 532 | 二维码7天内(8月28日前前)有效,过期请加微信 533 | |
534 |
540 |
541 |
12 |
13 |
14 |
15 | | 215 | 216 | **Windows** ✅ 217 | 218 | - x64 (64-bit) 219 | - x86 (32-bit) 220 | 221 | | 222 |223 | 224 | **macOS** ✅ 225 | 226 | - Intel (x64) 227 | - Apple Silicon (M1/M2) 228 | 229 | | 230 |231 | 232 | **Linux** ✅ 233 | 234 | - x64 (64-bit) 235 | - x86 (32-bit) 236 | - ARM64 237 | 238 | | 239 |
279 | |
531 | 微信赞赏 532 | ![]() 533 | 要到饭咧?啊咧?啊咧?不给也没事~ 请随意打赏 534 | |
535 |
536 | 支付宝赞赏 537 | ![]() 538 | 如果觉得有帮助,来包辣条犒劳一下吧~ 539 | |
540 |
541 | Alipay 542 | ![]() 543 | 1 Latiao = 1 AI thought cycle 544 | |
545 |
546 | WeChat 547 | ![]() 548 | 二维码7天内(8月28日前前)有效,过期请加微信 549 | |
550 |
556 |
557 |