diff --git a/README.md b/README.md index f009477..2d4eea2 100644 --- a/README.md +++ b/README.md @@ -58,21 +58,15 @@ this is a mistake. ### 🚀 One-Click Solution -
-Linux/macOS: Copy and paste in terminal - +**Linux/macOS**: Copy and paste in terminal ```bash curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash ``` -
- -
-Windows: Copy and paste in PowerShell (Admin) +**Windows**: Copy and paste in PowerShell (Admin) ```powershell irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex ``` -
That's it! The script will: 1. ✨ Install the tool automatically @@ -85,23 +79,23 @@ That's it! The script will:
Windows Packages -- 64-bit: `cursor-id-modifier_vX.X.X_Windows_x64.zip` -- 32-bit: `cursor-id-modifier_vX.X.X_Windows_x86.zip` +- 64-bit: `cursor-id-modifier_windows_x64.exe` +- 32-bit: `cursor-id-modifier_windows_x86.exe`
macOS Packages -- Intel: `cursor-id-modifier_vX.X.X_macOS_x64_intel.tar.gz` -- M1/M2: `cursor-id-modifier_vX.X.X_macOS_arm64_apple_silicon.tar.gz` +- Intel: `cursor-id-modifier_darwin_x64_intel` +- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon`
Linux Packages -- 64-bit: `cursor-id-modifier_vX.X.X_Linux_x64.tar.gz` -- 32-bit: `cursor-id-modifier_vX.X.X_Linux_x86.tar.gz` -- ARM64: `cursor-id-modifier_vX.X.X_Linux_arm64.tar.gz` +- 64-bit: `cursor-id-modifier_linux_x64` +- 32-bit: `cursor-id-modifier_linux_x86` +- ARM64: `cursor-id-modifier_linux_arm64`
### 🔧 Technical Details @@ -176,21 +170,15 @@ this is a mistake. ### 🚀 一键解决 -
-Linux/macOS: 在终端中复制粘贴 - +**Linux/macOS**: 在终端中复制粘贴 ```bash curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash ``` -
- -
-Windows: 在PowerShell(管理员)中复制粘贴 +**Windows**: 在PowerShell(管理员)中复制粘贴 ```powershell irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex ``` -
就这样!脚本会: 1. ✨ 自动安装工具 diff --git a/cmd/cursor-id-modifier/main.go b/cmd/cursor-id-modifier/main.go index ea846ea..856a816 100644 --- a/cmd/cursor-id-modifier/main.go +++ b/cmd/cursor-id-modifier/main.go @@ -10,7 +10,6 @@ import ( "runtime" "runtime/debug" "strings" - "time" "github.com/dacrab/go-cursor-help/internal/config" "github.com/dacrab/go-cursor-help/internal/lang" @@ -21,6 +20,7 @@ import ( "github.com/sirupsen/logrus" ) +// Global variables var ( version = "dev" setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode") @@ -29,7 +29,51 @@ var ( ) 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() { if r := recover(); r != nil { log.Errorf("Panic recovered: %v\n", r) @@ -37,230 +81,206 @@ func main() { waitExit() } }() +} - // Parse flags +func handleFlags() { flag.Parse() - - // Show version if requested if *showVersion { fmt.Printf("Cursor ID Modifier v%s\n", version) - return + os.Exit(0) } +} - // Initialize logger +func setupLogger() { log.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, + FullTimestamp: true, + DisableLevelTruncation: true, + PadLevelText: true, }) + log.SetLevel(logrus.InfoLevel) +} - // Get current user - username := os.Getenv("SUDO_USER") - if username == "" { - user, err := user.Current() - if err != nil { - log.Fatal(err) - } - username = user.Username +func getCurrentUser() string { + if username := os.Getenv("SUDO_USER"); username != "" { + return username } - // Initialize components - display := ui.NewDisplay(nil) - procManager := process.NewManager(process.DefaultConfig(), log) + user, err := user.Current() + if err != nil { + log.Fatal(err) + } + return user.Username +} + +func initConfigManager(username string) *config.Manager { configManager, err := config.NewManager(username) if err != nil { log.Fatal(err) } - generator := idgen.NewGenerator() + return configManager +} - // Check privileges +func handlePrivileges(display *ui.Display) error { isAdmin, err := checkAdminPrivileges() if err != nil { log.Error(err) waitExit() - return + return err } if !isAdmin { if runtime.GOOS == "windows" { - message := "\nRequesting administrator privileges..." - if lang.GetCurrentLanguage() == lang.CN { - message = "\n请求管理员权限..." - } - fmt.Println(message) - if err := selfElevate(); err != nil { - log.Error(err) - display.ShowPrivilegeError( - lang.GetText().PrivilegeError, - lang.GetText().RunAsAdmin, - lang.GetText().RunWithSudo, - lang.GetText().SudoExample, - ) - waitExit() - return - } - return + return handleWindowsPrivileges(display) } display.ShowPrivilegeError( lang.GetText().PrivilegeError, - lang.GetText().RunAsAdmin, lang.GetText().RunWithSudo, lang.GetText().SudoExample, ) waitExit() - return + return fmt.Errorf("insufficient privileges") } + return nil +} - // Ensure Cursor is closed - 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) +func handleWindowsPrivileges(display *ui.Display) error { + message := "\nRequesting administrator privileges..." + if lang.GetCurrentLanguage() == lang.CN { + message = "\n请求管理员权限..." + } + fmt.Println(message) + + if err := selfElevate(); err != nil { + log.Error(err) + display.ShowPrivilegeError( + lang.GetText().PrivilegeError, + lang.GetText().RunAsAdmin, + lang.GetText().RunWithSudo, + lang.GetText().SudoExample, + ) waitExit() - return + return err } + return nil +} - // 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 - } +func setupDisplay(display *ui.Display) { + if err := display.ClearScreen(); err != nil { + log.Warn("Failed to clear screen:", err) + } + display.ShowLogo() + fmt.Println() +} - 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 - } +func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error { + if os.Getenv("AUTOMATED_MODE") == "1" { + log.Debug("Running in automated mode, skipping Cursor process closing") + return nil + } + + display.ShowProgress("Closing Cursor...") + log.Debug("Attempting to close Cursor processes") - display.ShowProcessStatus(text.ProcessesClosed) - fmt.Println() + 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 } - // Clear screen - if err := display.ClearScreen(); err != nil { - log.Warn("Failed to clear screen:", 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") } - // Show logo - display.ShowLogo() + log.Debug("Successfully closed all Cursor processes") + display.StopProgress() + fmt.Println() + return nil +} - // Read existing config - text := lang.GetText() +func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig { + fmt.Println() display.ShowProgress(text.ReadingConfig) - oldConfig, err := configManager.ReadConfig() if err != nil { log.Warn("Failed to read existing config:", err) oldConfig = nil } + display.StopProgress() + fmt.Println() + return oldConfig +} - // Generate new IDs +func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig { display.ShowProgress(text.GeneratingIds) + newConfig := &config.StorageConfig{} - machineID, err := generator.GenerateMachineID() - if err != nil { + if machineID, err := generator.GenerateMachineID(); err != nil { log.Fatal("Failed to generate machine ID:", err) + } else { + newConfig.TelemetryMachineId = machineID } - macMachineID, err := generator.GenerateMacMachineID() - if err != nil { + if macMachineID, err := generator.GenerateMacMachineID(); err != nil { log.Fatal("Failed to generate MAC machine ID:", err) + } else { + newConfig.TelemetryMacMachineId = macMachineID } - deviceID, err := generator.GenerateDeviceID() - if err != nil { + if deviceID, err := generator.GenerateDeviceID(); err != nil { log.Fatal("Failed to generate device ID:", err) - } - - // Create new config - newConfig := &config.StorageConfig{ - TelemetryMachineId: machineID, - TelemetryMacMachineId: macMachineID, - TelemetryDevDeviceId: deviceID, + } else { + newConfig.TelemetryDevDeviceId = deviceID } if oldConfig != nil && oldConfig.TelemetrySqmId != "" { newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId + } else if sqmID, err := generator.GenerateMacMachineID(); err != nil { + log.Fatal("Failed to generate SQM ID:", err) } else { - sqmID, err := generator.GenerateMacMachineID() - if err != nil { - log.Fatal("Failed to generate SQM ID:", err) - } 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 { log.Error(err) waitExit() - return + return err } + display.StopProgress() + fmt.Println() + return nil +} + +func showCompletionMessages(display *ui.Display) { + display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage) + fmt.Println() - // Show success - display.ShowSuccess(text.SuccessMessage, text.RestartMessage) - message := "\nOperation completed!" + message := "Operation completed!" if lang.GetCurrentLanguage() == lang.CN { - message = "\n操作完成!" + message = "操作完成!" } display.ShowInfo(message) - - if os.Getenv("AUTOMATED_MODE") != "1" { - waitExit() - } + fmt.Println() } func waitExit() { - if os.Getenv("AUTOMATED_MODE") == "1" { - return - } - - fmt.Println(lang.GetText().PressEnterToExit) + fmt.Print(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() - return nil - } - - message := fmt.Sprintf("Please close Cursor before continuing. Attempt %d/%d\n%s", - attempt, maxAttempts, text.PleaseWait) - if lang.GetCurrentLanguage() == lang.CN { - message = fmt.Sprintf("请在继续之前关闭 Cursor。尝试 %d/%d\n%s", - attempt, maxAttempts, text.PleaseWait) - } - display.ShowProcessStatus(message) - - time.Sleep(5 * time.Second) - } - - return fmt.Errorf("cursor is still running") -} - func checkAdminPrivileges() (bool, error) { switch runtime.GOOS { case "windows": diff --git a/go.mod b/go.mod index 8bf96e2..d3192c7 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,11 @@ go 1.21 require ( github.com/fatih/color v1.15.0 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.10.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // 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 - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 75c1611..d930a66 100644 --- a/go.sum +++ b/go.sum @@ -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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/config.go b/internal/config/config.go index e3f3f53..b3d2c81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -32,10 +32,7 @@ func NewManager(username string) (*Manager, error) { if err != nil { 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 @@ -69,22 +66,23 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { 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) + // Prepare updated configuration + updatedConfig := m.prepareUpdatedConfig(config) + + // Write configuration + if err := m.writeConfigFile(updatedConfig, readOnly); err != nil { + return 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{}) + return nil +} + +// prepareUpdatedConfig merges existing config with updates +func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} { + // Read existing config + originalFile := make(map[string]interface{}) + if data, err := os.ReadFile(m.configPath); err == nil { + json.Unmarshal(data, &originalFile) } // Update fields @@ -95,15 +93,20 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) originalFile["version"] = "1.0.1" + return originalFile +} + +// writeConfigFile handles the atomic write of the config file +func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error { // Marshal with indentation - newFileContent, err := json.MarshalIndent(originalFile, "", " ") + content, err := json.MarshalIndent(config, "", " ") if err != nil { return fmt.Errorf("failed to marshal config: %w", err) } // Write to temporary file tmpPath := m.configPath + ".tmp" - if err := os.WriteFile(tmpPath, newFileContent, 0666); err != nil { + if err := os.WriteFile(tmpPath, content, 0666); err != nil { 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 if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { + defer dir.Close() dir.Sync() - dir.Close() } return nil diff --git a/internal/lang/lang.go b/internal/lang/lang.go index f5d8a7a..5b6ddde 100644 --- a/internal/lang/lang.go +++ b/internal/lang/lang.go @@ -7,34 +7,43 @@ import ( "sync" ) -// Language represents a supported language +// Language represents a supported language code type Language string const ( // CN represents Chinese language CN Language = "cn" - // EN represents English language + // 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 + // Success messages + SuccessMessage string + RestartMessage string + + // Progress messages + ReadingConfig string + GeneratingIds string + CheckingProcesses string + ClosingProcesses string + ProcessesClosed string + PleaseWait string + + // Error messages + ErrorPrefix string + PrivilegeError string + + // Instructions RunAsAdmin string RunWithSudo string SudoExample string - ConfigLocation string - CheckingProcesses string - ClosingProcesses string - ProcessesClosed string - PleaseWait string + PressEnterToExit string SetReadOnlyMessage string + + // Info messages + ConfigLocation string } var ( @@ -68,28 +77,32 @@ func GetText() TextResource { // 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 environment variables first + if isChineseEnvVar() { + return CN } - // Check Windows language settings + // Then check OS-specific locale if isWindows() { if isWindowsChineseLocale() { return CN } - } else { - // Check Unix locale - if isUnixChineseLocale() { - return CN - } + } else if isUnixChineseLocale() { + return CN } return EN } +func isChineseEnvVar() bool { + for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} { + if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") { + return true + } + } + return false +} + func isWindows() bool { return os.Getenv("OS") == "Windows_NT" } @@ -118,39 +131,57 @@ func isUnixChineseLocale() bool { // texts contains all translations var texts = map[Language]TextResource{ CN: { - SuccessMessage: "[√] 配置文件已成功更新!", - RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", - ReadingConfig: "正在读取配置文件...", - GeneratingIds: "正在生成新的标识符...", - PressEnterToExit: "按回车键退出程序...", - ErrorPrefix: "程序发生严重错误: %v", - PrivilegeError: "\n[!] 错误:需要管理员权限", + // Success messages + SuccessMessage: "[√] 配置文件已成功更新!", + RestartMessage: "[!] 请手动重启 Cursor 以使更新生效", + + // Progress messages + ReadingConfig: "正在读取配置文件...", + GeneratingIds: "正在生成新的标识符...", + CheckingProcesses: "正在检查运行中的 Cursor 实例...", + ClosingProcesses: "正在关闭 Cursor 实例...", + ProcessesClosed: "所有 Cursor 实例已关闭", + PleaseWait: "请稍候...", + + // Error messages + ErrorPrefix: "程序发生严重错误: %v", + PrivilegeError: "\n[!] 错误:需要管理员权限", + + // Instructions RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」", RunWithSudo: "请使用 sudo 命令运行此程序", SudoExample: "示例: sudo %s", - ConfigLocation: "配置文件位置:", - CheckingProcesses: "正在检查运行中的 Cursor 实例...", - ClosingProcesses: "正在关闭 Cursor 实例...", - ProcessesClosed: "所有 Cursor 实例已关闭", - PleaseWait: "请稍候...", + PressEnterToExit: "按回车键退出程序...", SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", + + // Info messages + ConfigLocation: "配置文件位置:", }, 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", + // Success messages + SuccessMessage: "[√] Configuration file updated successfully!", + RestartMessage: "[!] Please restart Cursor manually for changes to take effect", + + // Progress messages + ReadingConfig: "Reading configuration file...", + GeneratingIds: "Generating new identifiers...", + CheckingProcesses: "Checking for running Cursor instances...", + ClosingProcesses: "Closing Cursor instances...", + ProcessesClosed: "All Cursor instances have been closed", + PleaseWait: "Please wait...", + + // Error messages + ErrorPrefix: "Program encountered a serious error: %v", + PrivilegeError: "\n[!] Error: Administrator privileges required", + + // Instructions RunAsAdmin: "Please right-click and select 'Run as Administrator'", RunWithSudo: "Please run this program with sudo", SudoExample: "Example: sudo %s", - ConfigLocation: "Config file location:", - CheckingProcesses: "Checking for running Cursor instances...", - ClosingProcesses: "Closing Cursor instances...", - ProcessesClosed: "All Cursor instances have been closed", - PleaseWait: "Please wait...", + PressEnterToExit: "Press Enter to exit...", SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records", + + // Info messages + ConfigLocation: "Config file location:", }, } diff --git a/internal/process/manager.go b/internal/process/manager.go index 00d568a..f48ac20 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -12,22 +12,24 @@ import ( // Config holds process manager configuration type Config struct { - MaxAttempts int - RetryDelay time.Duration - ProcessPatterns []string + MaxAttempts int // Maximum number of attempts to kill processes + RetryDelay time.Duration // Delay between retry attempts + ProcessPatterns []string // Process names to look for } // DefaultConfig returns the default configuration func DefaultConfig() *Config { return &Config{ MaxAttempts: 3, - RetryDelay: time.Second, + RetryDelay: 2 * time.Second, ProcessPatterns: []string{ - "Cursor.exe", // Windows - "Cursor", // Linux/macOS binary - "cursor", // Linux/macOS process - "cursor-helper", // Helper process - "cursor-id-modifier", // Our tool + "Cursor.exe", // Windows executable + "Cursor ", // Linux/macOS executable with space + "cursor ", // Linux/macOS executable lowercase with space + "cursor", // Linux/macOS executable lowercase + "Cursor", // Linux/macOS executable + "*cursor*", // Any process containing cursor + "*Cursor*", // Any process containing Cursor }, } } @@ -38,7 +40,7 @@ type Manager struct { 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 { if config == nil { 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 { processes, err := m.getCursorProcesses() if err != nil { @@ -62,7 +64,7 @@ func (m *Manager) IsCursorRunning() bool { return len(processes) > 0 } -// KillCursorProcesses attempts to kill all Cursor processes +// KillCursorProcesses attempts to kill all running Cursor processes func (m *Manager) KillCursorProcesses() error { for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ { processes, err := m.getCursorProcesses() @@ -74,68 +76,116 @@ func (m *Manager) KillCursorProcesses() error { return nil } - for _, proc := range processes { - if err := m.killProcess(proc); err != nil { - m.log.Warnf("Failed to kill process %s: %v", proc, err) + // Try graceful shutdown first on Windows + if runtime.GOOS == "windows" { + for _, pid := range processes { + exec.Command("taskkill", "/PID", pid).Run() + time.Sleep(500 * time.Millisecond) } } + // Force kill remaining processes + remainingProcesses, _ := m.getCursorProcesses() + for _, pid := range remainingProcesses { + m.killProcess(pid) + } + time.Sleep(m.config.RetryDelay) - } - if m.IsCursorRunning() { - return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts) + if processes, _ := m.getCursorProcesses(); len(processes) == 0 { + return nil + } } return nil } +// getCursorProcesses returns PIDs of running Cursor processes func (m *Manager) getCursorProcesses() ([]string, error) { - var cmd *exec.Cmd - var processes []string + cmd := m.getProcessListCommand() + if cmd == nil { + return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute command: %w", err) + } + + return m.parseProcessList(string(output)), nil +} +// getProcessListCommand returns the appropriate command to list processes based on OS +func (m *Manager) getProcessListCommand() *exec.Cmd { switch runtime.GOOS { case "windows": - cmd = exec.Command("tasklist", "/FO", "CSV", "/NH") + return exec.Command("tasklist", "/FO", "CSV", "/NH") case "darwin": - cmd = exec.Command("ps", "-ax") + return exec.Command("ps", "-ax") case "linux": - cmd = exec.Command("ps", "-A") + return exec.Command("ps", "-A") default: - return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + return nil } +} - output, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("failed to execute command: %w", err) +// parseProcessList extracts Cursor process PIDs from process list output +func (m *Manager) parseProcessList(output string) []string { + var processes []string + for _, line := range strings.Split(output, "\n") { + lowerLine := strings.ToLower(line) + + if m.isOwnProcess(lowerLine) { + continue + } + + if pid := m.findCursorProcess(line, lowerLine); pid != "" { + processes = append(processes, pid) + } } + return processes +} - lines := strings.Split(string(output), "\n") - for _, line := range lines { - for _, pattern := range m.config.ProcessPatterns { - if strings.Contains(strings.ToLower(line), strings.ToLower(pattern)) { - // Extract PID based on OS - pid := m.extractPID(line) - if pid != "" { - processes = append(processes, pid) - } - } +// 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 "" +} - return processes, nil +// matchPattern checks if a line matches a pattern, supporting wildcards +func (m *Manager) matchPattern(line, pattern string) bool { + switch { + case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"): + search := pattern[1 : len(pattern)-1] + return strings.Contains(line, search) + case strings.HasPrefix(pattern, "*"): + return strings.HasSuffix(line, pattern[1:]) + case strings.HasSuffix(pattern, "*"): + return strings.HasPrefix(line, pattern[:len(pattern)-1]) + default: + return line == pattern + } } +// extractPID extracts process ID from a process list line based on OS format func (m *Manager) extractPID(line string) string { switch runtime.GOOS { case "windows": - // Windows CSV format: "ImageName","PID",... parts := strings.Split(line, ",") if len(parts) >= 2 { return strings.Trim(parts[1], "\"") } case "darwin", "linux": - // Unix format: PID TTY TIME CMD parts := strings.Fields(line) if len(parts) >= 1 { return parts[0] @@ -144,17 +194,23 @@ func (m *Manager) extractPID(line string) string { return "" } +// killProcess forcefully terminates a process by PID func (m *Manager) killProcess(pid string) error { - var cmd *exec.Cmd + cmd := m.getKillCommand(pid) + if cmd == nil { + return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + return cmd.Run() +} +// getKillCommand returns the appropriate command to kill a process based on OS +func (m *Manager) getKillCommand(pid string) *exec.Cmd { switch runtime.GOOS { case "windows": - cmd = exec.Command("taskkill", "/F", "/PID", pid) + return exec.Command("taskkill", "/F", "/PID", pid) case "darwin", "linux": - cmd = exec.Command("kill", "-9", pid) + return exec.Command("kill", "-9", pid) default: - return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + return nil } - - return cmd.Run() } diff --git a/internal/ui/display.go b/internal/ui/display.go index 0f94f69..32ed566 100644 --- a/internal/ui/display.go +++ b/internal/ui/display.go @@ -10,92 +10,85 @@ import ( "github.com/fatih/color" ) -// Display handles UI display operations +// Display handles UI operations for terminal output type Display struct { spinner *Spinner } -// NewDisplay creates a new display handler +// NewDisplay creates a new display instance with an optional spinner func NewDisplay(spinner *Spinner) *Display { if spinner == nil { spinner = NewSpinner(nil) } - return &Display{ - spinner: spinner, - } + return &Display{spinner: spinner} } -// ShowProgress shows a progress message with spinner -func (d *Display) ShowProgress(message string) { - d.spinner.SetMessage(message) - d.spinner.Start() -} +// Terminal Operations -// StopProgress stops the progress spinner -func (d *Display) StopProgress() { - d.spinner.Stop() -} - -// ClearScreen clears the terminal screen +// ClearScreen clears the terminal screen based on OS func (d *Display) ClearScreen() error { var cmd *exec.Cmd - if runtime.GOOS == "windows" { + switch runtime.GOOS { + case "windows": cmd = exec.Command("cmd", "/c", "cls") - } else { + default: 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) +// Progress Indicator - red.Println(errorMsg) - if runtime.GOOS == "windows" { - yellow.Println(adminMsg) - } else { - yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0])) - } +// ShowProgress displays a progress message with a spinner +func (d *Display) ShowProgress(message string) { + d.spinner.SetMessage(message) + d.spinner.Start() } -// 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) +// StopProgress stops the progress spinner +func (d *Display) StopProgress() { + d.spinner.Stop() } -// ShowError shows an error message -func (d *Display) ShowError(message string) { - red := color.New(color.FgRed, color.Bold) - red.Printf("\n%s\n", message) -} +// Message Display -// ShowWarning shows a warning message -func (d *Display) ShowWarning(message string) { - yellow := color.New(color.FgYellow, color.Bold) - yellow.Printf("\n%s\n", message) +// ShowSuccess displays success messages in green +func (d *Display) ShowSuccess(messages ...string) { + green := color.New(color.FgGreen) + for _, msg := range messages { + green.Println(msg) + } } -// ShowInfo shows an info message +// ShowInfo displays an info message in cyan func (d *Display) ShowInfo(message string) { cyan := color.New(color.FgCyan) - cyan.Printf("\n%s\n", message) + cyan.Println(message) +} + +// ShowError displays an error message in red +func (d *Display) ShowError(message string) { + red := color.New(color.FgRed) + red.Println(message) } -// ShowPrompt shows a prompt message and waits for user input -func (d *Display) ShowPrompt(message string) { - fmt.Print(message) - os.Stdout.Sync() +// ShowPrivilegeError displays privilege error messages with instructions +func (d *Display) ShowPrivilegeError(messages ...string) { + red := color.New(color.FgRed, color.Bold) + yellow := color.New(color.FgYellow) + + // Main error message + red.Println(messages[0]) + fmt.Println() + + // Additional instructions + for _, msg := range messages[1:] { + if strings.Contains(msg, "%s") { + exe, _ := os.Executable() + yellow.Printf(msg+"\n", exe) + } else { + yellow.Println(msg) + } + } } diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 439af52..564525a 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -5,15 +5,15 @@ import ( ) const cyberpunkLogo = ` - ______ ______ ______ - / ____/_ __________ ___ _____/ __/ // / / / - / / / / / / ___/ _ \/ __ \/ ___/ /_/ // /_/ / -/ /___/ /_/ / / / __/ /_/ (__ ) __/__ __/ / -\____/\__,_/_/ \___/\____/____/_/ /_/ /_/ - + ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ + ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ + ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ + ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ + ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ` -// ShowLogo displays the cyberpunk-style logo +// ShowLogo displays the application logo func (d *Display) ShowLogo() { cyan := color.New(color.FgCyan, color.Bold) cyan.Println(cyberpunkLogo) diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go index 4f85fcb..145f730 100644 --- a/internal/ui/spinner.go +++ b/internal/ui/spinner.go @@ -10,8 +10,8 @@ import ( // SpinnerConfig defines spinner configuration type SpinnerConfig struct { - Frames []string - Delay time.Duration + Frames []string // Animation frames for the spinner + Delay time.Duration // Delay between frame updates } // DefaultSpinnerConfig returns the default spinner configuration @@ -43,6 +43,8 @@ func NewSpinner(config *SpinnerConfig) *Spinner { } } +// State management + // SetMessage sets the spinner message func (s *Spinner) SetMessage(message string) { s.mu.Lock() @@ -50,7 +52,16 @@ func (s *Spinner) SetMessage(message string) { 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() { s.mu.Lock() if s.active { @@ -63,7 +74,7 @@ func (s *Spinner) Start() { go s.run() } -// Stop stops the spinner animation +// Stop halts the spinner animation func (s *Spinner) Stop() { s.mu.Lock() defer s.mu.Unlock() @@ -75,20 +86,21 @@ func (s *Spinner) Stop() { s.active = false close(s.stopCh) s.stopCh = make(chan struct{}) - fmt.Println() + fmt.Print("\r") // Clear the spinner line } -// IsActive returns whether the spinner is currently active -func (s *Spinner) IsActive() bool { - s.mu.RLock() - defer s.mu.RUnlock() - return s.active -} +// Internal methods func (s *Spinner) run() { ticker := time.NewTicker(s.config.Delay) defer ticker.Stop() + cyan := color.New(color.FgCyan, color.Bold) + message := s.message + + // Print initial state + fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message) + for { select { case <-s.stopCh: @@ -100,11 +112,11 @@ func (s *Spinner) run() { 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) + fmt.Printf("\r %s", cyan.Sprint(frame)) + fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message } } } diff --git a/pkg/idgen/generator.go b/pkg/idgen/generator.go index 5a0058f..4a69341 100644 --- a/pkg/idgen/generator.go +++ b/pkg/idgen/generator.go @@ -1,95 +1,59 @@ package idgen import ( - cryptorand "crypto/rand" - "crypto/sha256" + "crypto/rand" "encoding/hex" "fmt" - "math/big" - "sync" + "time" ) -// Generator handles the generation of various IDs -type Generator struct { - charsetMu sync.RWMutex - charset string -} +// Generator handles secure ID generation for machines and devices +type Generator struct{} -// NewGenerator creates a new ID generator with default settings +// NewGenerator creates a new ID generator func NewGenerator() *Generator { - return &Generator{ - charset: "0123456789ABCDEFGHJKLMNPQRSTVWXYZ", - } + return &Generator{} } -// 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 -} +// Helper methods +// ------------- -// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id] -func (g *Generator) GenerateMachineID() (string, error) { - prefix := "auth0|user_" +// simulateWork adds a small delay to make progress visible +func (g *Generator) simulateWork() { + time.Sleep(800 * time.Millisecond) +} - // 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) +// 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) } - sequence := fmt.Sprintf("%02d", seqNum.Int64()) + return hex.EncodeToString(bytes), nil +} - uniqueID, err := g.generateUniqueID(23) - if err != nil { - return "", fmt.Errorf("failed to generate unique ID: %w", err) - } +// Public methods +// ------------- - fullID := prefix + sequence + uniqueID - return hex.EncodeToString([]byte(fullID)), nil +// GenerateMachineID generates a new 32-byte machine ID +func (g *Generator) GenerateMachineID() (string, error) { + g.simulateWork() + return g.generateRandomHex(32) } -// GenerateMacMachineID generates a new MAC machine ID using SHA-256 +// GenerateMacMachineID generates a new 64-byte MAC machine ID 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 + g.simulateWork() + return g.generateRandomHex(64) } -// GenerateDeviceID generates a new device ID in UUID v4 format +// GenerateDeviceID generates a new device ID in UUID 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()] + g.simulateWork() + id, err := g.generateRandomHex(16) + if err != nil { + return "", err } - - return string(result), nil + return fmt.Sprintf("%s-%s-%s-%s-%s", + id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil } diff --git a/pkg/idgen/generator_test.go b/pkg/idgen/generator_test.go deleted file mode 100644 index 81dcea9..0000000 --- a/pkg/idgen/generator_test.go +++ /dev/null @@ -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") -} diff --git a/scripts/install.ps1 b/scripts/install.ps1 index fb14a5b..18e6cb9 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -9,13 +9,6 @@ if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdenti # Set TLS to 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -# Colors for output -$Red = "`e[31m" -$Green = "`e[32m" -$Blue = "`e[36m" -$Yellow = "`e[33m" -$Reset = "`e[0m" - # Create temporary directory $TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) New-Item -ItemType Directory -Path $TmpDir | Out-Null @@ -29,7 +22,7 @@ function Cleanup { # Error handler trap { - Write-Host "${Red}Error: $_${Reset}" + Write-Host "Error: $_" -ForegroundColor Red Cleanup exit 1 } @@ -44,7 +37,7 @@ function Get-SystemArch { } # Download with progress -function Download-WithProgress { +function Get-FileWithProgress { param ( [string]$Url, [string]$OutputFile @@ -58,18 +51,18 @@ function Download-WithProgress { return $true } catch { - Write-Host "${Red}Failed to download: $_${Reset}" + Write-Host "Failed to download: $_" -ForegroundColor Red return $false } } # Main installation function function Install-CursorModifier { - Write-Host "${Blue}Starting installation...${Reset}" + Write-Host "Starting installation..." -ForegroundColor Cyan # Detect architecture $arch = Get-SystemArch - Write-Host "${Green}Detected architecture: $arch${Reset}" + Write-Host "Detected architecture: $arch" -ForegroundColor Green # Set installation directory $InstallDir = "$env:ProgramFiles\CursorModifier" @@ -80,28 +73,36 @@ function Install-CursorModifier { # Get latest release try { $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/dacrab/go-cursor-help/releases/latest" + Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan + + # Updated binary name format to match actual assets $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) { + Write-Host "Available assets:" -ForegroundColor Yellow + $latestRelease.assets | ForEach-Object { Write-Host $_.name } throw "Could not find download URL for $binaryName" } } catch { - Write-Host "${Red}Failed to get latest release: $_${Reset}" + Write-Host "Failed to get latest release: $_" -ForegroundColor Red exit 1 } # 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" - if (!(Download-WithProgress -Url $downloadUrl -OutputFile $binaryPath)) { + if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) { exit 1 } # Install binary - Write-Host "${Blue}Installing...${Reset}" + Write-Host "Installing..." -ForegroundColor Cyan try { Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force @@ -112,24 +113,23 @@ function Install-CursorModifier { } } catch { - Write-Host "${Red}Failed to install: $_${Reset}" + Write-Host "Failed to install: $_" -ForegroundColor Red exit 1 } - Write-Host "${Green}Installation completed successfully!${Reset}" - Write-Host "${Blue}Running cursor-id-modifier...${Reset}" + Write-Host "Installation completed successfully!" -ForegroundColor Green + Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan # Run the program try { - $env:AUTOMATED_MODE = "1" & "$InstallDir\cursor-id-modifier.exe" 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 } } catch { - Write-Host "${Red}Failed to run cursor-id-modifier: $_${Reset}" + Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red exit 1 } } @@ -140,4 +140,6 @@ try { } finally { Cleanup + Write-Host "Press any key to continue..." + $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') } \ No newline at end of file