refactor: streamline configuration management and enhance UI interactions

- Updated go.mod and go.sum to include necessary dependencies.
- Refactored README.md for clearer installation instructions and improved formatting.
- Enhanced main.go with better error handling and user feedback during execution.
- Improved configuration management in config.go, ensuring atomic writes and better error handling.
- Updated language support in lang.go for clearer user messages.
- Enhanced process management in manager.go to ensure more reliable process termination.
- Improved UI display methods for better user experience.
- Removed outdated test file generator_test.go to clean up the codebase.
- Updated install.ps1 script for better output formatting and error handling.
This commit is contained in:
Vaggelis kavouras 2024-12-28 23:52:24 +02:00
parent 56f09677ca
commit 947d11fbc6
13 changed files with 541 additions and 502 deletions

View File

@ -58,21 +58,15 @@ this is a mistake.
### 🚀 One-Click Solution ### 🚀 One-Click Solution
<details> **Linux/macOS**: Copy and paste in terminal
<summary><b>Linux/macOS</b>: Copy and paste in terminal</summary>
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash
``` ```
</details>
<details>
<summary><b>Windows</b>: Copy and paste in PowerShell (Admin)</summary>
**Windows**: Copy and paste in PowerShell (Admin)
```powershell ```powershell
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
``` ```
</details>
That's it! The script will: That's it! The script will:
1. ✨ Install the tool automatically 1. ✨ Install the tool automatically
@ -85,23 +79,23 @@ That's it! The script will:
<details> <details>
<summary>Windows Packages</summary> <summary>Windows Packages</summary>
- 64-bit: `cursor-id-modifier_vX.X.X_Windows_x64.zip` - 64-bit: `cursor-id-modifier_windows_x64.exe`
- 32-bit: `cursor-id-modifier_vX.X.X_Windows_x86.zip` - 32-bit: `cursor-id-modifier_windows_x86.exe`
</details> </details>
<details> <details>
<summary>macOS Packages</summary> <summary>macOS Packages</summary>
- Intel: `cursor-id-modifier_vX.X.X_macOS_x64_intel.tar.gz` - Intel: `cursor-id-modifier_darwin_x64_intel`
- M1/M2: `cursor-id-modifier_vX.X.X_macOS_arm64_apple_silicon.tar.gz` - M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon`
</details> </details>
<details> <details>
<summary>Linux Packages</summary> <summary>Linux Packages</summary>
- 64-bit: `cursor-id-modifier_vX.X.X_Linux_x64.tar.gz` - 64-bit: `cursor-id-modifier_linux_x64`
- 32-bit: `cursor-id-modifier_vX.X.X_Linux_x86.tar.gz` - 32-bit: `cursor-id-modifier_linux_x86`
- ARM64: `cursor-id-modifier_vX.X.X_Linux_arm64.tar.gz` - ARM64: `cursor-id-modifier_linux_arm64`
</details> </details>
### 🔧 Technical Details ### 🔧 Technical Details
@ -176,21 +170,15 @@ this is a mistake.
### 🚀 一键解决 ### 🚀 一键解决
<details> **Linux/macOS**: 在终端中复制粘贴
<summary><b>Linux/macOS</b>: 在终端中复制粘贴</summary>
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash
``` ```
</details>
<details>
<summary><b>Windows</b>: 在PowerShell管理员中复制粘贴</summary>
**Windows**: 在PowerShell管理员中复制粘贴
```powershell ```powershell
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
``` ```
</details>
就这样!脚本会: 就这样!脚本会:
1. ✨ 自动安装工具 1. ✨ 自动安装工具

View File

@ -10,7 +10,6 @@ import (
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings" "strings"
"time"
"github.com/dacrab/go-cursor-help/internal/config" "github.com/dacrab/go-cursor-help/internal/config"
"github.com/dacrab/go-cursor-help/internal/lang" "github.com/dacrab/go-cursor-help/internal/lang"
@ -21,6 +20,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// Global variables
var ( var (
version = "dev" version = "dev"
setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode") setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode")
@ -29,7 +29,51 @@ var (
) )
func main() { func main() {
// Initialize error recovery setupErrorRecovery()
handleFlags()
setupLogger()
username := getCurrentUser()
log.Debug("Running as user:", username)
// Initialize components
display := ui.NewDisplay(nil)
configManager := initConfigManager(username)
generator := idgen.NewGenerator()
processManager := process.NewManager(nil, log)
// Check and handle privileges
if err := handlePrivileges(display); err != nil {
return
}
// Setup display
setupDisplay(display)
text := lang.GetText()
// Handle Cursor processes
if err := handleCursorProcesses(display, processManager); err != nil {
return
}
// Handle configuration
oldConfig := readExistingConfig(display, configManager, text)
newConfig := generateNewConfig(display, generator, oldConfig, text)
if err := saveConfiguration(display, configManager, newConfig); err != nil {
return
}
// Show completion messages
showCompletionMessages(display)
if os.Getenv("AUTOMATED_MODE") != "1" {
waitExit()
}
}
func setupErrorRecovery() {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
log.Errorf("Panic recovered: %v\n", r) log.Errorf("Panic recovered: %v\n", r)
@ -37,55 +81,75 @@ func main() {
waitExit() waitExit()
} }
}() }()
// Parse flags
flag.Parse()
// Show version if requested
if *showVersion {
fmt.Printf("Cursor ID Modifier v%s\n", version)
return
} }
// Initialize logger func handleFlags() {
flag.Parse()
if *showVersion {
fmt.Printf("Cursor ID Modifier v%s\n", version)
os.Exit(0)
}
}
func setupLogger() {
log.SetFormatter(&logrus.TextFormatter{ log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true, FullTimestamp: true,
DisableLevelTruncation: true,
PadLevelText: true,
}) })
log.SetLevel(logrus.InfoLevel)
}
func getCurrentUser() string {
if username := os.Getenv("SUDO_USER"); username != "" {
return username
}
// Get current user
username := os.Getenv("SUDO_USER")
if username == "" {
user, err := user.Current() user, err := user.Current()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
username = user.Username return user.Username
} }
// Initialize components func initConfigManager(username string) *config.Manager {
display := ui.NewDisplay(nil)
procManager := process.NewManager(process.DefaultConfig(), log)
configManager, err := config.NewManager(username) configManager, err := config.NewManager(username)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
generator := idgen.NewGenerator() return configManager
}
// Check privileges func handlePrivileges(display *ui.Display) error {
isAdmin, err := checkAdminPrivileges() isAdmin, err := checkAdminPrivileges()
if err != nil { if err != nil {
log.Error(err) log.Error(err)
waitExit() waitExit()
return return err
} }
if !isAdmin { if !isAdmin {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return handleWindowsPrivileges(display)
}
display.ShowPrivilegeError(
lang.GetText().PrivilegeError,
lang.GetText().RunWithSudo,
lang.GetText().SudoExample,
)
waitExit()
return fmt.Errorf("insufficient privileges")
}
return nil
}
func handleWindowsPrivileges(display *ui.Display) error {
message := "\nRequesting administrator privileges..." message := "\nRequesting administrator privileges..."
if lang.GetCurrentLanguage() == lang.CN { if lang.GetCurrentLanguage() == lang.CN {
message = "\n请求管理员权限..." message = "\n请求管理员权限..."
} }
fmt.Println(message) fmt.Println(message)
if err := selfElevate(); err != nil { if err := selfElevate(); err != nil {
log.Error(err) log.Error(err)
display.ShowPrivilegeError( display.ShowPrivilegeError(
@ -95,170 +159,126 @@ func main() {
lang.GetText().SudoExample, lang.GetText().SudoExample,
) )
waitExit() waitExit()
return return err
} }
return return nil
}
display.ShowPrivilegeError(
lang.GetText().PrivilegeError,
lang.GetText().RunAsAdmin,
lang.GetText().RunWithSudo,
lang.GetText().SudoExample,
)
waitExit()
return
} }
// Ensure Cursor is closed func setupDisplay(display *ui.Display) {
if err := ensureCursorClosed(display, procManager); err != nil {
message := "\nError: Please close Cursor manually before running this program."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n错误请在运行此程序之前手动关闭 Cursor。"
}
display.ShowError(message)
waitExit()
return
}
// Kill any remaining Cursor processes
if procManager.IsCursorRunning() {
text := lang.GetText()
display.ShowProcessStatus(text.ClosingProcesses)
if err := procManager.KillCursorProcesses(); err != nil {
fmt.Println()
message := "Warning: Could not close all Cursor instances. Please close them manually."
if lang.GetCurrentLanguage() == lang.CN {
message = "警告:无法关闭所有 Cursor 实例,请手动关闭。"
}
display.ShowWarning(message)
waitExit()
return
}
if procManager.IsCursorRunning() {
fmt.Println()
message := "\nWarning: Cursor is still running. Please close it manually."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n警告Cursor 仍在运行,请手动关闭。"
}
display.ShowWarning(message)
waitExit()
return
}
display.ShowProcessStatus(text.ProcessesClosed)
fmt.Println()
}
// Clear screen
if err := display.ClearScreen(); err != nil { if err := display.ClearScreen(); err != nil {
log.Warn("Failed to clear screen:", err) log.Warn("Failed to clear screen:", err)
} }
// Show logo
display.ShowLogo() display.ShowLogo()
fmt.Println()
}
// Read existing config func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error {
text := lang.GetText() if os.Getenv("AUTOMATED_MODE") == "1" {
log.Debug("Running in automated mode, skipping Cursor process closing")
return nil
}
display.ShowProgress("Closing Cursor...")
log.Debug("Attempting to close Cursor processes")
if err := processManager.KillCursorProcesses(); err != nil {
log.Error("Failed to close Cursor:", err)
display.StopProgress()
display.ShowError("Failed to close Cursor. Please close it manually and try again.")
waitExit()
return err
}
if processManager.IsCursorRunning() {
log.Error("Cursor processes still detected after closing")
display.StopProgress()
display.ShowError("Failed to close Cursor completely. Please close it manually and try again.")
waitExit()
return fmt.Errorf("cursor still running")
}
log.Debug("Successfully closed all Cursor processes")
display.StopProgress()
fmt.Println()
return nil
}
func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig {
fmt.Println()
display.ShowProgress(text.ReadingConfig) display.ShowProgress(text.ReadingConfig)
oldConfig, err := configManager.ReadConfig() oldConfig, err := configManager.ReadConfig()
if err != nil { if err != nil {
log.Warn("Failed to read existing config:", err) log.Warn("Failed to read existing config:", err)
oldConfig = nil oldConfig = nil
} }
display.StopProgress()
fmt.Println()
return oldConfig
}
// Generate new IDs func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig {
display.ShowProgress(text.GeneratingIds) display.ShowProgress(text.GeneratingIds)
newConfig := &config.StorageConfig{}
machineID, err := generator.GenerateMachineID() if machineID, err := generator.GenerateMachineID(); err != nil {
if err != nil {
log.Fatal("Failed to generate machine ID:", err) log.Fatal("Failed to generate machine ID:", err)
} else {
newConfig.TelemetryMachineId = machineID
} }
macMachineID, err := generator.GenerateMacMachineID() if macMachineID, err := generator.GenerateMacMachineID(); err != nil {
if err != nil {
log.Fatal("Failed to generate MAC machine ID:", err) log.Fatal("Failed to generate MAC machine ID:", err)
} else {
newConfig.TelemetryMacMachineId = macMachineID
} }
deviceID, err := generator.GenerateDeviceID() if deviceID, err := generator.GenerateDeviceID(); err != nil {
if err != nil {
log.Fatal("Failed to generate device ID:", err) log.Fatal("Failed to generate device ID:", err)
} } else {
newConfig.TelemetryDevDeviceId = deviceID
// Create new config
newConfig := &config.StorageConfig{
TelemetryMachineId: machineID,
TelemetryMacMachineId: macMachineID,
TelemetryDevDeviceId: deviceID,
} }
if oldConfig != nil && oldConfig.TelemetrySqmId != "" { if oldConfig != nil && oldConfig.TelemetrySqmId != "" {
newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId
} else { } else if sqmID, err := generator.GenerateMacMachineID(); err != nil {
sqmID, err := generator.GenerateMacMachineID()
if err != nil {
log.Fatal("Failed to generate SQM ID:", err) log.Fatal("Failed to generate SQM ID:", err)
} } else {
newConfig.TelemetrySqmId = sqmID newConfig.TelemetrySqmId = sqmID
} }
// Save config display.StopProgress()
fmt.Println()
return newConfig
}
func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error {
display.ShowProgress("Saving configuration...")
if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil { if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil {
log.Error(err) log.Error(err)
waitExit() waitExit()
return return err
} }
display.StopProgress()
// Show success
display.ShowSuccess(text.SuccessMessage, text.RestartMessage)
message := "\nOperation completed!"
if lang.GetCurrentLanguage() == lang.CN {
message = "\n操作完成"
}
display.ShowInfo(message)
if os.Getenv("AUTOMATED_MODE") != "1" {
waitExit()
}
}
func waitExit() {
if os.Getenv("AUTOMATED_MODE") == "1" {
return
}
fmt.Println(lang.GetText().PressEnterToExit)
os.Stdout.Sync()
bufio.NewReader(os.Stdin).ReadString('\n')
}
func ensureCursorClosed(display *ui.Display, procManager *process.Manager) error {
maxAttempts := 3
text := lang.GetText()
display.ShowProcessStatus(text.CheckingProcesses)
for attempt := 1; attempt <= maxAttempts; attempt++ {
if !procManager.IsCursorRunning() {
display.ShowProcessStatus(text.ProcessesClosed)
fmt.Println() fmt.Println()
return nil return nil
} }
message := fmt.Sprintf("Please close Cursor before continuing. Attempt %d/%d\n%s", func showCompletionMessages(display *ui.Display) {
attempt, maxAttempts, text.PleaseWait) display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage)
fmt.Println()
message := "Operation completed!"
if lang.GetCurrentLanguage() == lang.CN { if lang.GetCurrentLanguage() == lang.CN {
message = fmt.Sprintf("请在继续之前关闭 Cursor。尝试 %d/%d\n%s", message = "操作完成!"
attempt, maxAttempts, text.PleaseWait)
} }
display.ShowProcessStatus(message) display.ShowInfo(message)
fmt.Println()
time.Sleep(5 * time.Second)
} }
return fmt.Errorf("cursor is still running") func waitExit() {
fmt.Print(lang.GetText().PressEnterToExit)
os.Stdout.Sync()
bufio.NewReader(os.Stdin).ReadString('\n')
} }
func checkAdminPrivileges() (bool, error) { func checkAdminPrivileges() (bool, error) {

5
go.mod
View File

@ -5,14 +5,11 @@ go 1.21
require ( require (
github.com/fatih/color v1.15.0 github.com/fatih/color v1.15.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

1
go.sum
View File

@ -20,7 +20,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -32,10 +32,7 @@ func NewManager(username string) (*Manager, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get config path: %w", err) return nil, fmt.Errorf("failed to get config path: %w", err)
} }
return &Manager{configPath: configPath}, nil
return &Manager{
configPath: configPath,
}, nil
} }
// ReadConfig reads the existing configuration // ReadConfig reads the existing configuration
@ -69,22 +66,23 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
return fmt.Errorf("failed to create config directory: %w", err) return fmt.Errorf("failed to create config directory: %w", err)
} }
// Set file permissions // Prepare updated configuration
if err := os.Chmod(m.configPath, 0666); err != nil && !os.IsNotExist(err) { updatedConfig := m.prepareUpdatedConfig(config)
return fmt.Errorf("failed to set file permissions: %w", err)
// Write configuration
if err := m.writeConfigFile(updatedConfig, readOnly); err != nil {
return err
} }
// Read existing config to preserve other fields return nil
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{}) // prepareUpdatedConfig merges existing config with updates
func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} {
// Read existing config
originalFile := make(map[string]interface{})
if data, err := os.ReadFile(m.configPath); err == nil {
json.Unmarshal(data, &originalFile)
} }
// Update fields // Update fields
@ -95,15 +93,20 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339)
originalFile["version"] = "1.0.1" originalFile["version"] = "1.0.1"
return originalFile
}
// writeConfigFile handles the atomic write of the config file
func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error {
// Marshal with indentation // Marshal with indentation
newFileContent, err := json.MarshalIndent(originalFile, "", " ") content, err := json.MarshalIndent(config, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal config: %w", err) return fmt.Errorf("failed to marshal config: %w", err)
} }
// Write to temporary file // Write to temporary file
tmpPath := m.configPath + ".tmp" tmpPath := m.configPath + ".tmp"
if err := os.WriteFile(tmpPath, newFileContent, 0666); err != nil { if err := os.WriteFile(tmpPath, content, 0666); err != nil {
return fmt.Errorf("failed to write temporary file: %w", err) return fmt.Errorf("failed to write temporary file: %w", err)
} }
@ -126,8 +129,8 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
// Sync directory // Sync directory
if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil {
defer dir.Close()
dir.Sync() dir.Sync()
dir.Close()
} }
return nil return nil

View File

@ -7,7 +7,7 @@ import (
"sync" "sync"
) )
// Language represents a supported language // Language represents a supported language code
type Language string type Language string
const ( const (
@ -19,22 +19,31 @@ const (
// TextResource contains all translatable text resources // TextResource contains all translatable text resources
type TextResource struct { type TextResource struct {
// Success messages
SuccessMessage string SuccessMessage string
RestartMessage string RestartMessage string
// Progress messages
ReadingConfig string ReadingConfig string
GeneratingIds string GeneratingIds string
PressEnterToExit string
ErrorPrefix string
PrivilegeError string
RunAsAdmin string
RunWithSudo string
SudoExample string
ConfigLocation string
CheckingProcesses string CheckingProcesses string
ClosingProcesses string ClosingProcesses string
ProcessesClosed string ProcessesClosed string
PleaseWait string PleaseWait string
// Error messages
ErrorPrefix string
PrivilegeError string
// Instructions
RunAsAdmin string
RunWithSudo string
SudoExample string
PressEnterToExit string
SetReadOnlyMessage string SetReadOnlyMessage string
// Info messages
ConfigLocation string
} }
var ( var (
@ -68,28 +77,32 @@ func GetText() TextResource {
// detectLanguage detects the system language // detectLanguage detects the system language
func detectLanguage() Language { func detectLanguage() Language {
// Check environment variables // Check environment variables first
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { if isChineseEnvVar() {
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") {
return CN return CN
} }
}
// Check Windows language settings // Then check OS-specific locale
if isWindows() { if isWindows() {
if isWindowsChineseLocale() { if isWindowsChineseLocale() {
return CN return CN
} }
} else { } else if isUnixChineseLocale() {
// Check Unix locale
if isUnixChineseLocale() {
return CN return CN
} }
}
return EN return EN
} }
func isChineseEnvVar() bool {
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} {
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") {
return true
}
}
return false
}
func isWindows() bool { func isWindows() bool {
return os.Getenv("OS") == "Windows_NT" return os.Getenv("OS") == "Windows_NT"
} }
@ -118,39 +131,57 @@ func isUnixChineseLocale() bool {
// texts contains all translations // texts contains all translations
var texts = map[Language]TextResource{ var texts = map[Language]TextResource{
CN: { CN: {
// Success messages
SuccessMessage: "[√] 配置文件已成功更新!", SuccessMessage: "[√] 配置文件已成功更新!",
RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", RestartMessage: "[!] 请手动重启 Cursor 以使更新生效",
// Progress messages
ReadingConfig: "正在读取配置文件...", ReadingConfig: "正在读取配置文件...",
GeneratingIds: "正在生成新的标识符...", GeneratingIds: "正在生成新的标识符...",
PressEnterToExit: "按回车键退出程序...",
ErrorPrefix: "程序发生严重错误: %v",
PrivilegeError: "\n[!] 错误:需要管理员权限",
RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」",
RunWithSudo: "请使用 sudo 命令运行此程序",
SudoExample: "示例: sudo %s",
ConfigLocation: "配置文件位置:",
CheckingProcesses: "正在检查运行中的 Cursor 实例...", CheckingProcesses: "正在检查运行中的 Cursor 实例...",
ClosingProcesses: "正在关闭 Cursor 实例...", ClosingProcesses: "正在关闭 Cursor 实例...",
ProcessesClosed: "所有 Cursor 实例已关闭", ProcessesClosed: "所有 Cursor 实例已关闭",
PleaseWait: "请稍候...", PleaseWait: "请稍候...",
// Error messages
ErrorPrefix: "程序发生严重错误: %v",
PrivilegeError: "\n[!] 错误:需要管理员权限",
// Instructions
RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」",
RunWithSudo: "请使用 sudo 命令运行此程序",
SudoExample: "示例: sudo %s",
PressEnterToExit: "按回车键退出程序...",
SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题",
// Info messages
ConfigLocation: "配置文件位置:",
}, },
EN: { EN: {
// Success messages
SuccessMessage: "[√] Configuration file updated successfully!", SuccessMessage: "[√] Configuration file updated successfully!",
RestartMessage: "[!] Please restart Cursor manually for changes to take effect", RestartMessage: "[!] Please restart Cursor manually for changes to take effect",
// Progress messages
ReadingConfig: "Reading configuration file...", ReadingConfig: "Reading configuration file...",
GeneratingIds: "Generating new identifiers...", 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...", CheckingProcesses: "Checking for running Cursor instances...",
ClosingProcesses: "Closing Cursor instances...", ClosingProcesses: "Closing Cursor instances...",
ProcessesClosed: "All Cursor instances have been closed", ProcessesClosed: "All Cursor instances have been closed",
PleaseWait: "Please wait...", PleaseWait: "Please wait...",
// Error messages
ErrorPrefix: "Program encountered a serious error: %v",
PrivilegeError: "\n[!] Error: Administrator privileges required",
// Instructions
RunAsAdmin: "Please right-click and select 'Run as Administrator'",
RunWithSudo: "Please run this program with sudo",
SudoExample: "Example: sudo %s",
PressEnterToExit: "Press Enter to exit...",
SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records",
// Info messages
ConfigLocation: "Config file location:",
}, },
} }

View File

@ -12,22 +12,24 @@ import (
// Config holds process manager configuration // Config holds process manager configuration
type Config struct { type Config struct {
MaxAttempts int MaxAttempts int // Maximum number of attempts to kill processes
RetryDelay time.Duration RetryDelay time.Duration // Delay between retry attempts
ProcessPatterns []string ProcessPatterns []string // Process names to look for
} }
// DefaultConfig returns the default configuration // DefaultConfig returns the default configuration
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
MaxAttempts: 3, MaxAttempts: 3,
RetryDelay: time.Second, RetryDelay: 2 * time.Second,
ProcessPatterns: []string{ ProcessPatterns: []string{
"Cursor.exe", // Windows "Cursor.exe", // Windows executable
"Cursor", // Linux/macOS binary "Cursor ", // Linux/macOS executable with space
"cursor", // Linux/macOS process "cursor ", // Linux/macOS executable lowercase with space
"cursor-helper", // Helper process "cursor", // Linux/macOS executable lowercase
"cursor-id-modifier", // Our tool "Cursor", // Linux/macOS executable
"*cursor*", // Any process containing cursor
"*Cursor*", // Any process containing Cursor
}, },
} }
} }
@ -38,7 +40,7 @@ type Manager struct {
log *logrus.Logger log *logrus.Logger
} }
// NewManager creates a new process manager // NewManager creates a new process manager with optional config and logger
func NewManager(config *Config, log *logrus.Logger) *Manager { func NewManager(config *Config, log *logrus.Logger) *Manager {
if config == nil { if config == nil {
config = DefaultConfig() config = DefaultConfig()
@ -52,7 +54,7 @@ func NewManager(config *Config, log *logrus.Logger) *Manager {
} }
} }
// IsCursorRunning checks if any Cursor process is running // IsCursorRunning checks if any Cursor process is currently running
func (m *Manager) IsCursorRunning() bool { func (m *Manager) IsCursorRunning() bool {
processes, err := m.getCursorProcesses() processes, err := m.getCursorProcesses()
if err != nil { if err != nil {
@ -62,7 +64,7 @@ func (m *Manager) IsCursorRunning() bool {
return len(processes) > 0 return len(processes) > 0
} }
// KillCursorProcesses attempts to kill all Cursor processes // KillCursorProcesses attempts to kill all running Cursor processes
func (m *Manager) KillCursorProcesses() error { func (m *Manager) KillCursorProcesses() error {
for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ { for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ {
processes, err := m.getCursorProcesses() processes, err := m.getCursorProcesses()
@ -74,34 +76,34 @@ func (m *Manager) KillCursorProcesses() error {
return nil return nil
} }
for _, proc := range processes { // Try graceful shutdown first on Windows
if err := m.killProcess(proc); err != nil { if runtime.GOOS == "windows" {
m.log.Warnf("Failed to kill process %s: %v", proc, err) for _, pid := range processes {
exec.Command("taskkill", "/PID", pid).Run()
time.Sleep(500 * time.Millisecond)
} }
} }
// Force kill remaining processes
remainingProcesses, _ := m.getCursorProcesses()
for _, pid := range remainingProcesses {
m.killProcess(pid)
}
time.Sleep(m.config.RetryDelay) time.Sleep(m.config.RetryDelay)
}
if m.IsCursorRunning() { if processes, _ := m.getCursorProcesses(); len(processes) == 0 {
return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts) return nil
}
} }
return nil return nil
} }
// getCursorProcesses returns PIDs of running Cursor processes
func (m *Manager) getCursorProcesses() ([]string, error) { func (m *Manager) getCursorProcesses() ([]string, error) {
var cmd *exec.Cmd cmd := m.getProcessListCommand()
var processes []string if cmd == nil {
switch runtime.GOOS {
case "windows":
cmd = exec.Command("tasklist", "/FO", "CSV", "/NH")
case "darwin":
cmd = exec.Command("ps", "-ax")
case "linux":
cmd = exec.Command("ps", "-A")
default:
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
} }
@ -110,32 +112,80 @@ func (m *Manager) getCursorProcesses() ([]string, error) {
return nil, fmt.Errorf("failed to execute command: %w", err) return nil, fmt.Errorf("failed to execute command: %w", err)
} }
lines := strings.Split(string(output), "\n") return m.parseProcessList(string(output)), nil
for _, line := range lines { }
for _, pattern := range m.config.ProcessPatterns {
if strings.Contains(strings.ToLower(line), strings.ToLower(pattern)) { // getProcessListCommand returns the appropriate command to list processes based on OS
// Extract PID based on OS func (m *Manager) getProcessListCommand() *exec.Cmd {
pid := m.extractPID(line) switch runtime.GOOS {
if pid != "" { case "windows":
return exec.Command("tasklist", "/FO", "CSV", "/NH")
case "darwin":
return exec.Command("ps", "-ax")
case "linux":
return exec.Command("ps", "-A")
default:
return nil
}
}
// parseProcessList extracts Cursor process PIDs from process list output
func (m *Manager) parseProcessList(output string) []string {
var processes []string
for _, line := range strings.Split(output, "\n") {
lowerLine := strings.ToLower(line)
if m.isOwnProcess(lowerLine) {
continue
}
if pid := m.findCursorProcess(line, lowerLine); pid != "" {
processes = append(processes, pid) processes = append(processes, pid)
} }
} }
return processes
}
// isOwnProcess checks if the process belongs to this application
func (m *Manager) isOwnProcess(line string) bool {
return strings.Contains(line, "cursor-id-modifier") ||
strings.Contains(line, "cursor-helper")
}
// findCursorProcess checks if a process line matches Cursor patterns and returns its PID
func (m *Manager) findCursorProcess(line, lowerLine string) string {
for _, pattern := range m.config.ProcessPatterns {
if m.matchPattern(lowerLine, strings.ToLower(pattern)) {
return m.extractPID(line)
}
}
return ""
}
// matchPattern checks if a line matches a pattern, supporting wildcards
func (m *Manager) matchPattern(line, pattern string) bool {
switch {
case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"):
search := pattern[1 : len(pattern)-1]
return strings.Contains(line, search)
case strings.HasPrefix(pattern, "*"):
return strings.HasSuffix(line, pattern[1:])
case strings.HasSuffix(pattern, "*"):
return strings.HasPrefix(line, pattern[:len(pattern)-1])
default:
return line == pattern
} }
} }
return processes, nil // extractPID extracts process ID from a process list line based on OS format
}
func (m *Manager) extractPID(line string) string { func (m *Manager) extractPID(line string) string {
switch runtime.GOOS { switch runtime.GOOS {
case "windows": case "windows":
// Windows CSV format: "ImageName","PID",...
parts := strings.Split(line, ",") parts := strings.Split(line, ",")
if len(parts) >= 2 { if len(parts) >= 2 {
return strings.Trim(parts[1], "\"") return strings.Trim(parts[1], "\"")
} }
case "darwin", "linux": case "darwin", "linux":
// Unix format: PID TTY TIME CMD
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) >= 1 { if len(parts) >= 1 {
return parts[0] return parts[0]
@ -144,17 +194,23 @@ func (m *Manager) extractPID(line string) string {
return "" return ""
} }
// killProcess forcefully terminates a process by PID
func (m *Manager) killProcess(pid string) error { func (m *Manager) killProcess(pid string) error {
var cmd *exec.Cmd cmd := m.getKillCommand(pid)
if cmd == nil {
switch runtime.GOOS {
case "windows":
cmd = exec.Command("taskkill", "/F", "/PID", pid)
case "darwin", "linux":
cmd = exec.Command("kill", "-9", pid)
default:
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
} }
return cmd.Run() return cmd.Run()
} }
// getKillCommand returns the appropriate command to kill a process based on OS
func (m *Manager) getKillCommand(pid string) *exec.Cmd {
switch runtime.GOOS {
case "windows":
return exec.Command("taskkill", "/F", "/PID", pid)
case "darwin", "linux":
return exec.Command("kill", "-9", pid)
default:
return nil
}
}

View File

@ -10,22 +10,37 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
) )
// Display handles UI display operations // Display handles UI operations for terminal output
type Display struct { type Display struct {
spinner *Spinner spinner *Spinner
} }
// NewDisplay creates a new display handler // NewDisplay creates a new display instance with an optional spinner
func NewDisplay(spinner *Spinner) *Display { func NewDisplay(spinner *Spinner) *Display {
if spinner == nil { if spinner == nil {
spinner = NewSpinner(nil) spinner = NewSpinner(nil)
} }
return &Display{ return &Display{spinner: spinner}
spinner: spinner,
}
} }
// ShowProgress shows a progress message with spinner // Terminal Operations
// ClearScreen clears the terminal screen based on OS
func (d *Display) ClearScreen() error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "cls")
default:
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
return cmd.Run()
}
// Progress Indicator
// ShowProgress displays a progress message with a spinner
func (d *Display) ShowProgress(message string) { func (d *Display) ShowProgress(message string) {
d.spinner.SetMessage(message) d.spinner.SetMessage(message)
d.spinner.Start() d.spinner.Start()
@ -36,66 +51,44 @@ func (d *Display) StopProgress() {
d.spinner.Stop() d.spinner.Stop()
} }
// ClearScreen clears the terminal screen // Message Display
func (d *Display) ClearScreen() error {
var cmd *exec.Cmd // ShowSuccess displays success messages in green
if runtime.GOOS == "windows" { func (d *Display) ShowSuccess(messages ...string) {
cmd = exec.Command("cmd", "/c", "cls") green := color.New(color.FgGreen)
} else { for _, msg := range messages {
cmd = exec.Command("clear") green.Println(msg)
} }
cmd.Stdout = os.Stdout
return cmd.Run()
} }
// ShowProcessStatus shows the current process status // ShowInfo displays an info message in cyan
func (d *Display) ShowProcessStatus(message string) { func (d *Display) ShowInfo(message string) {
fmt.Printf("\r%s", strings.Repeat(" ", 80)) // Clear line cyan := color.New(color.FgCyan)
fmt.Printf("\r%s", color.CyanString("⚡ "+message)) cyan.Println(message)
} }
// ShowPrivilegeError shows the privilege error message // ShowError displays an error message in red
func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) { func (d *Display) ShowError(message string) {
red := color.New(color.FgRed)
red.Println(message)
}
// ShowPrivilegeError displays privilege error messages with instructions
func (d *Display) ShowPrivilegeError(messages ...string) {
red := color.New(color.FgRed, color.Bold) red := color.New(color.FgRed, color.Bold)
yellow := color.New(color.FgYellow) yellow := color.New(color.FgYellow)
red.Println(errorMsg) // Main error message
if runtime.GOOS == "windows" { red.Println(messages[0])
yellow.Println(adminMsg) fmt.Println()
// Additional instructions
for _, msg := range messages[1:] {
if strings.Contains(msg, "%s") {
exe, _ := os.Executable()
yellow.Printf(msg+"\n", exe)
} else { } else {
yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0])) yellow.Println(msg)
} }
} }
// 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()
} }

View File

@ -5,15 +5,15 @@ import (
) )
const cyberpunkLogo = ` const cyberpunkLogo = `
______ ______ ______
/ ____/_ __________ ___ _____/ __/ // / / /
/ / / / / / ___/ _ \/ __ \/ ___/ /_/ // /_/ /
/ /___/ /_/ / / / __/ /_/ (__ ) __/__ __/ /
\____/\__,_/_/ \___/\____/____/_/ /_/ /_/
` `
// ShowLogo displays the cyberpunk-style logo // ShowLogo displays the application logo
func (d *Display) ShowLogo() { func (d *Display) ShowLogo() {
cyan := color.New(color.FgCyan, color.Bold) cyan := color.New(color.FgCyan, color.Bold)
cyan.Println(cyberpunkLogo) cyan.Println(cyberpunkLogo)

View File

@ -10,8 +10,8 @@ import (
// SpinnerConfig defines spinner configuration // SpinnerConfig defines spinner configuration
type SpinnerConfig struct { type SpinnerConfig struct {
Frames []string Frames []string // Animation frames for the spinner
Delay time.Duration Delay time.Duration // Delay between frame updates
} }
// DefaultSpinnerConfig returns the default spinner configuration // DefaultSpinnerConfig returns the default spinner configuration
@ -43,6 +43,8 @@ func NewSpinner(config *SpinnerConfig) *Spinner {
} }
} }
// State management
// SetMessage sets the spinner message // SetMessage sets the spinner message
func (s *Spinner) SetMessage(message string) { func (s *Spinner) SetMessage(message string) {
s.mu.Lock() s.mu.Lock()
@ -50,7 +52,16 @@ func (s *Spinner) SetMessage(message string) {
s.message = message s.message = message
} }
// Start starts the spinner animation // IsActive returns whether the spinner is currently active
func (s *Spinner) IsActive() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.active
}
// Control methods
// Start begins the spinner animation
func (s *Spinner) Start() { func (s *Spinner) Start() {
s.mu.Lock() s.mu.Lock()
if s.active { if s.active {
@ -63,7 +74,7 @@ func (s *Spinner) Start() {
go s.run() go s.run()
} }
// Stop stops the spinner animation // Stop halts the spinner animation
func (s *Spinner) Stop() { func (s *Spinner) Stop() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -75,20 +86,21 @@ func (s *Spinner) Stop() {
s.active = false s.active = false
close(s.stopCh) close(s.stopCh)
s.stopCh = make(chan struct{}) s.stopCh = make(chan struct{})
fmt.Println() fmt.Print("\r") // Clear the spinner line
} }
// IsActive returns whether the spinner is currently active // Internal methods
func (s *Spinner) IsActive() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.active
}
func (s *Spinner) run() { func (s *Spinner) run() {
ticker := time.NewTicker(s.config.Delay) ticker := time.NewTicker(s.config.Delay)
defer ticker.Stop() defer ticker.Stop()
cyan := color.New(color.FgCyan, color.Bold)
message := s.message
// Print initial state
fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message)
for { for {
select { select {
case <-s.stopCh: case <-s.stopCh:
@ -100,11 +112,11 @@ func (s *Spinner) run() {
return return
} }
frame := s.config.Frames[s.current%len(s.config.Frames)] frame := s.config.Frames[s.current%len(s.config.Frames)]
message := s.message
s.current++ s.current++
s.mu.RUnlock() s.mu.RUnlock()
fmt.Printf("\r%s %s", color.CyanString(frame), message) fmt.Printf("\r %s", cyan.Sprint(frame))
fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message
} }
} }
} }

View File

@ -1,95 +1,59 @@
package idgen package idgen
import ( import (
cryptorand "crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/big" "time"
"sync"
) )
// Generator handles the generation of various IDs // Generator handles secure ID generation for machines and devices
type Generator struct { type Generator struct{}
charsetMu sync.RWMutex
charset string
}
// NewGenerator creates a new ID generator with default settings // NewGenerator creates a new ID generator
func NewGenerator() *Generator { func NewGenerator() *Generator {
return &Generator{ return &Generator{}
charset: "0123456789ABCDEFGHJKLMNPQRSTVWXYZ",
}
} }
// SetCharset allows customizing the character set used for ID generation // Helper methods
func (g *Generator) SetCharset(charset string) { // -------------
g.charsetMu.Lock()
defer g.charsetMu.Unlock() // simulateWork adds a small delay to make progress visible
g.charset = charset func (g *Generator) simulateWork() {
time.Sleep(800 * time.Millisecond)
} }
// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id] // generateRandomHex generates a random hex string of specified length
func (g *Generator) generateRandomHex(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// Public methods
// -------------
// GenerateMachineID generates a new 32-byte machine ID
func (g *Generator) GenerateMachineID() (string, error) { func (g *Generator) GenerateMachineID() (string, error) {
prefix := "auth0|user_" g.simulateWork()
return g.generateRandomHex(32)
// 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 // GenerateMacMachineID generates a new 64-byte MAC machine ID
return hex.EncodeToString([]byte(fullID)), nil
}
// GenerateMacMachineID generates a new MAC machine ID using SHA-256
func (g *Generator) GenerateMacMachineID() (string, error) { func (g *Generator) GenerateMacMachineID() (string, error) {
data := make([]byte, 32) g.simulateWork()
if _, err := cryptorand.Read(data); err != nil { return g.generateRandomHex(64)
return "", fmt.Errorf("failed to generate random data: %w", err)
} }
hash := sha256.Sum256(data) // GenerateDeviceID generates a new device ID in UUID format
return hex.EncodeToString(hash[:]), nil
}
// GenerateDeviceID generates a new device ID in UUID v4 format
func (g *Generator) GenerateDeviceID() (string, error) { func (g *Generator) GenerateDeviceID() (string, error) {
uuid := make([]byte, 16) g.simulateWork()
if _, err := cryptorand.Read(uuid); err != nil { id, err := g.generateRandomHex(16)
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 { if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err) return "", err
} }
result[i] = g.charset[randNum.Int64()] return fmt.Sprintf("%s-%s-%s-%s-%s",
} id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil
return string(result), nil
} }

View File

@ -1,26 +0,0 @@
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")
}

View File

@ -9,13 +9,6 @@ if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdenti
# Set TLS to 1.2 # Set TLS to 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Colors for output
$Red = "`e[31m"
$Green = "`e[32m"
$Blue = "`e[36m"
$Yellow = "`e[33m"
$Reset = "`e[0m"
# Create temporary directory # Create temporary directory
$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) $TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $TmpDir | Out-Null New-Item -ItemType Directory -Path $TmpDir | Out-Null
@ -29,7 +22,7 @@ function Cleanup {
# Error handler # Error handler
trap { trap {
Write-Host "${Red}Error: $_${Reset}" Write-Host "Error: $_" -ForegroundColor Red
Cleanup Cleanup
exit 1 exit 1
} }
@ -44,7 +37,7 @@ function Get-SystemArch {
} }
# Download with progress # Download with progress
function Download-WithProgress { function Get-FileWithProgress {
param ( param (
[string]$Url, [string]$Url,
[string]$OutputFile [string]$OutputFile
@ -58,18 +51,18 @@ function Download-WithProgress {
return $true return $true
} }
catch { catch {
Write-Host "${Red}Failed to download: $_${Reset}" Write-Host "Failed to download: $_" -ForegroundColor Red
return $false return $false
} }
} }
# Main installation function # Main installation function
function Install-CursorModifier { function Install-CursorModifier {
Write-Host "${Blue}Starting installation...${Reset}" Write-Host "Starting installation..." -ForegroundColor Cyan
# Detect architecture # Detect architecture
$arch = Get-SystemArch $arch = Get-SystemArch
Write-Host "${Green}Detected architecture: $arch${Reset}" Write-Host "Detected architecture: $arch" -ForegroundColor Green
# Set installation directory # Set installation directory
$InstallDir = "$env:ProgramFiles\CursorModifier" $InstallDir = "$env:ProgramFiles\CursorModifier"
@ -80,28 +73,36 @@ function Install-CursorModifier {
# Get latest release # Get latest release
try { try {
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/dacrab/go-cursor-help/releases/latest" $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/dacrab/go-cursor-help/releases/latest"
Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan
# Updated binary name format to match actual assets
$binaryName = "cursor-id-modifier_windows_$arch.exe" $binaryName = "cursor-id-modifier_windows_$arch.exe"
$downloadUrl = $latestRelease.assets | Where-Object { $_.name -eq $binaryName } | Select-Object -ExpandProperty browser_download_url Write-Host "Looking for asset: $binaryName" -ForegroundColor Cyan
$asset = $latestRelease.assets | Where-Object { $_.name -eq $binaryName }
$downloadUrl = $asset.browser_download_url
if (!$downloadUrl) { if (!$downloadUrl) {
Write-Host "Available assets:" -ForegroundColor Yellow
$latestRelease.assets | ForEach-Object { Write-Host $_.name }
throw "Could not find download URL for $binaryName" throw "Could not find download URL for $binaryName"
} }
} }
catch { catch {
Write-Host "${Red}Failed to get latest release: $_${Reset}" Write-Host "Failed to get latest release: $_" -ForegroundColor Red
exit 1 exit 1
} }
# Download binary # Download binary
Write-Host "${Blue}Downloading latest release...${Reset}" Write-Host "Downloading latest release from $downloadUrl..." -ForegroundColor Cyan
$binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe" $binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe"
if (!(Download-WithProgress -Url $downloadUrl -OutputFile $binaryPath)) { if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) {
exit 1 exit 1
} }
# Install binary # Install binary
Write-Host "${Blue}Installing...${Reset}" Write-Host "Installing..." -ForegroundColor Cyan
try { try {
Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force
@ -112,24 +113,23 @@ function Install-CursorModifier {
} }
} }
catch { catch {
Write-Host "${Red}Failed to install: $_${Reset}" Write-Host "Failed to install: $_" -ForegroundColor Red
exit 1 exit 1
} }
Write-Host "${Green}Installation completed successfully!${Reset}" Write-Host "Installation completed successfully!" -ForegroundColor Green
Write-Host "${Blue}Running cursor-id-modifier...${Reset}" Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan
# Run the program # Run the program
try { try {
$env:AUTOMATED_MODE = "1"
& "$InstallDir\cursor-id-modifier.exe" & "$InstallDir\cursor-id-modifier.exe"
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "${Red}Failed to run cursor-id-modifier${Reset}" Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red
exit 1 exit 1
} }
} }
catch { catch {
Write-Host "${Red}Failed to run cursor-id-modifier: $_${Reset}" Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red
exit 1 exit 1
} }
} }
@ -140,4 +140,6 @@ try {
} }
finally { finally {
Cleanup Cleanup
Write-Host "Press any key to continue..."
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
} }