committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2211 additions and 1933 deletions
-
272.github/workflows/auto-tag-release.yml
-
37.github/workflows/release.yml
-
47.gitignore
-
86.goreleaser.yml
-
53CHANGELOG.md
-
21LICENSE
-
305README.md
-
330cmd/cursor-id-modifier/main.go
-
8go.mod
-
16go.sum
-
BINimg/wx_public.jpg
-
BINimg/wx_public_2.png
-
153internal/config/config.go
-
187internal/lang/lang.go
-
216internal/process/manager.go
-
94internal/ui/display.go
-
20internal/ui/logo.go
-
122internal/ui/spinner.go
-
1050main.go
-
116pkg/idgen/generator.go
-
146scripts/build_all.bat
-
108scripts/build_all.sh
-
421scripts/install.ps1
-
336scripts/install.sh
@ -0,0 +1,272 @@ |
|||||
|
# This workflow requires Ubuntu 22.04 or 24.04 |
||||
|
|
||||
|
name: Auto Tag & Release |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- master |
||||
|
- main |
||||
|
tags: |
||||
|
- "v*" |
||||
|
paths-ignore: |
||||
|
- "**.md" |
||||
|
- "LICENSE" |
||||
|
- ".gitignore" |
||||
|
workflow_call: {} |
||||
|
|
||||
|
permissions: |
||||
|
contents: write |
||||
|
packages: write |
||||
|
actions: write |
||||
|
|
||||
|
jobs: |
||||
|
pre_job: |
||||
|
runs-on: ubuntu-22.04 |
||||
|
outputs: |
||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip }} |
||||
|
steps: |
||||
|
- id: skip_check |
||||
|
uses: fkirc/skip-duplicate-actions@v5.3.0 |
||||
|
with: |
||||
|
cancel_others: "true" |
||||
|
concurrent_skipping: "same_content" |
||||
|
|
||||
|
auto-tag-release: |
||||
|
needs: pre_job |
||||
|
if: | |
||||
|
needs.pre_job.outputs.should_skip != 'true' || |
||||
|
startsWith(github.ref, 'refs/tags/v') |
||||
|
runs-on: ubuntu-22.04 |
||||
|
timeout-minutes: 15 |
||||
|
outputs: |
||||
|
version: ${{ steps.get_latest_tag.outputs.version }} |
||||
|
concurrency: |
||||
|
group: ${{ github.workflow }}-${{ github.ref }} |
||||
|
cancel-in-progress: true |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout |
||||
|
uses: actions/checkout@v3 |
||||
|
with: |
||||
|
fetch-depth: 0 |
||||
|
lfs: true |
||||
|
submodules: recursive |
||||
|
|
||||
|
- name: Setup Go |
||||
|
uses: actions/setup-go@v3 |
||||
|
with: |
||||
|
go-version: "1.21" |
||||
|
check-latest: true |
||||
|
cache: true |
||||
|
|
||||
|
- name: Cache |
||||
|
uses: actions/cache@v3 |
||||
|
with: |
||||
|
path: | |
||||
|
~/.cache/go-build |
||||
|
~/go/pkg/mod |
||||
|
~/.cache/git |
||||
|
key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }} |
||||
|
restore-keys: | |
||||
|
${{ runner.os }}-build- |
||||
|
${{ runner.os }}- |
||||
|
|
||||
|
# 只在非tag推送时执行自动打tag |
||||
|
- name: Get latest tag |
||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }} |
||||
|
id: get_latest_tag |
||||
|
run: | |
||||
|
set -euo pipefail |
||||
|
git fetch --tags --force || { |
||||
|
echo "::error::Failed to fetch tags" |
||||
|
exit 1 |
||||
|
} |
||||
|
latest_tag=$(git tag -l 'v*' --sort=-v:refname | head -n 1) |
||||
|
if [ -z "$latest_tag" ]; then |
||||
|
new_version="v0.1.0" |
||||
|
else |
||||
|
major=$(echo $latest_tag | cut -d. -f1) |
||||
|
minor=$(echo $latest_tag | cut -d. -f2) |
||||
|
patch=$(echo $latest_tag | cut -d. -f3) |
||||
|
new_patch=$((patch + 1)) |
||||
|
new_version="$major.$minor.$new_patch" |
||||
|
fi |
||||
|
echo "version=$new_version" >> "$GITHUB_OUTPUT" |
||||
|
echo "Generated version: $new_version" |
||||
|
|
||||
|
- name: Validate version |
||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }} |
||||
|
run: | |
||||
|
set -euo pipefail |
||||
|
new_tag="${{ steps.get_latest_tag.outputs.version }}" |
||||
|
echo "Validating version: $new_tag" |
||||
|
if [[ ! $new_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
||||
|
echo "::error::Invalid version format: $new_tag" |
||||
|
exit 1 |
||||
|
fi |
||||
|
major=$(echo $new_tag | cut -d. -f1 | tr -d 'v') |
||||
|
minor=$(echo $new_tag | cut -d. -f2) |
||||
|
patch=$(echo $new_tag | cut -d. -f3) |
||||
|
if [[ $major -gt 99 || $minor -gt 99 || $patch -gt 999 ]]; then |
||||
|
echo "::error::Version numbers out of valid range" |
||||
|
exit 1 |
||||
|
fi |
||||
|
echo "Version validation passed" |
||||
|
|
||||
|
- name: Create new tag |
||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/v') }} |
||||
|
env: |
||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
|
run: | |
||||
|
new_tag=${{ steps.get_latest_tag.outputs.version }} |
||||
|
git config --global user.name 'github-actions[bot]' |
||||
|
git config --global user.email 'github-actions[bot]@users.noreply.github.com' |
||||
|
git tag -a $new_tag -m "Release $new_tag" |
||||
|
git push origin $new_tag |
||||
|
|
||||
|
# 在 Run GoReleaser 之前添加配置检查步骤 |
||||
|
- name: Check GoReleaser config |
||||
|
run: | |
||||
|
if [ ! -f ".goreleaser.yml" ] && [ ! -f ".goreleaser.yaml" ]; then |
||||
|
echo "::error::GoReleaser configuration file not found" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# 添加依赖检查步骤 |
||||
|
- name: Check Dependencies |
||||
|
run: | |
||||
|
go mod verify |
||||
|
go mod download |
||||
|
# 如果使用 vendor 模式,则执行以下命令 |
||||
|
if [ -d "vendor" ]; then |
||||
|
go mod vendor |
||||
|
fi |
||||
|
|
||||
|
# 添加构建环境准备步骤 |
||||
|
- name: Prepare Build Environment |
||||
|
run: | |
||||
|
echo "Building version: ${VERSION:-development}" |
||||
|
echo "GOOS=${GOOS:-$(go env GOOS)}" >> $GITHUB_ENV |
||||
|
echo "GOARCH=${GOARCH:-$(go env GOARCH)}" >> $GITHUB_ENV |
||||
|
echo "GO111MODULE=on" >> $GITHUB_ENV |
||||
|
|
||||
|
# 添加清理步骤 |
||||
|
- name: Cleanup workspace |
||||
|
run: | |
||||
|
rm -rf /tmp/go/ |
||||
|
rm -rf .cache/ |
||||
|
rm -rf dist/ |
||||
|
git clean -fdx |
||||
|
git status |
||||
|
|
||||
|
# 修改 GoReleaser 步骤 |
||||
|
- name: Run GoReleaser |
||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} |
||||
|
uses: goreleaser/goreleaser-action@v3 |
||||
|
with: |
||||
|
distribution: goreleaser |
||||
|
version: latest |
||||
|
args: release --clean --timeout 60m |
||||
|
env: |
||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
||||
|
VERSION: ${{ steps.get_latest_tag.outputs.version }} |
||||
|
CGO_ENABLED: 0 |
||||
|
GOPATH: /tmp/go |
||||
|
GOROOT: ${{ env.GOROOT }} |
||||
|
GOCACHE: /tmp/.cache/go-build |
||||
|
GOMODCACHE: /tmp/go/pkg/mod |
||||
|
GORELEASER_DEBUG: 1 |
||||
|
GORELEASER_CURRENT_TAG: ${{ steps.get_latest_tag.outputs.version }} |
||||
|
# 添加额外的构建信息 |
||||
|
BUILD_TIME: ${{ steps.get_latest_tag.outputs.version }} |
||||
|
BUILD_COMMIT: ${{ github.sha }} |
||||
|
|
||||
|
# 优化 vendor 同步步骤 |
||||
|
- name: Sync vendor directory |
||||
|
run: | |
||||
|
echo "Syncing vendor directory..." |
||||
|
go mod tidy |
||||
|
go mod vendor |
||||
|
go mod verify |
||||
|
# 验证 vendor 目录 |
||||
|
if [ -d "vendor" ]; then |
||||
|
echo "Verifying vendor directory..." |
||||
|
go mod verify |
||||
|
# 检查是否有未跟踪的文件 |
||||
|
if [ -n "$(git status --porcelain vendor/)" ]; then |
||||
|
echo "Warning: Vendor directory has uncommitted changes" |
||||
|
git status vendor/ |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# 添加错误检查步骤 |
||||
|
- name: Check GoReleaser Output |
||||
|
if: failure() |
||||
|
run: | |
||||
|
echo "::group::GoReleaser Debug Info" |
||||
|
cat dist/artifacts.json || true |
||||
|
echo "::endgroup::" |
||||
|
|
||||
|
echo "::group::GoReleaser Config" |
||||
|
cat .goreleaser.yml |
||||
|
echo "::endgroup::" |
||||
|
|
||||
|
echo "::group::Environment Info" |
||||
|
go version |
||||
|
go env |
||||
|
echo "::endgroup::" |
||||
|
|
||||
|
- name: Set Release Version |
||||
|
if: startsWith(github.ref, 'refs/tags/v') |
||||
|
run: | |
||||
|
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV |
||||
|
|
||||
|
# 改进验证步骤 |
||||
|
- name: Verify Release |
||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || (success() && steps.get_latest_tag.outputs.version != '') }} |
||||
|
run: | |
||||
|
echo "Verifying release artifacts..." |
||||
|
if [ ! -d "dist" ]; then |
||||
|
echo "::error::Release artifacts not found" |
||||
|
exit 1 |
||||
|
fi |
||||
|
# 验证生成的二进制文件 |
||||
|
for file in dist/cursor-id-modifier_*; do |
||||
|
if [ -f "$file" ]; then |
||||
|
echo "Verifying: $file" |
||||
|
if [[ "$file" == *.exe ]]; then |
||||
|
# Windows 二进制文件检查 |
||||
|
if ! [ -x "$file" ]; then |
||||
|
echo "::error::$file is not executable" |
||||
|
exit 1 |
||||
|
fi |
||||
|
else |
||||
|
# Unix 二进制文件检查 |
||||
|
if ! [ -x "$file" ]; then |
||||
|
echo "::error::$file is not executable" |
||||
|
exit 1 |
||||
|
fi |
||||
|
fi |
||||
|
fi |
||||
|
done |
||||
|
|
||||
|
- name: Notify on failure |
||||
|
if: failure() |
||||
|
run: | |
||||
|
echo "::error::Release process failed" |
||||
|
|
||||
|
# 修改构建摘要步骤 |
||||
|
- name: Build Summary |
||||
|
if: always() |
||||
|
run: | |
||||
|
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY |
||||
|
echo "- Go Version: $(go version)" >> $GITHUB_STEP_SUMMARY |
||||
|
echo "- Release Version: ${VERSION:-N/A}" >> $GITHUB_STEP_SUMMARY |
||||
|
echo "- GPG Signing: Disabled" >> $GITHUB_STEP_SUMMARY |
||||
|
echo "- Build Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY |
||||
|
|
||||
|
if [ -d "dist" ]; then |
||||
|
echo "### Generated Artifacts" >> $GITHUB_STEP_SUMMARY |
||||
|
ls -lh dist/ | awk '{print "- "$9" ("$5")"}' >> $GITHUB_STEP_SUMMARY |
||||
|
fi |
@ -1,37 +0,0 @@ |
|||||
name: Release |
|
||||
|
|
||||
on: |
|
||||
push: |
|
||||
tags: |
|
||||
- 'v*' |
|
||||
|
|
||||
permissions: |
|
||||
contents: write |
|
||||
|
|
||||
jobs: |
|
||||
goreleaser: |
|
||||
runs-on: ubuntu-latest |
|
||||
steps: |
|
||||
- name: Checkout |
|
||||
uses: actions/checkout@v4 |
|
||||
with: |
|
||||
fetch-depth: 0 |
|
||||
|
|
||||
- name: Set up Go |
|
||||
uses: actions/setup-go@v5 |
|
||||
with: |
|
||||
check-latest: true |
|
||||
|
|
||||
- name: Set Repository Variables |
|
||||
run: | |
|
||||
echo "GITHUB_REPOSITORY_OWNER=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV |
|
||||
echo "GITHUB_REPOSITORY_NAME=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV |
|
||||
|
|
||||
- name: Run GoReleaser |
|
||||
uses: goreleaser/goreleaser-action@v5 |
|
||||
with: |
|
||||
distribution: goreleaser |
|
||||
version: latest |
|
||||
args: release --clean |
|
||||
env: |
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
|
@ -1,23 +1,42 @@ |
|||||
# Binary files |
|
||||
*.dll |
|
||||
*.so |
|
||||
*.dylib |
|
||||
|
|
||||
# Build directories |
|
||||
releases/ |
|
||||
cursor-id-modifier |
|
||||
|
# Compiled binary |
||||
|
/cursor-id-modifier |
||||
|
/cursor-id-modifier.exe |
||||
|
|
||||
|
# Build output directories |
||||
|
bin/ |
||||
|
dist/ |
||||
|
|
||||
# Go specific |
# Go specific |
||||
go.sum |
go.sum |
||||
|
go/ |
||||
|
.cache/ |
||||
|
|
||||
# Temporary files |
|
||||
*.tmp |
|
||||
*~ |
|
||||
|
# IDE and editor files |
||||
|
.vscode/ |
||||
|
.idea/ |
||||
|
*.swp |
||||
|
*.swo |
||||
|
|
||||
# System files |
|
||||
|
# OS specific |
||||
.DS_Store |
.DS_Store |
||||
Thumbs.db |
Thumbs.db |
||||
|
|
||||
.vscode |
|
||||
/.idea |
|
||||
|
# Build and release artifacts |
||||
|
releases/ |
||||
|
*.syso |
||||
|
*.exe |
||||
|
*.exe~ |
||||
|
*.dll |
||||
|
*.so |
||||
|
*.dylib |
||||
|
|
||||
|
# Test files |
||||
|
*.test |
||||
|
*.out |
||||
|
coverage.txt |
||||
|
|
||||
|
# Temporary files |
||||
|
*.tmp |
||||
|
*~ |
||||
|
*.bak |
||||
|
*.log |
@ -0,0 +1,53 @@ |
|||||
|
# 📝 Changelog |
||||
|
|
||||
|
All notable changes to this project will be documented in this file. |
||||
|
|
||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), |
||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
||||
|
|
||||
|
## [0.1.23] - 2024-12-29 🚀 |
||||
|
|
||||
|
### ✨ Features |
||||
|
- **Initial Release**: First public release of Cursor ID Modifier |
||||
|
- **Multi-Platform Support**: |
||||
|
- 🪟 Windows (x64, x86) |
||||
|
- 🍎 macOS (Intel & Apple Silicon) |
||||
|
- 🐧 Linux (x64, x86, ARM64) |
||||
|
- **Installation**: |
||||
|
- Automated installation scripts for all platforms |
||||
|
- One-line installation commands |
||||
|
- Secure download and verification |
||||
|
- **Core Functionality**: |
||||
|
- Telemetry ID modification for Cursor IDE |
||||
|
- Automatic process management |
||||
|
- Secure configuration handling |
||||
|
|
||||
|
### 🐛 Bug Fixes |
||||
|
- **Installation**: |
||||
|
- Fixed environment variable preservation in sudo operations |
||||
|
- Enhanced error handling during installation |
||||
|
- Improved binary download reliability |
||||
|
- **Process Management**: |
||||
|
- Improved Cursor process detection accuracy |
||||
|
- Enhanced process termination reliability |
||||
|
- Better handling of edge cases |
||||
|
- **User Experience**: |
||||
|
- Enhanced error messages and user feedback |
||||
|
- Improved progress indicators |
||||
|
- Better handling of system permissions |
||||
|
|
||||
|
### 🔧 Technical Improvements |
||||
|
- Optimized binary size with proper build flags |
||||
|
- Enhanced cross-platform compatibility |
||||
|
- Improved error handling and logging |
||||
|
- Better system resource management |
||||
|
|
||||
|
### 📚 Documentation |
||||
|
- Added comprehensive installation instructions |
||||
|
- Included platform-specific guidelines |
||||
|
- Enhanced error troubleshooting guide |
||||
|
|
||||
|
--- |
||||
|
*For more details about the changes, please refer to the [commit history](https://github.com/yuaotian/go-cursor-help/commits/main).* |
||||
|
|
||||
|
[0.1.22]: https://github.com/yuaotian/go-cursor-help/releases/tag/v0.1.23 |
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 2024 dacrab |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
@ -0,0 +1,330 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"flag" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"os/user" |
||||
|
"runtime" |
||||
|
"runtime/debug" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/yuaotian/go-cursor-help/internal/config" |
||||
|
"github.com/yuaotian/go-cursor-help/internal/lang" |
||||
|
"github.com/yuaotian/go-cursor-help/internal/process" |
||||
|
"github.com/yuaotian/go-cursor-help/internal/ui" |
||||
|
"github.com/yuaotian/go-cursor-help/pkg/idgen" |
||||
|
|
||||
|
"github.com/sirupsen/logrus" |
||||
|
) |
||||
|
|
||||
|
// Global variables
|
||||
|
var ( |
||||
|
version = "dev" |
||||
|
setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode") |
||||
|
showVersion = flag.Bool("v", false, "show version information") |
||||
|
log = logrus.New() |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
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) |
||||
|
debug.PrintStack() |
||||
|
waitExit() |
||||
|
} |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
func handleFlags() { |
||||
|
flag.Parse() |
||||
|
if *showVersion { |
||||
|
fmt.Printf("Cursor ID Modifier v%s\n", version) |
||||
|
os.Exit(0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func setupLogger() { |
||||
|
log.SetFormatter(&logrus.TextFormatter{ |
||||
|
FullTimestamp: true, |
||||
|
DisableLevelTruncation: true, |
||||
|
PadLevelText: true, |
||||
|
}) |
||||
|
log.SetLevel(logrus.InfoLevel) |
||||
|
} |
||||
|
|
||||
|
func getCurrentUser() string { |
||||
|
if username := os.Getenv("SUDO_USER"); username != "" { |
||||
|
return username |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
} |
||||
|
return configManager |
||||
|
} |
||||
|
|
||||
|
func handlePrivileges(display *ui.Display) error { |
||||
|
isAdmin, err := checkAdminPrivileges() |
||||
|
if err != nil { |
||||
|
log.Error(err) |
||||
|
waitExit() |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if !isAdmin { |
||||
|
if runtime.GOOS == "windows" { |
||||
|
return handleWindowsPrivileges(display) |
||||
|
} |
||||
|
display.ShowPrivilegeError( |
||||
|
lang.GetText().PrivilegeError, |
||||
|
lang.GetText().RunWithSudo, |
||||
|
lang.GetText().SudoExample, |
||||
|
) |
||||
|
waitExit() |
||||
|
return fmt.Errorf("insufficient privileges") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func handleWindowsPrivileges(display *ui.Display) error { |
||||
|
message := "\nRequesting administrator privileges..." |
||||
|
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 err |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func setupDisplay(display *ui.Display) { |
||||
|
if err := display.ClearScreen(); err != nil { |
||||
|
log.Warn("Failed to clear screen:", err) |
||||
|
} |
||||
|
display.ShowLogo() |
||||
|
fmt.Println() |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig { |
||||
|
display.ShowProgress(text.GeneratingIds) |
||||
|
newConfig := &config.StorageConfig{} |
||||
|
|
||||
|
if machineID, err := generator.GenerateMachineID(); err != nil { |
||||
|
log.Fatal("Failed to generate machine ID:", err) |
||||
|
} else { |
||||
|
newConfig.TelemetryMachineId = machineID |
||||
|
} |
||||
|
|
||||
|
if macMachineID, err := generator.GenerateMacMachineID(); err != nil { |
||||
|
log.Fatal("Failed to generate MAC machine ID:", err) |
||||
|
} else { |
||||
|
newConfig.TelemetryMacMachineId = macMachineID |
||||
|
} |
||||
|
|
||||
|
if deviceID, err := generator.GenerateDeviceID(); err != nil { |
||||
|
log.Fatal("Failed to generate device ID:", err) |
||||
|
} else { |
||||
|
newConfig.TelemetryDevDeviceId = deviceID |
||||
|
} |
||||
|
|
||||
|
if oldConfig != nil && oldConfig.TelemetrySqmId != "" { |
||||
|
newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId |
||||
|
} else if sqmID, err := generator.GenerateSQMID(); err != nil { |
||||
|
log.Fatal("Failed to generate SQM ID:", err) |
||||
|
} else { |
||||
|
newConfig.TelemetrySqmId = sqmID |
||||
|
} |
||||
|
|
||||
|
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 err |
||||
|
} |
||||
|
display.StopProgress() |
||||
|
fmt.Println() |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func showCompletionMessages(display *ui.Display) { |
||||
|
display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage) |
||||
|
fmt.Println() |
||||
|
|
||||
|
message := "Operation completed!" |
||||
|
if lang.GetCurrentLanguage() == lang.CN { |
||||
|
message = "操作完成!" |
||||
|
} |
||||
|
display.ShowInfo(message) |
||||
|
} |
||||
|
|
||||
|
func waitExit() { |
||||
|
fmt.Print(lang.GetText().PressEnterToExit) |
||||
|
os.Stdout.Sync() |
||||
|
bufio.NewReader(os.Stdin).ReadString('\n') |
||||
|
} |
||||
|
|
||||
|
func checkAdminPrivileges() (bool, error) { |
||||
|
switch runtime.GOOS { |
||||
|
case "windows": |
||||
|
cmd := exec.Command("net", "session") |
||||
|
return cmd.Run() == nil, nil |
||||
|
|
||||
|
case "darwin", "linux": |
||||
|
currentUser, err := user.Current() |
||||
|
if err != nil { |
||||
|
return false, fmt.Errorf("failed to get current user: %w", err) |
||||
|
} |
||||
|
return currentUser.Uid == "0", nil |
||||
|
|
||||
|
default: |
||||
|
return false, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func selfElevate() error { |
||||
|
os.Setenv("AUTOMATED_MODE", "1") |
||||
|
|
||||
|
switch runtime.GOOS { |
||||
|
case "windows": |
||||
|
verb := "runas" |
||||
|
exe, _ := os.Executable() |
||||
|
cwd, _ := os.Getwd() |
||||
|
args := strings.Join(os.Args[1:], " ") |
||||
|
|
||||
|
cmd := exec.Command("cmd", "/C", "start", verb, exe, args) |
||||
|
cmd.Dir = cwd |
||||
|
return cmd.Run() |
||||
|
|
||||
|
case "darwin", "linux": |
||||
|
exe, err := os.Executable() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
cmd := exec.Command("sudo", append([]string{exe}, os.Args[1:]...)...) |
||||
|
cmd.Stdin = os.Stdin |
||||
|
cmd.Stdout = os.Stdout |
||||
|
cmd.Stderr = os.Stderr |
||||
|
return cmd.Run() |
||||
|
|
||||
|
default: |
||||
|
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) |
||||
|
} |
||||
|
} |
@ -1,11 +1,15 @@ |
|||||
module cursor-id-modifier |
|
||||
|
module github.com/yuaotian/go-cursor-help |
||||
|
|
||||
go 1.21 |
go 1.21 |
||||
|
|
||||
require github.com/fatih/color v1.15.0 |
|
||||
|
require ( |
||||
|
github.com/fatih/color v1.15.0 |
||||
|
github.com/sirupsen/logrus v1.9.3 |
||||
|
) |
||||
|
|
||||
require ( |
require ( |
||||
github.com/mattn/go-colorable v0.1.13 // indirect |
github.com/mattn/go-colorable v0.1.13 // indirect |
||||
github.com/mattn/go-isatty v0.0.17 // indirect |
github.com/mattn/go-isatty v0.0.17 // indirect |
||||
|
github.com/stretchr/testify v1.10.0 // indirect |
||||
golang.org/x/sys v0.13.0 // indirect |
golang.org/x/sys v0.13.0 // indirect |
||||
) |
) |
After Width: 258 | Height: 258 | Size: 27 KiB |
After Width: 580 | Height: 206 | Size: 30 KiB |
@ -0,0 +1,153 @@ |
|||||
|
package config |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"runtime" |
||||
|
"sync" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// StorageConfig represents the storage configuration
|
||||
|
type StorageConfig struct { |
||||
|
TelemetryMacMachineId string `json:"telemetry.macMachineId"` |
||||
|
TelemetryMachineId string `json:"telemetry.machineId"` |
||||
|
TelemetryDevDeviceId string `json:"telemetry.devDeviceId"` |
||||
|
TelemetrySqmId string `json:"telemetry.sqmId"` |
||||
|
LastModified string `json:"lastModified"` |
||||
|
Version string `json:"version"` |
||||
|
} |
||||
|
|
||||
|
// Manager handles configuration operations
|
||||
|
type Manager struct { |
||||
|
configPath string |
||||
|
mu sync.RWMutex |
||||
|
} |
||||
|
|
||||
|
// NewManager creates a new configuration manager
|
||||
|
func NewManager(username string) (*Manager, error) { |
||||
|
configPath, err := getConfigPath(username) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to get config path: %w", err) |
||||
|
} |
||||
|
return &Manager{configPath: configPath}, nil |
||||
|
} |
||||
|
|
||||
|
// ReadConfig reads the existing configuration
|
||||
|
func (m *Manager) ReadConfig() (*StorageConfig, error) { |
||||
|
m.mu.RLock() |
||||
|
defer m.mu.RUnlock() |
||||
|
|
||||
|
data, err := os.ReadFile(m.configPath) |
||||
|
if err != nil { |
||||
|
if os.IsNotExist(err) { |
||||
|
return nil, nil |
||||
|
} |
||||
|
return nil, fmt.Errorf("failed to read config file: %w", err) |
||||
|
} |
||||
|
|
||||
|
var config StorageConfig |
||||
|
if err := json.Unmarshal(data, &config); err != nil { |
||||
|
return nil, fmt.Errorf("failed to parse config file: %w", err) |
||||
|
} |
||||
|
|
||||
|
return &config, nil |
||||
|
} |
||||
|
|
||||
|
// SaveConfig saves the configuration
|
||||
|
func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error { |
||||
|
m.mu.Lock() |
||||
|
defer m.mu.Unlock() |
||||
|
|
||||
|
// Ensure parent directories exist
|
||||
|
if err := os.MkdirAll(filepath.Dir(m.configPath), 0755); err != nil { |
||||
|
return fmt.Errorf("failed to create config directory: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Prepare updated configuration
|
||||
|
updatedConfig := m.prepareUpdatedConfig(config) |
||||
|
|
||||
|
// Write configuration
|
||||
|
if err := m.writeConfigFile(updatedConfig, readOnly); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
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
|
||||
|
originalFile["telemetry.sqmId"] = config.TelemetrySqmId |
||||
|
originalFile["telemetry.macMachineId"] = config.TelemetryMacMachineId |
||||
|
originalFile["telemetry.machineId"] = config.TelemetryMachineId |
||||
|
originalFile["telemetry.devDeviceId"] = config.TelemetryDevDeviceId |
||||
|
originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339) |
||||
|
// originalFile["version"] = "1.0.1"
|
||||
|
|
||||
|
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
|
||||
|
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, content, 0666); err != nil { |
||||
|
return fmt.Errorf("failed to write temporary file: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Set final permissions
|
||||
|
fileMode := os.FileMode(0666) |
||||
|
if readOnly { |
||||
|
fileMode = 0444 |
||||
|
} |
||||
|
|
||||
|
if err := os.Chmod(tmpPath, fileMode); err != nil { |
||||
|
os.Remove(tmpPath) |
||||
|
return fmt.Errorf("failed to set temporary file permissions: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Atomic rename
|
||||
|
if err := os.Rename(tmpPath, m.configPath); err != nil { |
||||
|
os.Remove(tmpPath) |
||||
|
return fmt.Errorf("failed to rename file: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Sync directory
|
||||
|
if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil { |
||||
|
defer dir.Close() |
||||
|
dir.Sync() |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// getConfigPath returns the path to the configuration file
|
||||
|
func getConfigPath(username string) (string, error) { |
||||
|
var configDir string |
||||
|
switch runtime.GOOS { |
||||
|
case "windows": |
||||
|
configDir = filepath.Join(os.Getenv("APPDATA"), "Cursor", "User", "globalStorage") |
||||
|
case "darwin": |
||||
|
configDir = filepath.Join("/Users", username, "Library", "Application Support", "Cursor", "User", "globalStorage") |
||||
|
case "linux": |
||||
|
configDir = filepath.Join("/home", username, ".config", "Cursor", "User", "globalStorage") |
||||
|
default: |
||||
|
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) |
||||
|
} |
||||
|
return filepath.Join(configDir, "storage.json"), nil |
||||
|
} |
@ -0,0 +1,187 @@ |
|||||
|
package lang |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
// Language represents a supported language code
|
||||
|
type Language string |
||||
|
|
||||
|
const ( |
||||
|
// CN represents Chinese language
|
||||
|
CN Language = "cn" |
||||
|
// EN represents English language
|
||||
|
EN Language = "en" |
||||
|
) |
||||
|
|
||||
|
// TextResource contains all translatable text resources
|
||||
|
type TextResource struct { |
||||
|
// 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 |
||||
|
PressEnterToExit string |
||||
|
SetReadOnlyMessage string |
||||
|
|
||||
|
// Info messages
|
||||
|
ConfigLocation string |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
currentLanguage Language |
||||
|
currentLanguageOnce sync.Once |
||||
|
languageMutex sync.RWMutex |
||||
|
) |
||||
|
|
||||
|
// GetCurrentLanguage returns the current language, detecting it if not already set
|
||||
|
func GetCurrentLanguage() Language { |
||||
|
currentLanguageOnce.Do(func() { |
||||
|
currentLanguage = detectLanguage() |
||||
|
}) |
||||
|
|
||||
|
languageMutex.RLock() |
||||
|
defer languageMutex.RUnlock() |
||||
|
return currentLanguage |
||||
|
} |
||||
|
|
||||
|
// SetLanguage sets the current language
|
||||
|
func SetLanguage(lang Language) { |
||||
|
languageMutex.Lock() |
||||
|
defer languageMutex.Unlock() |
||||
|
currentLanguage = lang |
||||
|
} |
||||
|
|
||||
|
// GetText returns the TextResource for the current language
|
||||
|
func GetText() TextResource { |
||||
|
return texts[GetCurrentLanguage()] |
||||
|
} |
||||
|
|
||||
|
// detectLanguage detects the system language
|
||||
|
func detectLanguage() Language { |
||||
|
// Check environment variables first
|
||||
|
if isChineseEnvVar() { |
||||
|
return CN |
||||
|
} |
||||
|
|
||||
|
// Then check OS-specific locale
|
||||
|
if isWindows() { |
||||
|
if isWindowsChineseLocale() { |
||||
|
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" |
||||
|
} |
||||
|
|
||||
|
func isWindowsChineseLocale() bool { |
||||
|
// Check Windows UI culture
|
||||
|
cmd := exec.Command("powershell", "-Command", |
||||
|
"[System.Globalization.CultureInfo]::CurrentUICulture.Name") |
||||
|
output, err := cmd.Output() |
||||
|
if err == nil && strings.HasPrefix(strings.ToLower(strings.TrimSpace(string(output))), "zh") { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Check Windows locale
|
||||
|
cmd = exec.Command("wmic", "os", "get", "locale") |
||||
|
output, err = cmd.Output() |
||||
|
return err == nil && strings.Contains(string(output), "2052") |
||||
|
} |
||||
|
|
||||
|
func isUnixChineseLocale() bool { |
||||
|
cmd := exec.Command("locale") |
||||
|
output, err := cmd.Output() |
||||
|
return err == nil && strings.Contains(strings.ToLower(string(output)), "zh_cn") |
||||
|
} |
||||
|
|
||||
|
// texts contains all translations
|
||||
|
var texts = map[Language]TextResource{ |
||||
|
CN: { |
||||
|
// 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", |
||||
|
PressEnterToExit: "\n按回车键退出程序...", |
||||
|
SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题", |
||||
|
|
||||
|
// Info messages
|
||||
|
ConfigLocation: "配置文件位置:", |
||||
|
}, |
||||
|
EN: { |
||||
|
// 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", |
||||
|
PressEnterToExit: "\nPress 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:", |
||||
|
}, |
||||
|
} |
@ -0,0 +1,216 @@ |
|||||
|
package process |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os/exec" |
||||
|
"runtime" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/sirupsen/logrus" |
||||
|
) |
||||
|
|
||||
|
// Config holds process manager configuration
|
||||
|
type Config struct { |
||||
|
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: 2 * time.Second, |
||||
|
ProcessPatterns: []string{ |
||||
|
"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
|
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Manager handles process-related operations
|
||||
|
type Manager struct { |
||||
|
config *Config |
||||
|
log *logrus.Logger |
||||
|
} |
||||
|
|
||||
|
// NewManager creates a new process manager with optional config and logger
|
||||
|
func NewManager(config *Config, log *logrus.Logger) *Manager { |
||||
|
if config == nil { |
||||
|
config = DefaultConfig() |
||||
|
} |
||||
|
if log == nil { |
||||
|
log = logrus.New() |
||||
|
} |
||||
|
return &Manager{ |
||||
|
config: config, |
||||
|
log: log, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// IsCursorRunning checks if any Cursor process is currently running
|
||||
|
func (m *Manager) IsCursorRunning() bool { |
||||
|
processes, err := m.getCursorProcesses() |
||||
|
if err != nil { |
||||
|
m.log.Warn("Failed to get Cursor processes:", err) |
||||
|
return false |
||||
|
} |
||||
|
return len(processes) > 0 |
||||
|
} |
||||
|
|
||||
|
// 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() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to get processes: %w", err) |
||||
|
} |
||||
|
|
||||
|
if len(processes) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// 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 processes, _ := m.getCursorProcesses(); len(processes) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// getCursorProcesses returns PIDs of running Cursor processes
|
||||
|
func (m *Manager) getCursorProcesses() ([]string, error) { |
||||
|
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": |
||||
|
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": |
||||
|
parts := strings.Split(line, ",") |
||||
|
if len(parts) >= 2 { |
||||
|
return strings.Trim(parts[1], "\"") |
||||
|
} |
||||
|
case "darwin", "linux": |
||||
|
parts := strings.Fields(line) |
||||
|
if len(parts) >= 1 { |
||||
|
return parts[0] |
||||
|
} |
||||
|
} |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
// killProcess forcefully terminates a process by PID
|
||||
|
func (m *Manager) killProcess(pid string) error { |
||||
|
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 |
||||
|
} |
||||
|
} |
@ -0,0 +1,94 @@ |
|||||
|
package ui |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"runtime" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/fatih/color" |
||||
|
) |
||||
|
|
||||
|
// Display handles UI operations for terminal output
|
||||
|
type Display struct { |
||||
|
spinner *Spinner |
||||
|
} |
||||
|
|
||||
|
// 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} |
||||
|
} |
||||
|
|
||||
|
// 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() |
||||
|
} |
||||
|
|
||||
|
// StopProgress stops the progress spinner
|
||||
|
func (d *Display) StopProgress() { |
||||
|
d.spinner.Stop() |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ShowInfo displays an info message in cyan
|
||||
|
func (d *Display) ShowInfo(message string) { |
||||
|
cyan := color.New(color.FgCyan) |
||||
|
cyan.Println(message) |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
|
||||
|
// 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) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
package ui |
||||
|
|
||||
|
import ( |
||||
|
"github.com/fatih/color" |
||||
|
) |
||||
|
|
||||
|
const cyberpunkLogo = ` |
||||
|
██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ |
||||
|
██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ |
||||
|
██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ |
||||
|
██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ |
||||
|
╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ |
||||
|
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ |
||||
|
` |
||||
|
|
||||
|
// ShowLogo displays the application logo
|
||||
|
func (d *Display) ShowLogo() { |
||||
|
cyan := color.New(color.FgCyan, color.Bold) |
||||
|
cyan.Println(cyberpunkLogo) |
||||
|
} |
@ -0,0 +1,122 @@ |
|||||
|
package ui |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/fatih/color" |
||||
|
) |
||||
|
|
||||
|
// SpinnerConfig defines spinner configuration
|
||||
|
type SpinnerConfig struct { |
||||
|
Frames []string // Animation frames for the spinner
|
||||
|
Delay time.Duration // Delay between frame updates
|
||||
|
} |
||||
|
|
||||
|
// DefaultSpinnerConfig returns the default spinner configuration
|
||||
|
func DefaultSpinnerConfig() *SpinnerConfig { |
||||
|
return &SpinnerConfig{ |
||||
|
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, |
||||
|
Delay: 100 * time.Millisecond, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Spinner represents a progress spinner
|
||||
|
type Spinner struct { |
||||
|
config *SpinnerConfig |
||||
|
message string |
||||
|
current int |
||||
|
active bool |
||||
|
stopCh chan struct{} |
||||
|
mu sync.RWMutex |
||||
|
} |
||||
|
|
||||
|
// NewSpinner creates a new spinner with the given configuration
|
||||
|
func NewSpinner(config *SpinnerConfig) *Spinner { |
||||
|
if config == nil { |
||||
|
config = DefaultSpinnerConfig() |
||||
|
} |
||||
|
return &Spinner{ |
||||
|
config: config, |
||||
|
stopCh: make(chan struct{}), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// State management
|
||||
|
|
||||
|
// SetMessage sets the spinner message
|
||||
|
func (s *Spinner) SetMessage(message string) { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
s.message = message |
||||
|
} |
||||
|
|
||||
|
// 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 { |
||||
|
s.mu.Unlock() |
||||
|
return |
||||
|
} |
||||
|
s.active = true |
||||
|
s.mu.Unlock() |
||||
|
|
||||
|
go s.run() |
||||
|
} |
||||
|
|
||||
|
// Stop halts the spinner animation
|
||||
|
func (s *Spinner) Stop() { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
|
||||
|
if !s.active { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
s.active = false |
||||
|
close(s.stopCh) |
||||
|
s.stopCh = make(chan struct{}) |
||||
|
fmt.Print("\r") // Clear the spinner line
|
||||
|
} |
||||
|
|
||||
|
// 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: |
||||
|
return |
||||
|
case <-ticker.C: |
||||
|
s.mu.RLock() |
||||
|
if !s.active { |
||||
|
s.mu.RUnlock() |
||||
|
return |
||||
|
} |
||||
|
frame := s.config.Frames[s.current%len(s.config.Frames)] |
||||
|
s.current++ |
||||
|
s.mu.RUnlock() |
||||
|
|
||||
|
fmt.Printf("\r %s", cyan.Sprint(frame)) |
||||
|
fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message
|
||||
|
} |
||||
|
} |
||||
|
} |
1050
main.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,116 @@ |
|||||
|
package idgen |
||||
|
|
||||
|
import ( |
||||
|
"crypto/rand" |
||||
|
"encoding/hex" |
||||
|
"fmt" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
// Generator handles secure ID generation for machines and devices
|
||||
|
type Generator struct { |
||||
|
bufferPool sync.Pool |
||||
|
} |
||||
|
|
||||
|
// NewGenerator creates a new ID generator
|
||||
|
func NewGenerator() *Generator { |
||||
|
return &Generator{ |
||||
|
bufferPool: sync.Pool{ |
||||
|
New: func() interface{} { |
||||
|
return make([]byte, 64) |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Constants for ID generation
|
||||
|
const ( |
||||
|
machineIDPrefix = "auth0|user_" |
||||
|
uuidFormat = "%s-%s-%s-%s-%s" |
||||
|
) |
||||
|
|
||||
|
// generateRandomHex generates a random hex string of specified length
|
||||
|
func (g *Generator) generateRandomHex(length int) (string, error) { |
||||
|
buffer := g.bufferPool.Get().([]byte) |
||||
|
defer g.bufferPool.Put(buffer) |
||||
|
|
||||
|
if _, err := rand.Read(buffer[:length]); err != nil { |
||||
|
return "", fmt.Errorf("failed to generate random bytes: %w", err) |
||||
|
} |
||||
|
return hex.EncodeToString(buffer[:length]), nil |
||||
|
} |
||||
|
|
||||
|
// GenerateMachineID generates a new machine ID with auth0|user_ prefix
|
||||
|
func (g *Generator) GenerateMachineID() (string, error) { |
||||
|
randomPart, err := g.generateRandomHex(32) // 生成64字符的十六进制
|
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
|
||||
|
return fmt.Sprintf("%x%s", []byte(machineIDPrefix), randomPart), nil |
||||
|
} |
||||
|
|
||||
|
// GenerateMacMachineID generates a new 64-byte MAC machine ID
|
||||
|
func (g *Generator) GenerateMacMachineID() (string, error) { |
||||
|
return g.generateRandomHex(32) // 生成64字符的十六进制
|
||||
|
} |
||||
|
|
||||
|
// GenerateDeviceID generates a new device ID in UUID format
|
||||
|
func (g *Generator) GenerateDeviceID() (string, error) { |
||||
|
id, err := g.generateRandomHex(16) |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
return fmt.Sprintf(uuidFormat, |
||||
|
id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil |
||||
|
} |
||||
|
|
||||
|
// GenerateSQMID generates a new SQM ID in UUID format (with braces)
|
||||
|
func (g *Generator) GenerateSQMID() (string, error) { |
||||
|
id, err := g.GenerateDeviceID() |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
return fmt.Sprintf("{%s}", id), nil |
||||
|
} |
||||
|
|
||||
|
// ValidateID validates the format of various ID types
|
||||
|
func (g *Generator) ValidateID(id string, idType string) bool { |
||||
|
switch idType { |
||||
|
case "machineID", "macMachineID": |
||||
|
return len(id) == 64 && isHexString(id) |
||||
|
case "deviceID": |
||||
|
return isValidUUID(id) |
||||
|
case "sqmID": |
||||
|
if len(id) < 2 || id[0] != '{' || id[len(id)-1] != '}' { |
||||
|
return false |
||||
|
} |
||||
|
return isValidUUID(id[1 : len(id)-1]) |
||||
|
default: |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Helper functions
|
||||
|
func isHexString(s string) bool { |
||||
|
_, err := hex.DecodeString(s) |
||||
|
return err == nil |
||||
|
} |
||||
|
|
||||
|
func isValidUUID(uuid string) bool { |
||||
|
if len(uuid) != 36 { |
||||
|
return false |
||||
|
} |
||||
|
for i, r := range uuid { |
||||
|
if i == 8 || i == 13 || i == 18 || i == 23 { |
||||
|
if r != '-' { |
||||
|
return false |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
@ -1,128 +1,74 @@ |
|||||
@echo off |
@echo off |
||||
setlocal EnableDelayedExpansion |
setlocal EnableDelayedExpansion |
||||
|
|
||||
|
:: Build optimization flags |
||||
|
set "OPTIMIZATION_FLAGS=-trimpath -ldflags=\"-s -w\"" |
||||
|
set "BUILD_JOBS=4" |
||||
|
|
||||
:: Messages / 消息 |
:: Messages / 消息 |
||||
set "EN_MESSAGES[0]=Starting build process for version" |
set "EN_MESSAGES[0]=Starting build process for version" |
||||
set "EN_MESSAGES[1]=Using optimization flags:" |
set "EN_MESSAGES[1]=Using optimization flags:" |
||||
set "EN_MESSAGES[2]=Cleaning old builds..." |
set "EN_MESSAGES[2]=Cleaning old builds..." |
||||
set "EN_MESSAGES[3]=Cleanup completed" |
set "EN_MESSAGES[3]=Cleanup completed" |
||||
set "EN_MESSAGES[4]=bin directory does not exist, no cleanup needed" |
|
||||
set "EN_MESSAGES[5]=Starting builds for all platforms..." |
|
||||
set "EN_MESSAGES[6]=Building for" |
|
||||
set "EN_MESSAGES[7]=Build successful:" |
|
||||
set "EN_MESSAGES[8]=Build failed for" |
|
||||
set "EN_MESSAGES[9]=All builds completed! Total time:" |
|
||||
set "EN_MESSAGES[10]=seconds" |
|
||||
|
|
||||
set "CN_MESSAGES[0]=开始构建版本" |
|
||||
set "CN_MESSAGES[1]=使用优化标志:" |
|
||||
set "CN_MESSAGES[2]=正在清理旧的构建文件..." |
|
||||
set "CN_MESSAGES[3]=清理完成" |
|
||||
set "CN_MESSAGES[4]=bin 目录不存在,无需清理" |
|
||||
set "CN_MESSAGES[5]=开始编译所有平台..." |
|
||||
set "CN_MESSAGES[6]=正在构建" |
|
||||
set "CN_MESSAGES[7]=构建成功:" |
|
||||
set "CN_MESSAGES[8]=构建失败:" |
|
||||
set "CN_MESSAGES[9]=所有构建完成!总耗时:" |
|
||||
set "CN_MESSAGES[10]=秒" |
|
||||
|
|
||||
:: 设置版本信息 / Set version |
|
||||
set VERSION=2.0.0 |
|
||||
|
set "EN_MESSAGES[4]=Starting builds for all platforms..." |
||||
|
set "EN_MESSAGES[5]=Building for" |
||||
|
set "EN_MESSAGES[6]=Build successful:" |
||||
|
set "EN_MESSAGES[7]=All builds completed!" |
||||
|
|
||||
:: 设置颜色代码 / Set color codes |
|
||||
|
:: Colors |
||||
set "GREEN=[32m" |
set "GREEN=[32m" |
||||
set "RED=[31m" |
set "RED=[31m" |
||||
set "YELLOW=[33m" |
|
||||
set "RESET=[0m" |
set "RESET=[0m" |
||||
|
|
||||
:: 设置编译优化标志 / Set build optimization flags |
|
||||
set "LDFLAGS=-s -w" |
|
||||
set "BUILDMODE=pie" |
|
||||
set "GCFLAGS=-N -l" |
|
||||
|
|
||||
:: 设置 CGO / Set CGO |
|
||||
set CGO_ENABLED=0 |
|
||||
|
|
||||
:: 检测系统语言 / Detect system language |
|
||||
for /f "tokens=2 delims==" %%a in ('wmic os get OSLanguage /value') do set OSLanguage=%%a |
|
||||
if "%OSLanguage%"=="2052" (set LANG=cn) else (set LANG=en) |
|
||||
|
|
||||
:: 显示编译信息 / Display build info |
|
||||
echo %YELLOW%!%LANG%_MESSAGES[0]! %VERSION%%RESET% |
|
||||
echo %YELLOW%!%LANG%_MESSAGES[1]! LDFLAGS=%LDFLAGS%, BUILDMODE=%BUILDMODE%%RESET% |
|
||||
echo %YELLOW%CGO_ENABLED=%CGO_ENABLED%%RESET% |
|
||||
|
|
||||
:: 清理旧的构建文件 / Clean old builds |
|
||||
echo %YELLOW%!%LANG%_MESSAGES[2]!%RESET% |
|
||||
|
:: Cleanup function |
||||
|
:cleanup |
||||
if exist "..\bin" ( |
if exist "..\bin" ( |
||||
rd /s /q "..\bin" |
rd /s /q "..\bin" |
||||
echo %GREEN%!%LANG%_MESSAGES[3]!%RESET% |
|
||||
) else ( |
|
||||
echo %YELLOW%!%LANG%_MESSAGES[4]!%RESET% |
|
||||
|
echo %GREEN%!EN_MESSAGES[3]!%RESET% |
||||
) |
) |
||||
|
|
||||
:: 创建输出目录 / Create output directory |
|
||||
mkdir "..\bin" 2>nul |
mkdir "..\bin" 2>nul |
||||
|
|
||||
:: 定义目标平台数组 / Define target platforms array |
|
||||
set platforms[0].os=windows |
|
||||
set platforms[0].arch=amd64 |
|
||||
set platforms[0].ext=.exe |
|
||||
set platforms[0].suffix= |
|
||||
|
:: Build function with optimizations |
||||
|
:build |
||||
|
set "os=%~1" |
||||
|
set "arch=%~2" |
||||
|
set "ext=" |
||||
|
if "%os%"=="windows" set "ext=.exe" |
||||
|
|
||||
set platforms[1].os=darwin |
|
||||
set platforms[1].arch=amd64 |
|
||||
set platforms[1].ext= |
|
||||
set platforms[1].suffix=_intel |
|
||||
|
echo %GREEN%!EN_MESSAGES[5]! %os%/%arch%%RESET% |
||||
|
|
||||
set platforms[2].os=darwin |
|
||||
set platforms[2].arch=arm64 |
|
||||
set platforms[2].ext= |
|
||||
set platforms[2].suffix=_m1 |
|
||||
|
set "CGO_ENABLED=0" |
||||
|
set "GOOS=%os%" |
||||
|
set "GOARCH=%arch%" |
||||
|
|
||||
set platforms[3].os=linux |
|
||||
set platforms[3].arch=amd64 |
|
||||
set platforms[3].ext= |
|
||||
set platforms[3].suffix= |
|
||||
|
start /b cmd /c "go build -trimpath -ldflags=\"-s -w\" -o ..\bin\%os%\%arch%\cursor-id-modifier%ext% -a -installsuffix cgo -mod=readonly ..\cmd\cursor-id-modifier" |
||||
|
exit /b 0 |
||||
|
|
||||
:: 设置开始时间 / Set start time |
|
||||
set start_time=%time% |
|
||||
|
:: Main execution |
||||
|
echo %GREEN%!EN_MESSAGES[0]!%RESET% |
||||
|
echo %GREEN%!EN_MESSAGES[1]! %OPTIMIZATION_FLAGS%%RESET% |
||||
|
|
||||
:: 编译所有目标 / Build all targets |
|
||||
echo !%LANG%_MESSAGES[5]! |
|
||||
|
call :cleanup |
||||
|
|
||||
for /L %%i in (0,1,3) do ( |
|
||||
set "os=!platforms[%%i].os!" |
|
||||
set "arch=!platforms[%%i].arch!" |
|
||||
set "ext=!platforms[%%i].ext!" |
|
||||
set "suffix=!platforms[%%i].suffix!" |
|
||||
|
|
||||
echo. |
|
||||
echo !%LANG%_MESSAGES[6]! !os! !arch!... |
|
||||
|
|
||||
set GOOS=!os! |
|
||||
set GOARCH=!arch! |
|
||||
|
|
||||
:: 构建输出文件名 / Build output filename |
|
||||
set "outfile=..\bin\cursor_id_modifier_v%VERSION%_!os!_!arch!!suffix!!ext!" |
|
||||
|
|
||||
:: 执行构建 / Execute build |
|
||||
go build -trimpath -buildmode=%BUILDMODE% -ldflags="%LDFLAGS%" -gcflags="%GCFLAGS%" -o "!outfile!" ..\main.go |
|
||||
|
|
||||
if !errorlevel! equ 0 ( |
|
||||
echo %GREEN%!%LANG%_MESSAGES[7]! !outfile!%RESET% |
|
||||
) else ( |
|
||||
echo %RED%!%LANG%_MESSAGES[8]! !os! !arch!%RESET% |
|
||||
|
echo %GREEN%!EN_MESSAGES[4]!%RESET% |
||||
|
|
||||
|
:: Start builds in parallel |
||||
|
set "pending=0" |
||||
|
for %%o in (windows linux darwin) do ( |
||||
|
for %%a in (amd64 386) do ( |
||||
|
call :build %%o %%a |
||||
|
set /a "pending+=1" |
||||
|
if !pending! geq %BUILD_JOBS% ( |
||||
|
timeout /t 1 /nobreak >nul |
||||
|
set "pending=0" |
||||
|
) |
||||
) |
) |
||||
) |
) |
||||
|
|
||||
:: 计算总耗时 / Calculate total time |
|
||||
set end_time=%time% |
|
||||
set /a duration = %end_time:~0,2% * 3600 + %end_time:~3,2% * 60 + %end_time:~6,2% - (%start_time:~0,2% * 3600 + %start_time:~3,2% * 60 + %start_time:~6,2%) |
|
||||
|
|
||||
echo. |
|
||||
echo %GREEN%!%LANG%_MESSAGES[9]! %duration% !%LANG%_MESSAGES[10]!%RESET% |
|
||||
if exist "..\bin" dir /b "..\bin" |
|
||||
|
:: Wait for all builds to complete |
||||
|
:wait_builds |
||||
|
timeout /t 2 /nobreak >nul |
||||
|
tasklist /fi "IMAGENAME eq go.exe" 2>nul | find "go.exe" >nul |
||||
|
if not errorlevel 1 goto wait_builds |
||||
|
|
||||
pause |
|
||||
endlocal |
|
||||
|
echo %GREEN%!EN_MESSAGES[7]!%RESET% |
@ -1,308 +1,193 @@ |
|||||
# Auto-elevate to admin rights if not already running as admin |
|
||||
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { |
|
||||
Write-Host "Requesting administrator privileges..." |
|
||||
$arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -ExecutionFromElevated" |
|
||||
Start-Process powershell.exe -ArgumentList $arguments -Verb RunAs |
|
||||
Exit |
|
||||
} |
|
||||
|
|
||||
# Set TLS to 1.2 / 设置 TLS 为 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" |
|
||||
|
|
||||
# Messages / 消息 |
|
||||
$EN_MESSAGES = @( |
|
||||
"Starting installation...", |
|
||||
"Detected architecture:", |
|
||||
"Only 64-bit Windows is supported", |
|
||||
"Latest version:", |
|
||||
"Creating installation directory...", |
|
||||
"Downloading latest release from:", |
|
||||
"Failed to download binary:", |
|
||||
"Downloaded file not found", |
|
||||
"Installing binary...", |
|
||||
"Failed to install binary:", |
|
||||
"Adding to PATH...", |
|
||||
"Cleaning up...", |
|
||||
"Installation completed successfully!", |
|
||||
"You can now use 'cursor-id-modifier' directly", |
|
||||
"Checking for running Cursor instances...", |
|
||||
"Found running Cursor processes. Attempting to close them...", |
|
||||
"Successfully closed all Cursor instances", |
|
||||
"Failed to close Cursor instances. Please close them manually", |
|
||||
"Backing up storage.json...", |
|
||||
"Backup created at:" |
|
||||
) |
|
||||
|
|
||||
$CN_MESSAGES = @( |
|
||||
"开始安装...", |
|
||||
"检测到架构:", |
|
||||
"仅支持64位Windows系统", |
|
||||
"最新版本:", |
|
||||
"正在创建安装目录...", |
|
||||
"正在从以下地址下载最新版本:", |
|
||||
"下载二进制文件失败:", |
|
||||
"未找到下载的文件", |
|
||||
"正在安装程序...", |
|
||||
"安装二进制文件失败:", |
|
||||
"正在添加到PATH...", |
|
||||
"正在清理...", |
|
||||
"安装成功完成!", |
|
||||
"现在可以直接使用 'cursor-id-modifier' 了", |
|
||||
"正在检查运行中的Cursor进程...", |
|
||||
"发现正在运行的Cursor进程,尝试关闭...", |
|
||||
"成功关闭所有Cursor实例", |
|
||||
"无法关闭Cursor实例,请手动关闭", |
|
||||
"正在备份storage.json...", |
|
||||
"备份已创建于:" |
|
||||
) |
|
||||
|
|
||||
# Detect system language / 检测系统语言 |
|
||||
function Get-SystemLanguage { |
|
||||
if ((Get-Culture).Name -like "zh-CN") { |
|
||||
return "cn" |
|
||||
|
# Check for admin rights and handle elevation |
||||
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") |
||||
|
if (-NOT $isAdmin) { |
||||
|
# Detect PowerShell version and path |
||||
|
$pwshPath = if (Get-Command "pwsh" -ErrorAction SilentlyContinue) { |
||||
|
(Get-Command "pwsh").Source # PowerShell 7+ |
||||
|
} elseif (Test-Path "$env:ProgramFiles\PowerShell\7\pwsh.exe") { |
||||
|
"$env:ProgramFiles\PowerShell\7\pwsh.exe" |
||||
|
} else { |
||||
|
"powershell.exe" # Windows PowerShell |
||||
} |
} |
||||
return "en" |
|
||||
} |
|
||||
|
|
||||
# Get message based on language / 根据语言获取消息 |
|
||||
function Get-Message($Index) { |
|
||||
$lang = Get-SystemLanguage |
|
||||
if ($lang -eq "cn") { |
|
||||
return $CN_MESSAGES[$Index] |
|
||||
|
|
||||
|
try { |
||||
|
Write-Host "`nRequesting administrator privileges..." -ForegroundColor Cyan |
||||
|
$scriptPath = $MyInvocation.MyCommand.Path |
||||
|
$argList = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" |
||||
|
Start-Process -FilePath $pwshPath -Verb RunAs -ArgumentList $argList -Wait |
||||
|
exit |
||||
|
} |
||||
|
catch { |
||||
|
Write-Host "`nError: Administrator privileges required" -ForegroundColor Red |
||||
|
Write-Host "Please run this script from an Administrator PowerShell window" -ForegroundColor Yellow |
||||
|
Write-Host "`nTo do this:" -ForegroundColor Cyan |
||||
|
Write-Host "1. Press Win + X" -ForegroundColor White |
||||
|
Write-Host "2. Click 'Windows Terminal (Admin)' or 'PowerShell (Admin)'" -ForegroundColor White |
||||
|
Write-Host "3. Run the installation command again" -ForegroundColor White |
||||
|
Write-Host "`nPress enter to exit..." |
||||
|
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') |
||||
|
exit 1 |
||||
} |
} |
||||
return $EN_MESSAGES[$Index] |
|
||||
} |
|
||||
|
|
||||
# Functions for colored output / 彩色输出函数 |
|
||||
function Write-Status($Message) { |
|
||||
Write-Host "${Blue}[*]${Reset} $Message" |
|
||||
} |
|
||||
|
|
||||
function Write-Success($Message) { |
|
||||
Write-Host "${Green}[✓]${Reset} $Message" |
|
||||
} |
} |
||||
|
|
||||
function Write-Warning($Message) { |
|
||||
Write-Host "${Yellow}[!]${Reset} $Message" |
|
||||
} |
|
||||
|
# Set TLS to 1.2 |
||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 |
||||
|
|
||||
function Write-Error($Message) { |
|
||||
Write-Host "${Red}[✗]${Reset} $Message" |
|
||||
Exit 1 |
|
||||
} |
|
||||
|
# Create temporary directory |
||||
|
$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) |
||||
|
New-Item -ItemType Directory -Path $TmpDir | Out-Null |
||||
|
|
||||
# Close Cursor instances / 关闭Cursor实例 |
|
||||
function Close-CursorInstances { |
|
||||
Write-Status (Get-Message 14) |
|
||||
$cursorProcesses = Get-Process "Cursor" -ErrorAction SilentlyContinue |
|
||||
|
|
||||
if ($cursorProcesses) { |
|
||||
Write-Status (Get-Message 15) |
|
||||
try { |
|
||||
$cursorProcesses | ForEach-Object { $_.CloseMainWindow() | Out-Null } |
|
||||
Start-Sleep -Seconds 2 |
|
||||
$cursorProcesses | Where-Object { !$_.HasExited } | Stop-Process -Force |
|
||||
Write-Success (Get-Message 16) |
|
||||
} catch { |
|
||||
Write-Error (Get-Message 17) |
|
||||
} |
|
||||
|
# Cleanup function |
||||
|
function Cleanup { |
||||
|
if (Test-Path $TmpDir) { |
||||
|
Remove-Item -Recurse -Force $TmpDir |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
# Backup storage.json / 备份storage.json |
|
||||
function Backup-StorageJson { |
|
||||
Write-Status (Get-Message 18) |
|
||||
$storageJsonPath = "$env:APPDATA\Cursor\User\globalStorage\storage.json" |
|
||||
if (Test-Path $storageJsonPath) { |
|
||||
$backupPath = "$storageJsonPath.backup" |
|
||||
Copy-Item -Path $storageJsonPath -Destination $backupPath -Force |
|
||||
Write-Success "$(Get-Message 19) $backupPath" |
|
||||
} |
|
||||
|
# Error handler |
||||
|
trap { |
||||
|
Write-Host "Error: $_" -ForegroundColor Red |
||||
|
Cleanup |
||||
|
Write-Host "Press enter to exit..." |
||||
|
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') |
||||
|
exit 1 |
||||
} |
} |
||||
|
|
||||
# Get latest release version from GitHub / 从GitHub获取最新版本 |
|
||||
function Get-LatestVersion { |
|
||||
$repo = "yuaotian/go-cursor-help" |
|
||||
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases/latest" |
|
||||
return $release.tag_name |
|
||||
|
# Detect system architecture |
||||
|
function Get-SystemArch { |
||||
|
if ([Environment]::Is64BitOperatingSystem) { |
||||
|
return "x86_64" |
||||
|
} else { |
||||
|
return "i386" |
||||
|
} |
||||
} |
} |
||||
|
|
||||
# 在文件开头添加日志函数 |
|
||||
function Write-Log { |
|
||||
param( |
|
||||
[string]$Message, |
|
||||
[string]$Level = "INFO" |
|
||||
|
# Download with progress |
||||
|
function Get-FileWithProgress { |
||||
|
param ( |
||||
|
[string]$Url, |
||||
|
[string]$OutputFile |
||||
) |
) |
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" |
|
||||
$logMessage = "[$timestamp] [$Level] $Message" |
|
||||
$logFile = "$env:TEMP\cursor-id-modifier-install.log" |
|
||||
Add-Content -Path $logFile -Value $logMessage |
|
||||
|
|
||||
# 同时输出到控制台 |
|
||||
switch ($Level) { |
|
||||
"ERROR" { Write-Error $Message } |
|
||||
"WARNING" { Write-Warning $Message } |
|
||||
"SUCCESS" { Write-Success $Message } |
|
||||
default { Write-Status $Message } |
|
||||
|
try { |
||||
|
$webClient = New-Object System.Net.WebClient |
||||
|
$webClient.Headers.Add("User-Agent", "PowerShell Script") |
||||
|
|
||||
|
$webClient.DownloadFile($Url, $OutputFile) |
||||
|
return $true |
||||
|
} |
||||
|
catch { |
||||
|
Write-Host "Failed to download: $_" -ForegroundColor Red |
||||
|
return $false |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
# 添加安装前检查函数 |
|
||||
function Test-Prerequisites { |
|
||||
Write-Log "Checking prerequisites..." "INFO" |
|
||||
|
# Main installation function |
||||
|
function Install-CursorModifier { |
||||
|
Write-Host "Starting installation..." -ForegroundColor Cyan |
||||
|
|
||||
# 检查PowerShell版本 |
|
||||
if ($PSVersionTable.PSVersion.Major -lt 5) { |
|
||||
Write-Log "PowerShell 5.0 or higher is required" "ERROR" |
|
||||
return $false |
|
||||
|
# Detect architecture |
||||
|
$arch = Get-SystemArch |
||||
|
Write-Host "Detected architecture: $arch" -ForegroundColor Green |
||||
|
|
||||
|
# Set installation directory |
||||
|
$InstallDir = "$env:ProgramFiles\CursorModifier" |
||||
|
if (!(Test-Path $InstallDir)) { |
||||
|
New-Item -ItemType Directory -Path $InstallDir | Out-Null |
||||
} |
} |
||||
|
|
||||
# 检查网络连接 |
|
||||
|
# Get latest release |
||||
try { |
try { |
||||
$testConnection = Test-Connection -ComputerName "github.com" -Count 1 -Quiet |
|
||||
if (-not $testConnection) { |
|
||||
Write-Log "No internet connection available" "ERROR" |
|
||||
return $false |
|
||||
|
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/yuaotian/go-cursor-help/releases/latest" |
||||
|
Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan |
||||
|
|
||||
|
# Look for Windows binary with our architecture |
||||
|
$version = $latestRelease.tag_name.TrimStart('v') |
||||
|
Write-Host "Version: $version" -ForegroundColor Cyan |
||||
|
$possibleNames = @( |
||||
|
"cursor-id-modifier_${version}_windows_x86_64.exe", |
||||
|
"cursor-id-modifier_${version}_windows_$($arch).exe" |
||||
|
) |
||||
|
|
||||
|
$asset = $null |
||||
|
foreach ($name in $possibleNames) { |
||||
|
Write-Host "Checking for asset: $name" -ForegroundColor Cyan |
||||
|
$asset = $latestRelease.assets | Where-Object { $_.name -eq $name } |
||||
|
if ($asset) { |
||||
|
Write-Host "Found matching asset: $($asset.name)" -ForegroundColor Green |
||||
|
break |
||||
|
} |
||||
} |
} |
||||
} catch { |
|
||||
Write-Log "Failed to check internet connection: $_" "ERROR" |
|
||||
return $false |
|
||||
|
|
||||
|
if (!$asset) { |
||||
|
Write-Host "`nAvailable assets:" -ForegroundColor Yellow |
||||
|
$latestRelease.assets | ForEach-Object { Write-Host "- $($_.name)" } |
||||
|
throw "Could not find appropriate Windows binary for $arch architecture" |
||||
|
} |
||||
|
|
||||
|
$downloadUrl = $asset.browser_download_url |
||||
|
} |
||||
|
catch { |
||||
|
Write-Host "Failed to get latest release: $_" -ForegroundColor Red |
||||
|
exit 1 |
||||
} |
} |
||||
|
|
||||
return $true |
|
||||
} |
|
||||
|
|
||||
# 添加文件验证函数 |
|
||||
function Test-FileHash { |
|
||||
param( |
|
||||
[string]$FilePath, |
|
||||
[string]$ExpectedHash |
|
||||
) |
|
||||
|
# Download binary |
||||
|
Write-Host "`nDownloading latest release..." -ForegroundColor Cyan |
||||
|
$binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe" |
||||
|
|
||||
$actualHash = Get-FileHash -Path $FilePath -Algorithm SHA256 |
|
||||
return $actualHash.Hash -eq $ExpectedHash |
|
||||
} |
|
||||
|
|
||||
# 修改下载函数,添加进度条 |
|
||||
function Download-File { |
|
||||
param( |
|
||||
[string]$Url, |
|
||||
[string]$OutFile |
|
||||
) |
|
||||
|
if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) { |
||||
|
exit 1 |
||||
|
} |
||||
|
|
||||
|
# Install binary |
||||
|
Write-Host "Installing..." -ForegroundColor Cyan |
||||
try { |
try { |
||||
$webClient = New-Object System.Net.WebClient |
|
||||
$webClient.Headers.Add("User-Agent", "PowerShell Script") |
|
||||
|
|
||||
$webClient.DownloadFileAsync($Url, $OutFile) |
|
||||
|
Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force |
||||
|
|
||||
while ($webClient.IsBusy) { |
|
||||
Write-Progress -Activity "Downloading..." -Status "Progress:" -PercentComplete -1 |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
|
# Add to PATH if not already present |
||||
|
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine") |
||||
|
if ($currentPath -notlike "*$InstallDir*") { |
||||
|
[Environment]::SetEnvironmentVariable("Path", "$currentPath;$InstallDir", "Machine") |
||||
} |
} |
||||
|
|
||||
Write-Progress -Activity "Downloading..." -Completed |
|
||||
return $true |
|
||||
} |
} |
||||
catch { |
catch { |
||||
Write-Log "Download failed: $_" "ERROR" |
|
||||
return $false |
|
||||
|
Write-Host "Failed to install: $_" -ForegroundColor Red |
||||
|
exit 1 |
||||
} |
} |
||||
finally { |
|
||||
if ($webClient) { |
|
||||
$webClient.Dispose() |
|
||||
|
|
||||
|
Write-Host "Installation completed successfully!" -ForegroundColor Green |
||||
|
Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan |
||||
|
|
||||
|
# Run the program |
||||
|
try { |
||||
|
& "$InstallDir\cursor-id-modifier.exe" |
||||
|
if ($LASTEXITCODE -ne 0) { |
||||
|
Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red |
||||
|
exit 1 |
||||
} |
} |
||||
} |
} |
||||
} |
|
||||
|
|
||||
# Main installation process / 主安装过程 |
|
||||
Write-Status (Get-Message 0) |
|
||||
|
|
||||
# Close any running Cursor instances |
|
||||
Close-CursorInstances |
|
||||
|
|
||||
# Backup storage.json |
|
||||
Backup-StorageJson |
|
||||
|
|
||||
# Get system architecture / 获取系统架构 |
|
||||
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" } |
|
||||
Write-Status "$(Get-Message 1) $arch" |
|
||||
|
|
||||
if ($arch -ne "amd64") { |
|
||||
Write-Error (Get-Message 2) |
|
||||
} |
|
||||
|
|
||||
# Get latest version / 获取最新版本 |
|
||||
$version = Get-LatestVersion |
|
||||
Write-Status "$(Get-Message 3) $version" |
|
||||
|
|
||||
# Set up paths / 设置路径 |
|
||||
$installDir = "$env:ProgramFiles\cursor-id-modifier" |
|
||||
$versionWithoutV = $version.TrimStart('v') # 移除版本号前面的 'v' 字符 |
|
||||
$binaryName = "cursor_id_modifier_${versionWithoutV}_windows_amd64.exe" |
|
||||
$downloadUrl = "https://github.com/yuaotian/go-cursor-help/releases/download/$version/$binaryName" |
|
||||
$tempFile = "$env:TEMP\$binaryName" |
|
||||
|
|
||||
# Create installation directory / 创建安装目录 |
|
||||
Write-Status (Get-Message 4) |
|
||||
if (-not (Test-Path $installDir)) { |
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null |
|
||||
} |
|
||||
|
|
||||
# Download binary / 下载二进制文件 |
|
||||
Write-Status "$(Get-Message 5) $downloadUrl" |
|
||||
try { |
|
||||
if (-not (Download-File -Url $downloadUrl -OutFile $tempFile)) { |
|
||||
Write-Error "$(Get-Message 6)" |
|
||||
|
catch { |
||||
|
Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red |
||||
|
exit 1 |
||||
} |
} |
||||
} catch { |
|
||||
Write-Error "$(Get-Message 6) $_" |
|
||||
} |
|
||||
|
|
||||
# Verify download / 验证下载 |
|
||||
if (-not (Test-Path $tempFile)) { |
|
||||
Write-Error (Get-Message 7) |
|
||||
} |
} |
||||
|
|
||||
# Install binary / 安装二进制文件 |
|
||||
Write-Status (Get-Message 8) |
|
||||
|
# Run installation |
||||
try { |
try { |
||||
Move-Item -Force $tempFile "$installDir\cursor-id-modifier.exe" |
|
||||
} catch { |
|
||||
Write-Error "$(Get-Message 9) $_" |
|
||||
} |
|
||||
|
|
||||
# Add to PATH if not already present / 如果尚未添加则添加到PATH |
|
||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User") |
|
||||
if ($userPath -notlike "*$installDir*") { |
|
||||
Write-Status (Get-Message 10) |
|
||||
[Environment]::SetEnvironmentVariable( |
|
||||
"Path", |
|
||||
"$userPath;$installDir", |
|
||||
"User" |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
# Cleanup / 清理 |
|
||||
Write-Status (Get-Message 11) |
|
||||
if (Test-Path $tempFile) { |
|
||||
Remove-Item -Force $tempFile |
|
||||
} |
|
||||
|
|
||||
Write-Success (Get-Message 12) |
|
||||
Write-Success (Get-Message 13) |
|
||||
Write-Host "" |
|
||||
|
|
||||
# 直接运行程序 |
|
||||
try { |
|
||||
Start-Process "$installDir\cursor-id-modifier.exe" -NoNewWindow |
|
||||
} catch { |
|
||||
Write-Warning "Failed to start cursor-id-modifier: $_" |
|
||||
|
Install-CursorModifier |
||||
|
} |
||||
|
catch { |
||||
|
Write-Host "Installation failed: $_" -ForegroundColor Red |
||||
|
Cleanup |
||||
|
Write-Host "Press enter to exit..." |
||||
|
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') |
||||
|
exit 1 |
||||
|
} |
||||
|
finally { |
||||
|
Cleanup |
||||
|
if ($LASTEXITCODE -ne 0) { |
||||
|
Write-Host "Press enter to exit..." -ForegroundColor Green |
||||
|
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') |
||||
|
} |
||||
} |
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue