mirror of
https://github.com/yuaotian/go-cursor-help.git
synced 2025-08-01 13:27:36 +08:00
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:
parent
56f09677ca
commit
947d11fbc6
34
README.md
34
README.md
@ -58,21 +58,15 @@ this is a mistake.
|
||||
|
||||
### 🚀 One-Click Solution
|
||||
|
||||
<details>
|
||||
<summary><b>Linux/macOS</b>: Copy and paste in terminal</summary>
|
||||
|
||||
**Linux/macOS**: Copy and paste in terminal
|
||||
```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
|
||||
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
```
|
||||
</details>
|
||||
|
||||
That's it! The script will:
|
||||
1. ✨ Install the tool automatically
|
||||
@ -85,23 +79,23 @@ That's it! The script will:
|
||||
<details>
|
||||
<summary>Windows Packages</summary>
|
||||
|
||||
- 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`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>macOS Packages</summary>
|
||||
|
||||
- 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`
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Linux Packages</summary>
|
||||
|
||||
- 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`
|
||||
</details>
|
||||
|
||||
### 🔧 Technical Details
|
||||
@ -176,21 +170,15 @@ this is a mistake.
|
||||
|
||||
### 🚀 一键解决
|
||||
|
||||
<details>
|
||||
<summary><b>Linux/macOS</b>: 在终端中复制粘贴</summary>
|
||||
|
||||
**Linux/macOS**: 在终端中复制粘贴
|
||||
```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
|
||||
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
|
||||
```
|
||||
</details>
|
||||
|
||||
就这样!脚本会:
|
||||
1. ✨ 自动安装工具
|
||||
|
@ -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,68 +81,77 @@ 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().RunWithSudo,
|
||||
lang.GetText().SudoExample,
|
||||
)
|
||||
waitExit()
|
||||
return fmt.Errorf("insufficient privileges")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
@ -106,161 +159,128 @@ func main() {
|
||||
lang.GetText().SudoExample,
|
||||
)
|
||||
waitExit()
|
||||
return
|
||||
return err
|
||||
}
|
||||
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)
|
||||
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
|
||||
func setupDisplay(display *ui.Display) {
|
||||
if err := display.ClearScreen(); err != nil {
|
||||
log.Warn("Failed to clear screen:", err)
|
||||
}
|
||||
|
||||
// Show logo
|
||||
display.ShowLogo()
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
text := lang.GetText()
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Show success
|
||||
display.ShowSuccess(text.SuccessMessage, text.RestartMessage)
|
||||
message := "\nOperation completed!"
|
||||
func showCompletionMessages(display *ui.Display) {
|
||||
display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage)
|
||||
fmt.Println()
|
||||
|
||||
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":
|
||||
|
5
go.mod
5
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
|
||||
)
|
||||
|
1
go.sum
1
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=
|
||||
|
@ -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
|
||||
|
@ -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:",
|
||||
},
|
||||
}
|
||||
|
@ -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,34 +76,34 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(m.config.RetryDelay)
|
||||
}
|
||||
// Force kill remaining processes
|
||||
remainingProcesses, _ := m.getCursorProcesses()
|
||||
for _, pid := range remainingProcesses {
|
||||
m.killProcess(pid)
|
||||
}
|
||||
|
||||
if m.IsCursorRunning() {
|
||||
return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts)
|
||||
time.Sleep(m.config.RetryDelay)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
cmd := m.getProcessListCommand()
|
||||
if cmd == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processes, nil
|
||||
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":
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("taskkill", "/F", "/PID", pid)
|
||||
case "darwin", "linux":
|
||||
cmd = exec.Command("kill", "-9", pid)
|
||||
default:
|
||||
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":
|
||||
return exec.Command("taskkill", "/F", "/PID", pid)
|
||||
case "darwin", "linux":
|
||||
return exec.Command("kill", "-9", pid)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -10,22 +10,37 @@ 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
|
||||
// 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) {
|
||||
d.spinner.SetMessage(message)
|
||||
d.spinner.Start()
|
||||
@ -36,66 +51,44 @@ func (d *Display) StopProgress() {
|
||||
d.spinner.Stop()
|
||||
}
|
||||
|
||||
// ClearScreen clears the terminal screen
|
||||
func (d *Display) ClearScreen() error {
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/c", "cls")
|
||||
} else {
|
||||
cmd = exec.Command("clear")
|
||||
// Message Display
|
||||
|
||||
// ShowSuccess displays success messages in green
|
||||
func (d *Display) ShowSuccess(messages ...string) {
|
||||
green := color.New(color.FgGreen)
|
||||
for _, msg := range messages {
|
||||
green.Println(msg)
|
||||
}
|
||||
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))
|
||||
// ShowInfo displays an info message in cyan
|
||||
func (d *Display) ShowInfo(message string) {
|
||||
cyan := color.New(color.FgCyan)
|
||||
cyan.Println(message)
|
||||
}
|
||||
|
||||
// ShowPrivilegeError shows the privilege error message
|
||||
func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) {
|
||||
// ShowError displays an error message in red
|
||||
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)
|
||||
yellow := color.New(color.FgYellow)
|
||||
|
||||
red.Println(errorMsg)
|
||||
if runtime.GOOS == "windows" {
|
||||
yellow.Println(adminMsg)
|
||||
} else {
|
||||
yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0]))
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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{}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
// -------------
|
||||
|
||||
// simulateWork adds a small delay to make progress visible
|
||||
func (g *Generator) simulateWork() {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// Public methods
|
||||
// -------------
|
||||
|
||||
// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id]
|
||||
// GenerateMachineID generates a new 32-byte machine ID
|
||||
func (g *Generator) GenerateMachineID() (string, error) {
|
||||
prefix := "auth0|user_"
|
||||
|
||||
// Generate random sequence number between 0-99
|
||||
seqNum, err := cryptorand.Int(cryptorand.Reader, big.NewInt(100))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate sequence number: %w", err)
|
||||
}
|
||||
sequence := fmt.Sprintf("%02d", seqNum.Int64())
|
||||
|
||||
uniqueID, err := g.generateUniqueID(23)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate unique ID: %w", err)
|
||||
}
|
||||
|
||||
fullID := prefix + sequence + uniqueID
|
||||
return hex.EncodeToString([]byte(fullID)), nil
|
||||
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)
|
||||
g.simulateWork()
|
||||
id, err := g.generateRandomHex(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Set version (4) and variant (2) bits according to RFC 4122
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80
|
||||
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x",
|
||||
uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]), nil
|
||||
}
|
||||
|
||||
// generateUniqueID generates a random string of specified length using the configured charset
|
||||
func (g *Generator) generateUniqueID(length int) (string, error) {
|
||||
g.charsetMu.RLock()
|
||||
defer g.charsetMu.RUnlock()
|
||||
|
||||
result := make([]byte, length)
|
||||
max := big.NewInt(int64(len(g.charset)))
|
||||
|
||||
for i := range result {
|
||||
randNum, err := cryptorand.Int(cryptorand.Reader, max)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
result[i] = g.charset[randNum.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s",
|
||||
id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
@ -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')
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user