Browse Source
chore: Refactor build and installation scripts; update configuration and release process
chore: Refactor build and installation scripts; update configuration and release process
- Enhanced build scripts for improved parallel execution and optimization flags. - Updated installation scripts for better user experience and error handling. - Modified .gitignore to include new build artifacts and IDE configurations. - Updated .goreleaser.yml for better release management and platform support. - Removed deprecated main.go file and adjusted README for clarity on installation and usage. - Added support for multiple architectures in build process, including 32-bit and 64-bit for Windows, macOS, and Linux. These changes streamline the development workflow and enhance the overall usability of the Cursor ID Modifier tool.pull/85/head
19 changed files with 1336 additions and 1884 deletions
-
86.github/workflows/release.yml
-
43.gitignore
-
106.goreleaser.yml
-
21Makefile
-
174README.md
-
9go.mod
-
17go.sum
-
150internal/config/config.go
-
156internal/lang/lang.go
-
162internal/process/manager.go
-
101internal/ui/display.go
-
110internal/ui/spinner.go
-
1050main.go
-
95pkg/idgen/generator.go
-
26pkg/idgen/generator_test.go
-
146scripts/build_all.bat
-
108scripts/build_all.sh
-
336scripts/install.ps1
-
324scripts/install.sh
@ -1,23 +1,40 @@ |
|||
# Binary files |
|||
*.dll |
|||
*.so |
|||
*.dylib |
|||
|
|||
# Build directories |
|||
releases/ |
|||
# Compiled binary |
|||
cursor-id-modifier |
|||
cursor-id-modifier.exe |
|||
|
|||
# Build output directories |
|||
bin/ |
|||
dist/ |
|||
|
|||
# Go specific |
|||
go.sum |
|||
|
|||
# Temporary files |
|||
*.tmp |
|||
*~ |
|||
# IDE and editor files |
|||
.vscode/ |
|||
.idea/ |
|||
*.swp |
|||
*.swo |
|||
|
|||
# System files |
|||
# OS specific |
|||
.DS_Store |
|||
Thumbs.db |
|||
|
|||
.vscode |
|||
/.idea |
|||
# Build and release artifacts |
|||
releases/ |
|||
*.syso |
|||
*.exe |
|||
*.exe~ |
|||
*.dll |
|||
*.so |
|||
*.dylib |
|||
|
|||
# Test files |
|||
*.test |
|||
*.out |
|||
coverage.txt |
|||
|
|||
# Temporary files |
|||
*.tmp |
|||
*~ |
|||
*.bak |
|||
*.log |
@ -1,56 +1,110 @@ |
|||
project_name: cursor-id-modifier |
|||
|
|||
before: |
|||
hooks: |
|||
- go mod tidy |
|||
|
|||
builds: |
|||
- env: |
|||
- main: ./cmd/cursor-id-modifier |
|||
binary: cursor-id-modifier |
|||
env: |
|||
- CGO_ENABLED=0 |
|||
ldflags: |
|||
- -s -w -X main.version={{.Version}} |
|||
goos: |
|||
- linux |
|||
- windows |
|||
- darwin |
|||
goarch: |
|||
- amd64 |
|||
- arm64 |
|||
mod_timestamp: '{{ .CommitTimestamp }}' |
|||
- amd64 # Intel 64-bit |
|||
- arm64 # Apple Silicon/ARM64 |
|||
- "386" # Intel 32-bit |
|||
ignore: |
|||
- goos: darwin |
|||
goarch: "386" # No 32-bit support for macOS |
|||
ldflags: |
|||
- -s -w |
|||
- -X main.version={{.Version}} |
|||
flags: |
|||
- -trimpath |
|||
binary: cursor_id_modifier_{{ .Version }}_{{ .Os }}_{{ .Arch }} |
|||
mod_timestamp: '{{ .CommitTimestamp }}' |
|||
|
|||
# Build matrix |
|||
matrix: |
|||
# Special builds for macOS |
|||
- goos: [darwin] |
|||
goarch: [amd64] |
|||
tags: ["intel"] |
|||
- goos: [darwin] |
|||
goarch: [arm64] |
|||
tags: ["apple_silicon"] |
|||
# Windows builds |
|||
- goos: [windows] |
|||
goarch: [amd64, "386"] |
|||
# Linux builds |
|||
- goos: [linux] |
|||
goarch: [amd64, arm64, "386"] |
|||
|
|||
archives: |
|||
- format: binary |
|||
name_template: "{{ .Binary }}" |
|||
allow_different_binary_count: true |
|||
- format: tar.gz |
|||
format_overrides: |
|||
- goos: windows |
|||
format: zip |
|||
name_template: >- |
|||
{{ .ProjectName }}_ |
|||
{{- .Version }}_ |
|||
{{- .Os }}_ |
|||
{{- .Arch }} |
|||
{{- with .Tags }}_{{ . }}{{ end }} |
|||
files: |
|||
- README.md |
|||
- LICENSE |
|||
- scripts/* # Include installation scripts |
|||
replacements: |
|||
darwin: macOS |
|||
linux: Linux |
|||
windows: Windows |
|||
386: x86 |
|||
amd64: x64 |
|||
arm64: arm64 |
|||
|
|||
checksum: |
|||
name_template: 'checksums.txt' |
|||
algorithm: sha256 |
|||
|
|||
changelog: |
|||
use: github |
|||
sort: asc |
|||
groups: |
|||
- title: Features |
|||
regexp: "^.*feat[(\\w)]*:+.*$" |
|||
order: 0 |
|||
- title: 'Bug Fixes' |
|||
regexp: "^.*fix[(\\w)]*:+.*$" |
|||
order: 1 |
|||
- title: Others |
|||
order: 999 |
|||
filters: |
|||
exclude: |
|||
- '^docs:' |
|||
- '^test:' |
|||
- '^ci:' |
|||
- '^chore:' |
|||
- Merge pull request |
|||
- Merge branch |
|||
|
|||
release: |
|||
github: |
|||
owner: '{{ .Env.GITHUB_REPOSITORY_OWNER }}' |
|||
name: '{{ .Env.GITHUB_REPOSITORY_NAME }}' |
|||
draft: false |
|||
prerelease: auto |
|||
mode: replace |
|||
header: | |
|||
## Cursor ID Modifier {{ .Version }} |
|||
|
|||
### Supported Platforms |
|||
- Windows: x64, x86 |
|||
- macOS: Intel (x64), Apple Silicon (M1/M2) |
|||
- Linux: x64, x86, ARM64 |
|||
|
|||
See [CHANGELOG](CHANGELOG.md) for more details. |
|||
footer: | |
|||
**Full Changelog**: https://github.com/dacrab/cursor-id-modifier/compare/{{ .PreviousTag }}...{{ .Tag }} |
|||
|
|||
## Quick Installation |
|||
|
|||
**Linux/macOS**: |
|||
```bash |
|||
curl -fsSL https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.sh | sudo bash && cursor-id-modifier |
|||
``` |
|||
|
|||
**Windows** (PowerShell Admin): |
|||
```powershell |
|||
irm https://raw.githubusercontent.com/dacrab/cursor-id-modifier/main/scripts/install.ps1 | iex; cursor-id-modifier |
|||
``` |
|||
|
|||
snapshot: |
|||
name_template: "{{ incpatch .Version }}-next" |
@ -0,0 +1,21 @@ |
|||
.PHONY: build clean test vet |
|||
|
|||
# Build the application
|
|||
build: |
|||
go build -v ./cmd/cursor-id-modifier |
|||
|
|||
# Clean build artifacts
|
|||
clean: |
|||
rm -f cursor-id-modifier |
|||
go clean |
|||
|
|||
# Run tests
|
|||
test: |
|||
go test -v ./... |
|||
|
|||
# Run go vet
|
|||
vet: |
|||
go vet ./... |
|||
|
|||
# Run all checks
|
|||
all: vet test build |
@ -0,0 +1,150 @@ |
|||
package config |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"os" |
|||
"path/filepath" |
|||
"runtime" |
|||
"sync" |
|||
"time" |
|||
) |
|||
|
|||
// StorageConfig represents the storage configuration
|
|||
type StorageConfig struct { |
|||
TelemetryMacMachineId string `json:"telemetry.macMachineId"` |
|||
TelemetryMachineId string `json:"telemetry.machineId"` |
|||
TelemetryDevDeviceId string `json:"telemetry.devDeviceId"` |
|||
TelemetrySqmId string `json:"telemetry.sqmId"` |
|||
LastModified string `json:"lastModified"` |
|||
Version string `json:"version"` |
|||
} |
|||
|
|||
// Manager handles configuration operations
|
|||
type Manager struct { |
|||
configPath string |
|||
mu sync.RWMutex |
|||
} |
|||
|
|||
// NewManager creates a new configuration manager
|
|||
func NewManager(username string) (*Manager, error) { |
|||
configPath, err := getConfigPath(username) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to get config path: %w", err) |
|||
} |
|||
|
|||
return &Manager{ |
|||
configPath: configPath, |
|||
}, nil |
|||
} |
|||
|
|||
// ReadConfig reads the existing configuration
|
|||
func (m *Manager) ReadConfig() (*StorageConfig, error) { |
|||
m.mu.RLock() |
|||
defer m.mu.RUnlock() |
|||
|
|||
data, err := os.ReadFile(m.configPath) |
|||
if err != nil { |
|||
if os.IsNotExist(err) { |
|||
return nil, nil |
|||
} |
|||
return nil, fmt.Errorf("failed to read config file: %w", err) |
|||
} |
|||
|
|||
var config StorageConfig |
|||
if err := json.Unmarshal(data, &config); err != nil { |
|||
return nil, fmt.Errorf("failed to parse config file: %w", err) |
|||
} |
|||
|
|||
return &config, nil |
|||
} |
|||
|
|||
// SaveConfig saves the configuration
|
|||
func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { |
|||
m.mu.Lock() |
|||
defer m.mu.Unlock() |
|||
|
|||
// Ensure parent directories exist
|
|||
if err := os.MkdirAll(filepath.Dir(m.configPath), 0755); err != nil { |
|||
return fmt.Errorf("failed to create config directory: %w", err) |
|||
} |
|||
|
|||
// Set file permissions
|
|||
if err := os.Chmod(m.configPath, 0666); err != nil && !os.IsNotExist(err) { |
|||
return fmt.Errorf("failed to set file permissions: %w", err) |
|||
} |
|||
|
|||
// Read existing config to preserve other fields
|
|||
var originalFile map[string]interface{} |
|||
originalFileContent, err := os.ReadFile(m.configPath) |
|||
if err != nil && !os.IsNotExist(err) { |
|||
return fmt.Errorf("failed to read original file: %w", err) |
|||
} else if err == nil { |
|||
if err := json.Unmarshal(originalFileContent, &originalFile); err != nil { |
|||
return fmt.Errorf("failed to parse original file: %w", err) |
|||
} |
|||
} else { |
|||
originalFile = make(map[string]interface{}) |
|||
} |
|||
|
|||
// Update fields
|
|||
originalFile["telemetry.sqmId"] = config.TelemetrySqmId |
|||
originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId |
|||
originalFile["telemetry.machineId"] = config.TelemetryMachineId |
|||
originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId |
|||
originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) |
|||
originalFile["version"] = "1.0.1" |
|||
|
|||
// Marshal with indentation
|
|||
newFileContent, err := json.MarshalIndent(originalFile, "", " ") |
|||
if err != nil { |
|||
return fmt.Errorf("failed to marshal config: %w", err) |
|||
} |
|||
|
|||
// Write to temporary file
|
|||
tmpPath := m.configPath + ".tmp" |
|||
if err := os.WriteFile(tmpPath, newFileContent, 0666); err != nil { |
|||
return fmt.Errorf("failed to write temporary file: %w", err) |
|||
} |
|||
|
|||
// Set final permissions
|
|||
fileMode := os.FileMode(0666) |
|||
if readOnly { |
|||
fileMode = 0444 |
|||
} |
|||
|
|||
if err := os.Chmod(tmpPath, fileMode); err != nil { |
|||
os.Remove(tmpPath) |
|||
return fmt.Errorf("failed to set temporary file permissions: %w", err) |
|||
} |
|||
|
|||
// Atomic rename
|
|||
if err := os.Rename(tmpPath, m.configPath); err != nil { |
|||
os.Remove(tmpPath) |
|||
return fmt.Errorf("failed to rename file: %w", err) |
|||
} |
|||
|
|||
// Sync directory
|
|||
if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { |
|||
dir.Sync() |
|||
dir.Close() |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// getConfigPath returns the path to the configuration file
|
|||
func getConfigPath(username string) (string, error) { |
|||
var configDir string |
|||
switch runtime.GOOS { |
|||
case "windows": |
|||
configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage") |
|||
case "darwin": |
|||
configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage") |
|||
case "linux": |
|||
configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage") |
|||
default: |
|||
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) |
|||
} |
|||
return filepath.Join(configDir, "storage.json"), nil |
|||
} |
@ -0,0 +1,156 @@ |
|||
package lang |
|||
|
|||
import ( |
|||
"os" |
|||
"os/exec" |
|||
"strings" |
|||
"sync" |
|||
) |
|||
|
|||
// Language represents a supported language
|
|||
type Language string |
|||
|
|||
const ( |
|||
// CN represents Chinese language
|
|||
CN Language = "cn" |
|||
// EN represents English language
|
|||
EN Language = "en" |
|||
) |
|||
|
|||
// TextResource contains all translatable text resources
|
|||
type TextResource struct { |
|||
SuccessMessage string |
|||
RestartMessage string |
|||
ReadingConfig string |
|||
GeneratingIds string |
|||
PressEnterToExit string |
|||
ErrorPrefix string |
|||
PrivilegeError string |
|||
RunAsAdmin string |
|||
RunWithSudo string |
|||
SudoExample string |
|||
ConfigLocation string |
|||
CheckingProcesses string |
|||
ClosingProcesses string |
|||
ProcessesClosed string |
|||
PleaseWait string |
|||
SetReadOnlyMessage string |
|||
} |
|||
|
|||
var ( |
|||
currentLanguage Language |
|||
currentLanguageOnce sync.Once |
|||
languageMutex sync.RWMutex |
|||
) |
|||
|
|||
// GetCurrentLanguage returns the current language, detecting it if not already set
|
|||
func GetCurrentLanguage() Language { |
|||
currentLanguageOnce.Do(func() { |
|||
currentLanguage = detectLanguage() |
|||
}) |
|||
|
|||
languageMutex.RLock() |
|||
defer languageMutex.RUnlock() |
|||
return currentLanguage |
|||
} |
|||
|
|||
// SetLanguage sets the current language
|
|||
func SetLanguage(lang Language) { |
|||
languageMutex.Lock() |
|||
defer languageMutex.Unlock() |
|||
currentLanguage = lang |
|||
} |
|||
|
|||
// GetText returns the TextResource for the current language
|
|||
func GetText() TextResource { |
|||
return texts[GetCurrentLanguage()] |
|||
} |
|||
|
|||
// detectLanguage detects the system language
|
|||
func detectLanguage() Language { |
|||
// Check environment variables
|
|||
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { |
|||
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") { |
|||
return CN |
|||
} |
|||
} |
|||
|
|||
// Check Windows language settings
|
|||
if isWindows() { |
|||
if isWindowsChineseLocale() { |
|||
return CN |
|||
} |
|||
} else { |
|||
// Check Unix locale
|
|||
if isUnixChineseLocale() { |
|||
return CN |
|||
} |
|||
} |
|||
|
|||
return EN |
|||
} |
|||
|
|||
func isWindows() bool { |
|||
return os.Getenv("OS") == "Windows_NT" |
|||
} |
|||
|
|||
func isWindowsChineseLocale() bool { |
|||
// Check Windows UI culture
|
|||
cmd := exec.Command("powershell", "-Command", |
|||
"[System.Globalization.CultureInfo]::CurrentUICulture.Name") |
|||
output, err := cmd.Output() |
|||
if err == nil && strings.HasPrefix(strings.ToLower(strings.TrimSpace(string(output))), "zh") { |
|||
return true |
|||
} |
|||
|
|||
// Check Windows locale
|
|||
cmd = exec.Command("wmic", "os", "get", "locale") |
|||
output, err = cmd.Output() |
|||
return err == nil && strings.Contains(string(output), "2052") |
|||
} |
|||
|
|||
func isUnixChineseLocale() bool { |
|||
cmd := exec.Command("locale") |
|||
output, err := cmd.Output() |
|||
return err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn") |
|||
} |
|||
|
|||
// texts contains all translations
|
|||
var texts = map[Language]TextResource{ |
|||
CN: { |
|||
SuccessMessage: "[√] 配置文件已成功更新!", |
|||
RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", |
|||
ReadingConfig: "正在读取配置文件...", |
|||
GeneratingIds: "正在生成新的标识符...", |
|||
PressEnterToExit: "按回车键退出程序...", |
|||
ErrorPrefix: "程序发生严重错误: %v", |
|||
PrivilegeError: "\n[!] 错误:需要管理员权限", |
|||
RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」", |
|||
RunWithSudo: "请使用 sudo 命令运行此程序", |
|||
SudoExample: "示例: sudo %s", |
|||
ConfigLocation: "配置文件位置:", |
|||
CheckingProcesses: "正在检查运行中的 Cursor 实例...", |
|||
ClosingProcesses: "正在关闭 Cursor 实例...", |
|||
ProcessesClosed: "所有 Cursor 实例已关闭", |
|||
PleaseWait: "请稍候...", |
|||
SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", |
|||
}, |
|||
EN: { |
|||
SuccessMessage: "[√] Configuration file updated successfully!", |
|||
RestartMessage: "[!] Please restart Cursor manually for changes to take effect", |
|||
ReadingConfig: "Reading configuration file...", |
|||
GeneratingIds: "Generating new identifiers...", |
|||
PressEnterToExit: "Press Enter to exit...", |
|||
ErrorPrefix: "Program encountered a serious error: %v", |
|||
PrivilegeError: "\n[!] Error: Administrator privileges required", |
|||
RunAsAdmin: "Please right-click and select 'Run as Administrator'", |
|||
RunWithSudo: "Please run this program with sudo", |
|||
SudoExample: "Example: sudo %s", |
|||
ConfigLocation: "Config file location:", |
|||
CheckingProcesses: "Checking for running Cursor instances...", |
|||
ClosingProcesses: "Closing Cursor instances...", |
|||
ProcessesClosed: "All Cursor instances have been closed", |
|||
PleaseWait: "Please wait...", |
|||
SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", |
|||
}, |
|||
} |
@ -0,0 +1,162 @@ |
|||
package process |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"os/exec" |
|||
"runtime" |
|||
"strings" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/sirupsen/logrus" |
|||
) |
|||
|
|||
// Config holds process manager configuration
|
|||
type Config struct { |
|||
RetryAttempts int |
|||
RetryDelay time.Duration |
|||
Timeout time.Duration |
|||
} |
|||
|
|||
// DefaultConfig returns the default configuration
|
|||
func DefaultConfig() *Config { |
|||
return &Config{ |
|||
RetryAttempts: 3, |
|||
RetryDelay: time.Second, |
|||
Timeout: 30 * time.Second, |
|||
} |
|||
} |
|||
|
|||
// Manager handles process-related operations
|
|||
type Manager struct { |
|||
config *Config |
|||
log *logrus.Logger |
|||
mu sync.Mutex |
|||
} |
|||
|
|||
// NewManager creates a new process manager
|
|||
func NewManager(config *Config, log *logrus.Logger) *Manager { |
|||
if config == nil { |
|||
config = DefaultConfig() |
|||
} |
|||
if log == nil { |
|||
log = logrus.New() |
|||
} |
|||
return &Manager{ |
|||
config: config, |
|||
log: log, |
|||
} |
|||
} |
|||
|
|||
// KillCursorProcesses attempts to kill all Cursor processes
|
|||
func (m *Manager) KillCursorProcesses() error { |
|||
m.mu.Lock() |
|||
defer m.mu.Unlock() |
|||
|
|||
ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout) |
|||
defer cancel() |
|||
|
|||
for attempt := 0; attempt < m.config.RetryAttempts; attempt++ { |
|||
m.log.Debugf("Attempt %d/%d to kill Cursor processes", attempt+1, m.config.RetryAttempts) |
|||
|
|||
if err := m.killProcess(ctx); err != nil { |
|||
m.log.Warnf("Failed to kill processes on attempt %d: %v", attempt+1, err) |
|||
time.Sleep(m.config.RetryDelay) |
|||
continue |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.RetryAttempts) |
|||
} |
|||
|
|||
// IsCursorRunning checks if any Cursor process is running
|
|||
func (m *Manager) IsCursorRunning() bool { |
|||
m.mu.Lock() |
|||
defer m.mu.Unlock() |
|||
|
|||
processes, err := m.listCursorProcesses() |
|||
if err != nil { |
|||
m.log.Warnf("Failed to list Cursor processes: %v", err) |
|||
return false |
|||
} |
|||
|
|||
return len(processes) > 0 |
|||
} |
|||
|
|||
func (m *Manager) killProcess(ctx context.Context) error { |
|||
if runtime.GOOS == "windows" { |
|||
return m.killWindowsProcess(ctx) |
|||
} |
|||
return m.killUnixProcess(ctx) |
|||
} |
|||
|
|||
func (m *Manager) killWindowsProcess(ctx context.Context) error { |
|||
// First try graceful termination
|
|||
if err := exec.CommandContext(ctx, "taskkill", "/IM", "Cursor.exe").Run(); err != nil { |
|||
m.log.Debugf("Graceful termination failed: %v", err) |
|||
} |
|||
|
|||
time.Sleep(m.config.RetryDelay) |
|||
|
|||
// Force kill if still running
|
|||
if err := exec.CommandContext(ctx, "taskkill", "/F", "/IM", "Cursor.exe").Run(); err != nil { |
|||
return fmt.Errorf("failed to force kill Cursor process: %w", err) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (m *Manager) killUnixProcess(ctx context.Context) error { |
|||
processes, err := m.listCursorProcesses() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to list processes: %w", err) |
|||
} |
|||
|
|||
for _, pid := range processes { |
|||
if err := m.forceKillProcess(ctx, pid); err != nil { |
|||
m.log.Warnf("Failed to kill process %s: %v", pid, err) |
|||
continue |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (m *Manager) forceKillProcess(ctx context.Context, pid string) error { |
|||
// Try graceful termination first
|
|||
if err := exec.CommandContext(ctx, "kill", pid).Run(); err == nil { |
|||
m.log.Debugf("Process %s terminated gracefully", pid) |
|||
time.Sleep(2 * time.Second) |
|||
return nil |
|||
} |
|||
|
|||
// Force kill if still running
|
|||
if err := exec.CommandContext(ctx, "kill", "-9", pid).Run(); err != nil { |
|||
return fmt.Errorf("failed to force kill process %s: %w", pid, err) |
|||
} |
|||
|
|||
m.log.Debugf("Process %s force killed", pid) |
|||
return nil |
|||
} |
|||
|
|||
func (m *Manager) listCursorProcesses() ([]string, error) { |
|||
cmd := exec.Command("ps", "aux") |
|||
output, err := cmd.Output() |
|||
if err != nil { |
|||
return nil, fmt.Errorf("failed to execute ps command: %w", err) |
|||
} |
|||
|
|||
var pids []string |
|||
for _, line := range strings.Split(string(output), "\n") { |
|||
if strings.Contains(strings.ToLower(line), "apprun") { |
|||
fields := strings.Fields(line) |
|||
if len(fields) > 1 { |
|||
pids = append(pids, fields[1]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return pids, nil |
|||
} |
@ -0,0 +1,101 @@ |
|||
package ui |
|||
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
"os/exec" |
|||
"runtime" |
|||
"strings" |
|||
|
|||
"github.com/fatih/color" |
|||
) |
|||
|
|||
// Display handles UI display operations
|
|||
type Display struct { |
|||
spinner *Spinner |
|||
} |
|||
|
|||
// NewDisplay creates a new display handler
|
|||
func NewDisplay(spinner *Spinner) *Display { |
|||
if spinner == nil { |
|||
spinner = NewSpinner(nil) |
|||
} |
|||
return &Display{ |
|||
spinner: spinner, |
|||
} |
|||
} |
|||
|
|||
// ShowProgress shows a progress message with spinner
|
|||
func (d *Display) ShowProgress(message string) { |
|||
d.spinner.SetMessage(message) |
|||
d.spinner.Start() |
|||
} |
|||
|
|||
// StopProgress stops the progress spinner
|
|||
func (d *Display) StopProgress() { |
|||
d.spinner.Stop() |
|||
} |
|||
|
|||
// ClearScreen clears the terminal screen
|
|||
func (d *Display) ClearScreen() error { |
|||
var cmd *exec.Cmd |
|||
if runtime.GOOS == "windows" { |
|||
cmd = exec.Command("cmd", "/c", "cls") |
|||
} else { |
|||
cmd = exec.Command("clear") |
|||
} |
|||
cmd.Stdout = os.Stdout |
|||
return cmd.Run() |
|||
} |
|||
|
|||
// ShowProcessStatus shows the current process status
|
|||
func (d *Display) ShowProcessStatus(message string) { |
|||
fmt.Printf("\r%s", strings.Repeat(" ", 80)) // Clear line
|
|||
fmt.Printf("\r%s", color.CyanString("⚡ "+message)) |
|||
} |
|||
|
|||
// ShowPrivilegeError shows the privilege error message
|
|||
func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) { |
|||
red := color.New(color.FgRed, color.Bold) |
|||
yellow := color.New(color.FgYellow) |
|||
|
|||
red.Println(errorMsg) |
|||
if runtime.GOOS == "windows" { |
|||
yellow.Println(adminMsg) |
|||
} else { |
|||
yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0])) |
|||
} |
|||
} |
|||
|
|||
// ShowSuccess shows a success message
|
|||
func (d *Display) ShowSuccess(successMsg, restartMsg string) { |
|||
green := color.New(color.FgGreen, color.Bold) |
|||
yellow := color.New(color.FgYellow, color.Bold) |
|||
|
|||
green.Printf("\n%s\n", successMsg) |
|||
yellow.Printf("%s\n", restartMsg) |
|||
} |
|||
|
|||
// ShowError shows an error message
|
|||
func (d *Display) ShowError(message string) { |
|||
red := color.New(color.FgRed, color.Bold) |
|||
red.Printf("\n%s\n", message) |
|||
} |
|||
|
|||
// ShowWarning shows a warning message
|
|||
func (d *Display) ShowWarning(message string) { |
|||
yellow := color.New(color.FgYellow, color.Bold) |
|||
yellow.Printf("\n%s\n", message) |
|||
} |
|||
|
|||
// ShowInfo shows an info message
|
|||
func (d *Display) ShowInfo(message string) { |
|||
cyan := color.New(color.FgCyan) |
|||
cyan.Printf("\n%s\n", message) |
|||
} |
|||
|
|||
// ShowPrompt shows a prompt message and waits for user input
|
|||
func (d *Display) ShowPrompt(message string) { |
|||
fmt.Print(message) |
|||
os.Stdout.Sync() |
|||
} |
@ -0,0 +1,110 @@ |
|||
package ui |
|||
|
|||
import ( |
|||
"fmt" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/fatih/color" |
|||
) |
|||
|
|||
// SpinnerConfig defines spinner configuration
|
|||
type SpinnerConfig struct { |
|||
Frames []string |
|||
Delay time.Duration |
|||
} |
|||
|
|||
// DefaultSpinnerConfig returns the default spinner configuration
|
|||
func DefaultSpinnerConfig() *SpinnerConfig { |
|||
return &SpinnerConfig{ |
|||
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, |
|||
Delay: 100 * time.Millisecond, |
|||
} |
|||
} |
|||
|
|||
// Spinner represents a progress spinner
|
|||
type Spinner struct { |
|||
config *SpinnerConfig |
|||
message string |
|||
current int |
|||
active bool |
|||
stopCh chan struct{} |
|||
mu sync.RWMutex |
|||
} |
|||
|
|||
// NewSpinner creates a new spinner with the given configuration
|
|||
func NewSpinner(config *SpinnerConfig) *Spinner { |
|||
if config == nil { |
|||
config = DefaultSpinnerConfig() |
|||
} |
|||
return &Spinner{ |
|||
config: config, |
|||
stopCh: make(chan struct{}), |
|||
} |
|||
} |
|||
|
|||
// SetMessage sets the spinner message
|
|||
func (s *Spinner) SetMessage(message string) { |
|||
s.mu.Lock() |
|||
defer s.mu.Unlock() |
|||
s.message = message |
|||
} |
|||
|
|||
// Start starts the spinner animation
|
|||
func (s *Spinner) Start() { |
|||
s.mu.Lock() |
|||
if s.active { |
|||
s.mu.Unlock() |
|||
return |
|||
} |
|||
s.active = true |
|||
s.mu.Unlock() |
|||
|
|||
go s.run() |
|||
} |
|||
|
|||
// Stop stops the spinner animation
|
|||
func (s *Spinner) Stop() { |
|||
s.mu.Lock() |
|||
defer s.mu.Unlock() |
|||
|
|||
if !s.active { |
|||
return |
|||
} |
|||
|
|||
s.active = false |
|||
close(s.stopCh) |
|||
s.stopCh = make(chan struct{}) |
|||
fmt.Println() |
|||
} |
|||
|
|||
// IsActive returns whether the spinner is currently active
|
|||
func (s *Spinner) IsActive() bool { |
|||
s.mu.RLock() |
|||
defer s.mu.RUnlock() |
|||
return s.active |
|||
} |
|||
|
|||
func (s *Spinner) run() { |
|||
ticker := time.NewTicker(s.config.Delay) |
|||
defer ticker.Stop() |
|||
|
|||
for { |
|||
select { |
|||
case <-s.stopCh: |
|||
return |
|||
case <-ticker.C: |
|||
s.mu.RLock() |
|||
if !s.active { |
|||
s.mu.RUnlock() |
|||
return |
|||
} |
|||
frame := s.config.Frames[s.current%len(s.config.Frames)] |
|||
message := s.message |
|||
s.current++ |
|||
s.mu.RUnlock() |
|||
|
|||
fmt.Printf("\r%s %s", color.CyanString(frame), message) |
|||
} |
|||
} |
|||
} |
1050
main.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,95 @@ |
|||
package idgen |
|||
|
|||
import ( |
|||
cryptorand "crypto/rand" |
|||
"crypto/sha256" |
|||
"encoding/hex" |
|||
"fmt" |
|||
"math/big" |
|||
"sync" |
|||
) |
|||
|
|||
// Generator handles the generation of various IDs
|
|||
type Generator struct { |
|||
charsetMu sync.RWMutex |
|||
charset string |
|||
} |
|||
|
|||
// NewGenerator creates a new ID generator with default settings
|
|||
func NewGenerator() *Generator { |
|||
return &Generator{ |
|||
charset: "0123456789ABCDEFGHJKLMNPQRSTVWXYZ", |
|||
} |
|||
} |
|||
|
|||
// SetCharset allows customizing the character set used for ID generation
|
|||
func (g *Generator) SetCharset(charset string) { |
|||
g.charsetMu.Lock() |
|||
defer g.charsetMu.Unlock() |
|||
g.charset = charset |
|||
} |
|||
|
|||
// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id]
|
|||
func (g *Generator) GenerateMachineID() (string, error) { |
|||
prefix := "auth0|user_" |
|||
|
|||
// Generate random sequence number between 0-99
|
|||
seqNum, err := cryptorand.Int(cryptorand.Reader, big.NewInt(100)) |
|||
if err != nil { |
|||
return "", fmt.Errorf("failed to generate sequence number: %w", err) |
|||
} |
|||
sequence := fmt.Sprintf("%02d", seqNum.Int64()) |
|||
|
|||
uniqueID, err := g.generateUniqueID(23) |
|||
if err != nil { |
|||
return "", fmt.Errorf("failed to generate unique ID: %w", err) |
|||
} |
|||
|
|||
fullID := prefix + sequence + uniqueID |
|||
return hex.EncodeToString([]byte(fullID)), nil |
|||
} |
|||
|
|||
// GenerateMacMachineID generates a new MAC machine ID using SHA-256
|
|||
func (g *Generator) GenerateMacMachineID() (string, error) { |
|||
data := make([]byte, 32) |
|||
if _, err := cryptorand.Read(data); err != nil { |
|||
return "", fmt.Errorf("failed to generate random data: %w", err) |
|||
} |
|||
|
|||
hash := sha256.Sum256(data) |
|||
return hex.EncodeToString(hash[:]), nil |
|||
} |
|||
|
|||
// GenerateDeviceID generates a new device ID in UUID v4 format
|
|||
func (g *Generator) GenerateDeviceID() (string, error) { |
|||
uuid := make([]byte, 16) |
|||
if _, err := cryptorand.Read(uuid); err != nil { |
|||
return "", fmt.Errorf("failed to generate UUID: %w", err) |
|||
} |
|||
|
|||
// Set version (4) and variant (2) bits according to RFC 4122
|
|||
uuid[6] = (uuid[6] & 0x0f) | 0x40 |
|||
uuid[8] = (uuid[8] & 0x3f) | 0x80 |
|||
|
|||
return fmt.Sprintf("%x-%x-%x-%x-%x", |
|||
uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]), nil |
|||
} |
|||
|
|||
// generateUniqueID generates a random string of specified length using the configured charset
|
|||
func (g *Generator) generateUniqueID(length int) (string, error) { |
|||
g.charsetMu.RLock() |
|||
defer g.charsetMu.RUnlock() |
|||
|
|||
result := make([]byte, length) |
|||
max := big.NewInt(int64(len(g.charset))) |
|||
|
|||
for i := range result { |
|||
randNum, err := cryptorand.Int(cryptorand.Reader, max) |
|||
if err != nil { |
|||
return "", fmt.Errorf("failed to generate random number: %w", err) |
|||
} |
|||
result[i] = g.charset[randNum.Int64()] |
|||
} |
|||
|
|||
return string(result), nil |
|||
} |
@ -0,0 +1,26 @@ |
|||
package idgen |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestNewGenerator(t *testing.T) { |
|||
gen := NewGenerator() |
|||
assert.NotNil(t, gen, "Generator should not be nil") |
|||
} |
|||
|
|||
func TestGenerateMachineID(t *testing.T) { |
|||
gen := NewGenerator() |
|||
id, err := gen.GenerateMachineID() |
|||
assert.NoError(t, err, "Should not return an error") |
|||
assert.NotEmpty(t, id, "Generated machine ID should not be empty") |
|||
} |
|||
|
|||
func TestGenerateDeviceID(t *testing.T) { |
|||
gen := NewGenerator() |
|||
id, err := gen.GenerateDeviceID() |
|||
assert.NoError(t, err, "Should not return an error") |
|||
assert.NotEmpty(t, id, "Generated device ID should not be empty") |
|||
} |
@ -1,128 +1,74 @@ |
|||
@echo off |
|||
setlocal EnableDelayedExpansion |
|||
|
|||
:: Build optimization flags |
|||
set "OPTIMIZATION_FLAGS=-trimpath -ldflags=\"-s -w\"" |
|||
set "BUILD_JOBS=4" |
|||
|
|||
:: Messages / 消息 |
|||
set "EN_MESSAGES[0]=Starting build process for version" |
|||
set "EN_MESSAGES[1]=Using optimization flags:" |
|||
set "EN_MESSAGES[2]=Cleaning old builds..." |
|||
set "EN_MESSAGES[3]=Cleanup completed" |
|||
set "EN_MESSAGES[4]=bin directory does not exist, no cleanup needed" |
|||
set "EN_MESSAGES[5]=Starting builds for all platforms..." |
|||
set "EN_MESSAGES[6]=Building for" |
|||
set "EN_MESSAGES[7]=Build successful:" |
|||
set "EN_MESSAGES[8]=Build failed for" |
|||
set "EN_MESSAGES[9]=All builds completed! Total time:" |
|||
set "EN_MESSAGES[10]=seconds" |
|||
|
|||
set "CN_MESSAGES[0]=开始构建版本" |
|||
set "CN_MESSAGES[1]=使用优化标志:" |
|||
set "CN_MESSAGES[2]=正在清理旧的构建文件..." |
|||
set "CN_MESSAGES[3]=清理完成" |
|||
set "CN_MESSAGES[4]=bin 目录不存在,无需清理" |
|||
set "CN_MESSAGES[5]=开始编译所有平台..." |
|||
set "CN_MESSAGES[6]=正在构建" |
|||
set "CN_MESSAGES[7]=构建成功:" |
|||
set "CN_MESSAGES[8]=构建失败:" |
|||
set "CN_MESSAGES[9]=所有构建完成!总耗时:" |
|||
set "CN_MESSAGES[10]=秒" |
|||
|
|||
:: 设置版本信息 / Set version |
|||
set VERSION=2.0.0 |
|||
set "EN_MESSAGES[4]=Starting builds for all platforms..." |
|||
set "EN_MESSAGES[5]=Building for" |
|||
set "EN_MESSAGES[6]=Build successful:" |
|||
set "EN_MESSAGES[7]=All builds completed!" |
|||
|
|||
:: 设置颜色代码 / Set color codes |
|||
:: Colors |
|||
set "GREEN=[32m" |
|||
set "RED=[31m" |
|||
set "YELLOW=[33m" |
|||
set "RESET=[0m" |
|||
|
|||
:: 设置编译优化标志 / Set build optimization flags |
|||
set "LDFLAGS=-s -w" |
|||
set "BUILDMODE=pie" |
|||
set "GCFLAGS=-N -l" |
|||
|
|||
:: 设置 CGO / Set CGO |
|||
set CGO_ENABLED=0 |
|||
|
|||
:: 检测系统语言 / Detect system language |
|||
for /f "tokens=2 delims==" %%a in ('wmic os get OSLanguage /value') do set OSLanguage=%%a |
|||
if "%OSLanguage%"=="2052" (set LANG=cn) else (set LANG=en) |
|||
|
|||
:: 显示编译信息 / Display build info |
|||
echo %YELLOW%!%LANG%_MESSAGES[0]! %VERSION%%RESET% |
|||
echo %YELLOW%!%LANG%_MESSAGES[1]! LDFLAGS=%LDFLAGS%, BUILDMODE=%BUILDMODE%%RESET% |
|||
echo %YELLOW%CGO_ENABLED=%CGO_ENABLED%%RESET% |
|||
|
|||
:: 清理旧的构建文件 / Clean old builds |
|||
echo %YELLOW%!%LANG%_MESSAGES[2]!%RESET% |
|||
:: Cleanup function |
|||
:cleanup |
|||
if exist "..\bin" ( |
|||
rd /s /q "..\bin" |
|||
echo %GREEN%!%LANG%_MESSAGES[3]!%RESET% |
|||
) else ( |
|||
echo %YELLOW%!%LANG%_MESSAGES[4]!%RESET% |
|||
echo %GREEN%!EN_MESSAGES[3]!%RESET% |
|||
) |
|||
|
|||
:: 创建输出目录 / Create output directory |
|||
mkdir "..\bin" 2>nul |
|||
|
|||
:: 定义目标平台数组 / Define target platforms array |
|||
set platforms[0].os=windows |
|||
set platforms[0].arch=amd64 |
|||
set platforms[0].ext=.exe |
|||
set platforms[0].suffix= |
|||
:: Build function with optimizations |
|||
:build |
|||
set "os=%~1" |
|||
set "arch=%~2" |
|||
set "ext=" |
|||
if "%os%"=="windows" set "ext=.exe" |
|||
|
|||
set platforms[1].os=darwin |
|||
set platforms[1].arch=amd64 |
|||
set platforms[1].ext= |
|||
set platforms[1].suffix=_intel |
|||
echo %GREEN%!EN_MESSAGES[5]! %os%/%arch%%RESET% |
|||
|
|||
set platforms[2].os=darwin |
|||
set platforms[2].arch=arm64 |
|||
set platforms[2].ext= |
|||
set platforms[2].suffix=_m1 |
|||
set "CGO_ENABLED=0" |
|||
set "GOOS=%os%" |
|||
set "GOARCH=%arch%" |
|||
|
|||
set platforms[3].os=linux |
|||
set platforms[3].arch=amd64 |
|||
set platforms[3].ext= |
|||
set platforms[3].suffix= |
|||
start /b cmd /c "go build -trimpath -ldflags=\"-s -w\" -o ..\bin\%os%\%arch%\cursor-id-modifier%ext% -a -installsuffix cgo -mod=readonly ..\cmd\cursor-id-modifier" |
|||
exit /b 0 |
|||
|
|||
:: 设置开始时间 / Set start time |
|||
set start_time=%time% |
|||
:: Main execution |
|||
echo %GREEN%!EN_MESSAGES[0]!%RESET% |
|||
echo %GREEN%!EN_MESSAGES[1]! %OPTIMIZATION_FLAGS%%RESET% |
|||
|
|||
:: 编译所有目标 / Build all targets |
|||
echo !%LANG%_MESSAGES[5]! |
|||
call :cleanup |
|||
|
|||
for /L %%i in (0,1,3) do ( |
|||
set "os=!platforms[%%i].os!" |
|||
set "arch=!platforms[%%i].arch!" |
|||
set "ext=!platforms[%%i].ext!" |
|||
set "suffix=!platforms[%%i].suffix!" |
|||
|
|||
echo. |
|||
echo !%LANG%_MESSAGES[6]! !os! !arch!... |
|||
|
|||
set GOOS=!os! |
|||
set GOARCH=!arch! |
|||
|
|||
:: 构建输出文件名 / Build output filename |
|||
set "outfile=..\bin\cursor_id_modifier_v%VERSION%_!os!_!arch!!suffix!!ext!" |
|||
|
|||
:: 执行构建 / Execute build |
|||
go build -trimpath -buildmode=%BUILDMODE% -ldflags="%LDFLAGS%" -gcflags="%GCFLAGS%" -o "!outfile!" ..\main.go |
|||
|
|||
if !errorlevel! equ 0 ( |
|||
echo %GREEN%!%LANG%_MESSAGES[7]! !outfile!%RESET% |
|||
) else ( |
|||
echo %RED%!%LANG%_MESSAGES[8]! !os! !arch!%RESET% |
|||
echo %GREEN%!EN_MESSAGES[4]!%RESET% |
|||
|
|||
:: Start builds in parallel |
|||
set "pending=0" |
|||
for %%o in (windows linux darwin) do ( |
|||
for %%a in (amd64 386) do ( |
|||
call :build %%o %%a |
|||
set /a "pending+=1" |
|||
if !pending! geq %BUILD_JOBS% ( |
|||
timeout /t 1 /nobreak >nul |
|||
set "pending=0" |
|||
) |
|||
) |
|||
) |
|||
|
|||
:: 计算总耗时 / Calculate total time |
|||
set end_time=%time% |
|||
set /a duration = %end_time:~0,2% * 3600 + %end_time:~3,2% * 60 + %end_time:~6,2% - (%start_time:~0,2% * 3600 + %start_time:~3,2% * 60 + %start_time:~6,2%) |
|||
|
|||
echo. |
|||
echo %GREEN%!%LANG%_MESSAGES[9]! %duration% !%LANG%_MESSAGES[10]!%RESET% |
|||
if exist "..\bin" dir /b "..\bin" |
|||
:: Wait for all builds to complete |
|||
:wait_builds |
|||
timeout /t 2 /nobreak >nul |
|||
tasklist /fi "IMAGENAME eq go.exe" 2>nul | find "go.exe" >nul |
|||
if not errorlevel 1 goto wait_builds |
|||
|
|||
pause |
|||
endlocal |
|||
echo %GREEN%!EN_MESSAGES[7]!%RESET% |
Write
Preview
Loading…
Cancel
Save
Reference in new issue