diff --git a/Packages/src/Cli~/Core~/dist/darwin-amd64/uloop-core b/Packages/src/Cli~/Core~/dist/darwin-amd64/uloop-core index f76d82b74..016e8ad95 100755 Binary files a/Packages/src/Cli~/Core~/dist/darwin-amd64/uloop-core and b/Packages/src/Cli~/Core~/dist/darwin-amd64/uloop-core differ diff --git a/Packages/src/Cli~/Core~/dist/darwin-arm64/uloop-core b/Packages/src/Cli~/Core~/dist/darwin-arm64/uloop-core index 970abd203..ac32afd6a 100755 Binary files a/Packages/src/Cli~/Core~/dist/darwin-arm64/uloop-core and b/Packages/src/Cli~/Core~/dist/darwin-arm64/uloop-core differ diff --git a/Packages/src/Cli~/Core~/dist/windows-amd64/uloop-core.exe b/Packages/src/Cli~/Core~/dist/windows-amd64/uloop-core.exe index 440ad3dae..a176c44d7 100755 Binary files a/Packages/src/Cli~/Core~/dist/windows-amd64/uloop-core.exe and b/Packages/src/Cli~/Core~/dist/windows-amd64/uloop-core.exe differ diff --git a/Packages/src/Cli~/Core~/internal/presentation/cli/completion.go b/Packages/src/Cli~/Core~/internal/presentation/cli/completion.go index 727b87c70..bf76d7a92 100644 --- a/Packages/src/Cli~/Core~/internal/presentation/cli/completion.go +++ b/Packages/src/Cli~/Core~/internal/presentation/cli/completion.go @@ -212,11 +212,23 @@ func printOptionsForCommand(command string, cache toolsCache, stdout io.Writer) } func detectShell() string { - if runtime.GOOS == "windows" { + return detectShellFromEnvironment(runtime.GOOS, os.Getenv("SHELL"), os.Getenv("MSYSTEM")) +} + +func detectShellFromEnvironment(goos string, shellPath string, msystem string) string { + posixShell := detectPosixShell(shellPath) + if goos == "windows" { + if posixShell != "" && msystem != "" { + return posixShell + } return "powershell" } - shellPath := os.Getenv("SHELL") + return posixShell +} + +func detectPosixShell(shellPath string) string { + shellPath = strings.ToLower(shellPath) if strings.Contains(shellPath, "zsh") { return "zsh" } diff --git a/Packages/src/Cli~/Core~/internal/presentation/cli/completion_test.go b/Packages/src/Cli~/Core~/internal/presentation/cli/completion_test.go index 3f8743974..04e4ef62a 100644 --- a/Packages/src/Cli~/Core~/internal/presentation/cli/completion_test.go +++ b/Packages/src/Cli~/Core~/internal/presentation/cli/completion_test.go @@ -110,6 +110,24 @@ func TestCompletionPrintsShellScriptWithoutProject(t *testing.T) { } } +// Tests that Git Bash auto-install writes bash completion instead of PowerShell completion. +func TestDetectShellOnWindowsGitBashUsesBash(t *testing.T) { + shellName := detectShellFromEnvironment("windows", "/usr/bin/bash", "MINGW64") + + if shellName != "bash" { + t.Fatalf("windows Git Bash shell mismatch: %s", shellName) + } +} + +// Tests that regular Windows terminals still get the native PowerShell completion default. +func TestDetectShellOnWindowsPowerShellDefaultsToPowerShell(t *testing.T) { + shellName := detectShellFromEnvironment("windows", "", "") + + if shellName != "powershell" { + t.Fatalf("windows default shell mismatch: %s", shellName) + } +} + func containsString(values []string, expected string) bool { for _, value := range values { if value == expected { diff --git a/Packages/src/Cli~/Core~/internal/presentation/cli/help_test.go b/Packages/src/Cli~/Core~/internal/presentation/cli/help_test.go index c831711a7..80f257972 100644 --- a/Packages/src/Cli~/Core~/internal/presentation/cli/help_test.go +++ b/Packages/src/Cli~/Core~/internal/presentation/cli/help_test.go @@ -23,7 +23,7 @@ func TestPrintLauncherHelpListsNativeCommandsAndLiveToolGuidance(t *testing.T) { "uloop list", "--project-path ", "uloop --project-path /path/to/project list", - "uloop completion --list-options ", + "uloop --list-options ", } { if !strings.Contains(output, expected) { t.Fatalf("help output missing %q:\n%s", expected, output) diff --git a/Packages/src/Cli~/Core~/internal/presentation/cli/run_help.go b/Packages/src/Cli~/Core~/internal/presentation/cli/run_help.go index 943a64d63..0585ed4d2 100644 --- a/Packages/src/Cli~/Core~/internal/presentation/cli/run_help.go +++ b/Packages/src/Cli~/Core~/internal/presentation/cli/run_help.go @@ -48,8 +48,8 @@ func printMainHelp(stdout io.Writer, description string, cache toolsCache, hasPr writeLine(stdout, " uloop list Show the live Unity tool list") writeLine(stdout, " uloop --project-path /path/to/project list Show tools for another Unity project") writeLine(stdout, " uloop --help Show help for native commands that support it") - writeLine(stdout, " uloop completion --list-commands Print command names for completion") - writeLine(stdout, " uloop completion --list-options Print options for a Unity tool command") + writeLine(stdout, " uloop --list-commands Print command names for completion") + writeLine(stdout, " uloop --list-options Print options for a Unity tool command") } func printNativeCommandHelp(stdout io.Writer) { diff --git a/Packages/src/Cli~/Core~/internal/presentation/cli/tools.go b/Packages/src/Cli~/Core~/internal/presentation/cli/tools.go index 59fcf73ce..1fd8a6de1 100644 --- a/Packages/src/Cli~/Core~/internal/presentation/cli/tools.go +++ b/Packages/src/Cli~/Core~/internal/presentation/cli/tools.go @@ -129,7 +129,7 @@ func buildToolParams(args []string, tool toolDefinition) (map[string]any, string message: "Unknown option for " + tool.Name + ": --" + flag.name, option: "--" + flag.name, command: tool.Name, - nextActions: []string{"Run `uloop completion --list-options " + tool.Name + "` to inspect supported options."}, + nextActions: []string{"Run `uloop --list-options " + tool.Name + "` to inspect supported options."}, } } diff --git a/Packages/src/Cli~/Dispatcher~/dist/darwin-amd64/uloop-dispatcher b/Packages/src/Cli~/Dispatcher~/dist/darwin-amd64/uloop-dispatcher index 377bd7e68..3ffa89334 100755 Binary files a/Packages/src/Cli~/Dispatcher~/dist/darwin-amd64/uloop-dispatcher and b/Packages/src/Cli~/Dispatcher~/dist/darwin-amd64/uloop-dispatcher differ diff --git a/Packages/src/Cli~/Dispatcher~/dist/darwin-arm64/uloop-dispatcher b/Packages/src/Cli~/Dispatcher~/dist/darwin-arm64/uloop-dispatcher index 4d2c46664..97408888b 100755 Binary files a/Packages/src/Cli~/Dispatcher~/dist/darwin-arm64/uloop-dispatcher and b/Packages/src/Cli~/Dispatcher~/dist/darwin-arm64/uloop-dispatcher differ diff --git a/Packages/src/Cli~/Dispatcher~/dist/windows-amd64/uloop-dispatcher.exe b/Packages/src/Cli~/Dispatcher~/dist/windows-amd64/uloop-dispatcher.exe index fbe9073ea..c1c3924ba 100755 Binary files a/Packages/src/Cli~/Dispatcher~/dist/windows-amd64/uloop-dispatcher.exe and b/Packages/src/Cli~/Dispatcher~/dist/windows-amd64/uloop-dispatcher.exe differ diff --git a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/completion_shell.go b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/completion_shell.go index 6c66f5775..9cb932320 100644 --- a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/completion_shell.go +++ b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/completion_shell.go @@ -8,10 +8,30 @@ import ( ) func detectShell() string { - return detectShellForPlatform(runtime.GOOS, os.Getenv("SHELL"), exec.LookPath) + return detectShellForPlatform(runtime.GOOS, os.Getenv("SHELL"), os.Getenv("MSYSTEM"), exec.LookPath) } -func detectShellForPlatform(goos string, shellPath string, lookPath func(string) (string, error)) string { +func detectShellForPlatform(goos string, shellPath string, msystem string, lookPath func(string) (string, error)) string { + shellName := detectShellName(shellPath) + if goos == "windows" { + if isPosixShell(shellName) && msystem != "" { + return shellName + } + if shellName == "pwsh" || shellName == "powershell" { + return shellName + } + if _, err := lookPath("pwsh"); err == nil { + return "pwsh" + } + if _, err := lookPath("powershell"); err == nil { + return "powershell" + } + return "" + } + return shellName +} + +func detectShellName(shellPath string) string { shellPath = strings.ToLower(shellPath) if strings.Contains(shellPath, "pwsh") { return "pwsh" @@ -25,14 +45,5 @@ func detectShellForPlatform(goos string, shellPath string, lookPath func(string) if strings.Contains(shellPath, "bash") { return "bash" } - if goos == "windows" { - if _, err := lookPath("pwsh"); err == nil { - return "pwsh" - } - if _, err := lookPath("powershell"); err == nil { - return "powershell" - } - return "" - } return "" } diff --git a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/dispatcher_test.go b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/dispatcher_test.go index 82f5d729b..257498ccb 100644 --- a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/dispatcher_test.go +++ b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/dispatcher_test.go @@ -760,7 +760,7 @@ func TestRunCompletionListsNoUpdateOptions(t *testing.T) { func TestDetectShellForPlatformPrefersPwshOnWindows(t *testing.T) { // Verifies that Windows completion install targets PowerShell 7 when it is available. - shell := detectShellForPlatform("windows", "", func(name string) (string, error) { + shell := detectShellForPlatform("windows", "", "", func(name string) (string, error) { if name == "pwsh" { return filepath.Join("bin", "pwsh"), nil } @@ -774,7 +774,32 @@ func TestDetectShellForPlatformPrefersPwshOnWindows(t *testing.T) { func TestDetectShellForPlatformHonorsWindowsPowerShellEnvironment(t *testing.T) { // Verifies that Windows PowerShell is not mistaken for a pwsh profile. - shell := detectShellForPlatform("windows", `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, func(string) (string, error) { + shell := detectShellForPlatform("windows", `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, "", func(string) (string, error) { + return "", os.ErrNotExist + }) + + if shell != "powershell" { + t.Fatalf("shell mismatch: %s", shell) + } +} + +func TestDetectShellForPlatformHonorsWindowsGitBashEnvironment(t *testing.T) { + // Verifies that Git Bash completion install does not fall back to PowerShell. + shell := detectShellForPlatform("windows", "/usr/bin/bash", "MINGW64", func(string) (string, error) { + return "", os.ErrNotExist + }) + + if shell != "bash" { + t.Fatalf("shell mismatch: %s", shell) + } +} + +func TestDetectShellForPlatformIgnoresWindowsBashWithoutMsys(t *testing.T) { + // Verifies that a plain SHELL override is not enough to redirect Windows completion install. + shell := detectShellForPlatform("windows", "/usr/bin/bash", "", func(name string) (string, error) { + if name == "powershell" { + return filepath.Join("bin", "powershell"), nil + } return "", os.ErrNotExist }) diff --git a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/help.go b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/help.go index baffac858..aca318b1b 100644 --- a/Packages/src/Cli~/Dispatcher~/internal/dispatcher/help.go +++ b/Packages/src/Cli~/Dispatcher~/internal/dispatcher/help.go @@ -83,8 +83,8 @@ func printMainHelp(stdout io.Writer, cache cachedTools, hasProjectToolCache bool writeLine(stdout, " uloop list Show the live Unity tool list") writeLine(stdout, " uloop --project-path /path/to/project list Show tools for another Unity project") writeLine(stdout, " uloop --help Show help for native commands that support it") - writeLine(stdout, " uloop completion --list-commands Print command names for completion") - writeLine(stdout, " uloop completion --list-options Print options for a Unity tool command") + writeLine(stdout, " uloop --list-commands Print command names for completion") + writeLine(stdout, " uloop --list-options Print options for a Unity tool command") } func printUnityToolCommandHelp(stdout io.Writer, cache cachedTools, hasProjectToolCache bool) { diff --git a/scripts/install.sh b/scripts/install.sh index 631ced7f0..b5b4e766e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -7,9 +7,9 @@ VERSION="${ULOOP_VERSION:-latest}" report_path_shadowing() { resolved_uloop=$(command -v uloop 2>/dev/null || true) - expected_uloop="$INSTALL_DIR/uloop" + expected_uloop="$INSTALL_DIR/$installed_command_name" - if [ -z "$resolved_uloop" ] || [ "$resolved_uloop" = "$expected_uloop" ]; then + if [ -z "$resolved_uloop" ] || [ "$resolved_uloop" = "$expected_uloop" ] || [ "$resolved_uloop.exe" = "$expected_uloop" ]; then return fi @@ -24,6 +24,7 @@ detect_asset_name() { case "$os" in Darwin) os_name="darwin" ;; + MINGW*|MSYS*) os_name="windows" ;; *) echo "Unsupported OS: $os" >&2 exit 1 @@ -36,19 +37,105 @@ detect_asset_name() { *) echo "Unsupported architecture: $arch" >&2 exit 1 - ;; + ;; esac + if [ "$os_name" = "windows" ]; then + if [ "$arch_name" != "amd64" ]; then + echo "Unsupported Windows architecture: $arch" >&2 + exit 1 + fi + echo "uloop-windows-amd64.zip" + return + fi + echo "uloop-$os_name-$arch_name.tar.gz" } +detect_installed_command_name() { + case "$asset_name" in + *.zip) echo "uloop.exe" ;; + *) echo "uloop" ;; + esac +} + +find_latest_asset_url() { + page=1 + + while :; do + releases_json=$(curl -fsSL "https://api.github.com/repos/$REPOSITORY/releases?per_page=100&page=$page") + asset_url=$(printf '%s\n' "$releases_json" | awk -v asset_name="$asset_name" ' + /"browser_download_url":/ { + line = $0 + sub(/^[[:space:]]*"browser_download_url": "/, "", line) + sub(/",?[[:space:]]*$/, "", line) + count = split(line, parts, "/") + if (parts[count] == asset_name && found == "") { + found = line + } + } + END { + if (found != "") { + print found + } + } + ') + + if [ -n "$asset_url" ]; then + echo "$asset_url" + return + fi + + release_count=$(printf '%s\n' "$releases_json" | awk '/"tag_name":/ { count++ } END { print count + 0 }') + if [ "$release_count" -lt 100 ]; then + return + fi + + page=$((page + 1)) + done +} + +set_download_urls() { + if [ "$VERSION" != "latest" ]; then + download_url="https://github.com/$REPOSITORY/releases/download/$VERSION/$asset_name" + checksum_url="$download_url.sha256" + return + fi + + download_url=$(find_latest_asset_url) + if [ -z "$download_url" ]; then + echo "Could not find a latest release asset named $asset_name." >&2 + echo "Set ULOOP_VERSION to a release tag that provides this asset." >&2 + exit 1 + fi + checksum_url="$download_url.sha256" +} + +extract_asset() { + case "$asset_name" in + *.zip) + if ! command -v unzip >/dev/null 2>&1; then + echo "unzip is required to extract $asset_name" >&2 + exit 1 + fi + unzip -q "$tmp_dir/$asset_name" -d "$tmp_dir" + if [ ! -f "$tmp_dir/$installed_command_name" ]; then + echo "Expected $installed_command_name at archive root after extracting $asset_name." >&2 + exit 1 + fi + return + ;; + *) + tar -xzf "$tmp_dir/$asset_name" -C "$tmp_dir" + ;; + esac +} + asset_name=$(detect_asset_name) -if [ "$VERSION" = "latest" ]; then - download_url="https://github.com/$REPOSITORY/releases/latest/download/$asset_name" -else - download_url="https://github.com/$REPOSITORY/releases/download/$VERSION/$asset_name" -fi -checksum_url="$download_url.sha256" +installed_command_name=$(detect_installed_command_name) +download_url="" +checksum_url="" +set_download_urls tmp_dir=$(mktemp -d) staged_uloop_path="" @@ -81,11 +168,14 @@ mkdir -p "$INSTALL_DIR" curl -fsSL "$download_url" -o "$tmp_dir/$asset_name" curl -fsSL "$checksum_url" -o "$tmp_dir/$asset_name.sha256" verify_checksum -tar -xzf "$tmp_dir/$asset_name" -C "$tmp_dir" +extract_asset staged_uloop_path="$INSTALL_DIR/.uloop-install-$$" -install -m 0755 "$tmp_dir/uloop" "$staged_uloop_path" +if [ "$installed_command_name" = "uloop.exe" ]; then + staged_uloop_path="$staged_uloop_path.exe" +fi +install -m 0755 "$tmp_dir/$installed_command_name" "$staged_uloop_path" "$staged_uloop_path" --version >/dev/null -mv -f "$staged_uloop_path" "$INSTALL_DIR/uloop" +mv -f "$staged_uloop_path" "$INSTALL_DIR/$installed_command_name" staged_uloop_path="" case ":$PATH:" in @@ -97,5 +187,5 @@ case ":$PATH:" in ;; esac -"$INSTALL_DIR/uloop" --version +"$INSTALL_DIR/$installed_command_name" --version report_path_shadowing