From 7271c16295bc28bbcd63f6a10de5a9a08e393ff7 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 00:21:33 +0900 Subject: [PATCH 1/6] Silence legacy npm cleanup failures Keep the success message when the installer removes the old npm package, but avoid surfacing optional cleanup failures during native CLI setup. Update installer tests and docs to match the quieter behavior. --- README.md | 2 +- README_ja.md | 2 +- cli/internal/install/command.go | 51 +------------------ cli/internal/install/command_test.go | 24 +++++---- cli/internal/install/posix_install.go | 19 ------- scripts/install.ps1 | 69 +------------------------- scripts/install.sh | 24 +-------- scripts/test-install-release-filter.sh | 17 ++++--- 8 files changed, 27 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index f52f3f8adf..c86b9588f4 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/insta ``` After installing the native CLI, the installer automatically tries to remove the old npm package with `npm uninstall -g uloop-cli`. -If npm is unavailable or the old command belongs to a different Node prefix, the installer prints the manual command to run: +If the old npm command still shadows the native CLI, run this manually: ```bash npm uninstall -g uloop-cli diff --git a/README_ja.md b/README_ja.md index 95619b5455..9b1b26d23c 100644 --- a/README_ja.md +++ b/README_ja.md @@ -94,7 +94,7 @@ irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/insta ``` native CLI のインストール後、installer は古い npm package を `npm uninstall -g uloop-cli` で自動削除しようとします。 -npm が見つからない場合や、古い command が別の Node prefix に属している場合は、手動で実行する command を表示します。 +古い npm command が native CLI より優先される場合は、手動で実行してください。 ```bash npm uninstall -g uloop-cli diff --git a/cli/internal/install/command.go b/cli/internal/install/command.go index d85d43c2ac..1f6de59e91 100644 --- a/cli/internal/install/command.go +++ b/cli/internal/install/command.go @@ -122,21 +122,6 @@ function Test-LegacyNpmUloopPath { function Write-LegacyNpmMultilineArgumentWarning { Write-Host 'Legacy npm shims can alter multiline PowerShell arguments before the native CLI receives them.' } -function Write-LegacyNpmManualRemoval { - param([string]$LegacyUloopPath, [string]$LegacyPrefix) - Write-Host 'Could not remove the legacy npm package automatically.' - Write-LegacyNpmMultilineArgumentWarning - if ($LegacyUloopPath) { - Write-Host "Legacy uloop command: $LegacyUloopPath" - } - if ($LegacyPrefix) { - Write-Host 'Run this manually if that command still shadows the native CLI:' - Write-Host (' npm uninstall -g --prefix "' + $LegacyPrefix + '" uloop-cli') - return - } - Write-Host 'Run this manually if the old npm command still shadows the native CLI:' - Write-Host ' npm uninstall -g uloop-cli' -} function Remove-LegacyNpmArtifacts { param([string]$LegacyUloopPath, [string]$LegacyPrefix) if ($LegacyUloopPath -and (Test-Path $LegacyUloopPath -PathType Leaf)) { @@ -193,7 +178,6 @@ function Invoke-LegacyNpmPackageRemoval { $LegacyPrefix = Get-NpmPrefixFromUloopPath -CommandPath $LegacyUloopPath } if (-not $LegacyPrefix) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix return $false } if ($NpmCommand) { @@ -204,7 +188,6 @@ function Invoke-LegacyNpmPackageRemoval { Remove-LegacyNpmArtifacts -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix } if (Test-LegacyNpmArtifactsExist -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix return $false } Write-Host 'Removed legacy npm package: uloop-cli' @@ -262,7 +245,6 @@ function Invoke-AllLegacyNpmPackageRemoval { Invoke-DefaultLegacyNpmPackageRemoval foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { if (-not [string]::Equals($LegacyUloopPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix (Get-NpmPrefixFromUloopPath -CommandPath $LegacyUloopPath) $RemovedAll = $false } } @@ -290,34 +272,6 @@ function Set-UserPathWithInstallDirectoryFirst { } $env:Path = [string]::Join(';', @($Directory) + $CurrentPathEntries) } -function Get-FirstUloopCommandFromPath { - param([string]$PathValue) - if (-not $PathValue) { - return $null - } - foreach ($PathEntry in ($PathValue -split ';')) { - if (-not $PathEntry) { - continue - } - foreach ($ShimName in @('uloop.exe', 'uloop.cmd', 'uloop.ps1', 'uloop')) { - $CandidatePath = Join-Path $PathEntry $ShimName - if (Test-Path $CandidatePath -PathType Leaf) { - return $CandidatePath - } - } - } - return $null -} -function Test-PersistedUloopCommandResolvesToExpectedPath { - param([string]$ExpectedUloopPath) - $MachinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') - $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') - $ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(';', @($MachinePath, $UserPath))) - if (-not $ResolvedPath) { - return $false - } - return [string]::Equals($ResolvedPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase) -} function Report-PathShadowing { $ResolvedCommand = Get-Command uloop -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $ResolvedCommand) { @@ -334,10 +288,7 @@ function Report-PathShadowing { Write-Host "Move $InstallDir earlier in PATH, or remove the legacy installation if it owns that command." } Set-UserPathWithInstallDirectoryFirst -Directory $InstallDir -$LegacyCleanupSucceeded = Invoke-AllLegacyNpmPackageRemoval -ExpectedUloopPath $ExpectedUloopPath -if ((-not $LegacyCleanupSucceeded) -and (-not (Test-PersistedUloopCommandResolvesToExpectedPath -ExpectedUloopPath $ExpectedUloopPath))) { - throw 'Failed to remove the legacy npm uloop-cli package.' -} +Invoke-AllLegacyNpmPackageRemoval -ExpectedUloopPath $ExpectedUloopPath | Out-Null Report-PathShadowing `, powerShellSingleQuote(installDir), diff --git a/cli/internal/install/command_test.go b/cli/internal/install/command_test.go index bcd93d5689..46b61ad758 100644 --- a/cli/internal/install/command_test.go +++ b/cli/internal/install/command_test.go @@ -33,12 +33,11 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { "GetExtension($CommandPath), '.exe'", "foreach ($ShimName in @('uloop', 'uloop.cmd', 'uloop.ps1'))", "Invoke-AllLegacyNpmPackageRemoval -ExpectedUloopPath $ExpectedUloopPath", - "npm uninstall -g --prefix", - "npm uninstall -g uloop-cli", + "$NpmArgs = @('uninstall', '-g', '--prefix', $LegacyPrefix, 'uloop-cli')", + "$null = & $NpmCommand.Source @('uninstall', '-g', 'uloop-cli')", "Report-PathShadowing", "function Write-LegacyNpmMultilineArgumentWarning", "if (Test-LegacyNpmUloopPath -CommandPath $ResolvedCommand.Source) {\n Write-LegacyNpmMultilineArgumentWarning\n }", - "foreach ($ShimName in @('uloop.exe', 'uloop.cmd', 'uloop.ps1', 'uloop'))", "Legacy npm shims can alter multiline PowerShell arguments before the native CLI receives them.", } { if !strings.Contains(setupScript, expected) { @@ -60,8 +59,11 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { if count := strings.Count(setupScript, "Legacy npm shims can alter multiline PowerShell arguments before the native CLI receives them."); count != 1 { t.Fatalf("legacy npm multiline warning should have one message definition, got %d", count) } - if count := strings.Count(setupScript, "Write-LegacyNpmMultilineArgumentWarning"); count != 3 { - t.Fatalf("legacy npm multiline warning should be centralized and called from two sites, got %d occurrences", count) + if count := strings.Count(setupScript, "Write-LegacyNpmMultilineArgumentWarning"); count != 2 { + t.Fatalf("legacy npm multiline warning should be centralized and called from one site, got %d occurrences", count) + } + if strings.Contains(setupScript, "Failed to remove the legacy npm uloop-cli package.") { + t.Fatal("legacy npm cleanup failure should not fail installation") } } @@ -511,8 +513,8 @@ func TestPosixInstallScriptRemovesAbsoluteLegacyNpmShimBeforePrependingPath(t *t } } -func TestPosixInstallScriptReportsManualRemovalWhenLegacyShimRemains(t *testing.T) { - // Verifies macOS legacy cleanup does not report success when npm leaves the old shim behind. +func TestPosixInstallScriptSilencesLegacyFailureWhenLegacyShimRemains(t *testing.T) { + // Verifies macOS legacy cleanup stays quiet when npm leaves the old shim behind. if runtime.GOOS == "windows" { t.Skip("POSIX shell setup is not available on Windows") } @@ -561,11 +563,11 @@ func TestPosixInstallScriptReportsManualRemovalWhenLegacyShimRemains(t *testing. if strings.Contains(outputText, "Removed legacy npm package: uloop-cli") { t.Fatalf("cleanup should not report success while the legacy shim remains:\n%s", outputText) } - if !strings.Contains(outputText, "Could not remove the legacy npm package automatically.") { - t.Fatalf("cleanup should print manual removal guidance:\n%s", outputText) + if strings.Contains(outputText, "Could not remove the legacy npm package automatically.") { + t.Fatalf("cleanup should not report legacy npm removal failure:\n%s", outputText) } - if !strings.Contains(outputText, "npm uninstall -g --prefix \""+legacyPrefix+"\" uloop-cli") { - t.Fatalf("manual removal guidance should include the inferred prefix:\n%s", outputText) + if strings.Contains(outputText, "npm uninstall -g --prefix \""+legacyPrefix+"\" uloop-cli") { + t.Fatalf("cleanup should not print manual removal guidance:\n%s", outputText) } if _, err := os.Stat(legacyUloop); err != nil { t.Fatalf("legacy uloop should remain for this scenario: %v", err) diff --git a/cli/internal/install/posix_install.go b/cli/internal/install/posix_install.go index efd89290e3..df0a610332 100644 --- a/cli/internal/install/posix_install.go +++ b/cli/internal/install/posix_install.go @@ -227,22 +227,6 @@ is_legacy_npm_uloop_path() { return 1 } -print_legacy_npm_manual_removal() { - legacy_uloop=$1 - legacy_prefix=$2 - echo "Could not remove the legacy npm package automatically." - if [ -n "$legacy_uloop" ]; then - echo "Legacy uloop command: $legacy_uloop" - fi - if [ -n "$legacy_prefix" ]; then - echo "Run this manually if that command still shadows the native CLI:" - echo " npm uninstall -g --prefix \"$legacy_prefix\" uloop-cli" - return - fi - echo "Run this manually if the old npm command still shadows the native CLI:" - echo " npm uninstall -g uloop-cli" -} - try_remove_legacy_npm_package() { legacy_uloop=$1 legacy_prefix="" @@ -251,11 +235,9 @@ try_remove_legacy_npm_package() { fi legacy_prefix=$(infer_npm_prefix_from_uloop_path "$legacy_uloop") if [ -z "$legacy_prefix" ]; then - print_legacy_npm_manual_removal "$legacy_uloop" "" return fi if ! command -v npm >/dev/null 2>&1; then - print_legacy_npm_manual_removal "$legacy_uloop" "$legacy_prefix" return fi if npm uninstall -g --prefix "$legacy_prefix" uloop-cli; then @@ -264,7 +246,6 @@ try_remove_legacy_npm_package() { return fi fi - print_legacy_npm_manual_removal "$legacy_uloop" "$legacy_prefix" } try_remove_default_legacy_npm_package() { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 7a9dcf7886..878a50fe48 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -202,27 +202,6 @@ function Test-LegacyNpmUloopPath { return $CommandContent.Contains("node_modules/uloop-cli") -or $CommandContent.Contains("node_modules\uloop-cli") } -function Write-LegacyNpmManualRemoval { - param( - [string]$LegacyUloopPath, - [string]$LegacyPrefix - ) - - Write-Host "Could not remove the legacy npm package automatically." - if ($LegacyUloopPath) { - Write-Host "Legacy uloop command: $LegacyUloopPath" - } - - if ($LegacyPrefix) { - Write-Host "Run this manually if that command still shadows the native CLI:" - Write-Host (' npm uninstall -g --prefix "' + $LegacyPrefix + '" uloop-cli') - return - } - - Write-Host "Run this manually if the old npm command still shadows the native CLI:" - Write-Host " npm uninstall -g uloop-cli" -} - function Remove-LegacyNpmArtifacts { param( [string]$LegacyUloopPath, @@ -303,7 +282,6 @@ function Invoke-LegacyNpmPackageRemoval { } if (-not $LegacyPrefix) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix return $false } @@ -317,7 +295,6 @@ function Invoke-LegacyNpmPackageRemoval { } if (Test-LegacyNpmArtifactsExist -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix $LegacyPrefix return $false } @@ -391,7 +368,6 @@ function Invoke-AllLegacyNpmPackageRemoval { foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { if (-not [string]::Equals($LegacyUloopPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { - Write-LegacyNpmManualRemoval -LegacyUloopPath $LegacyUloopPath -LegacyPrefix (Get-NpmPrefixFromUloopPath -CommandPath $LegacyUloopPath) $RemovedAll = $false } } @@ -436,46 +412,6 @@ function Set-UserPathWithInstallDirectoryFirst { Set-CurrentPathWithInstallDirectoryFirst -Directory $Directory } -function Get-FirstUloopCommandFromPath { - param( - [string]$PathValue - ) - - if (-not $PathValue) { - return $null - } - - foreach ($PathEntry in ($PathValue -split ";")) { - if (-not $PathEntry) { - continue - } - - foreach ($ShimName in @("uloop.exe", "uloop.cmd", "uloop.ps1", "uloop")) { - $CandidatePath = Join-Path $PathEntry $ShimName - if (Test-Path $CandidatePath -PathType Leaf) { - return $CandidatePath - } - } - } - - return $null -} - -function Test-PersistedUloopCommandResolvesToExpectedPath { - param( - [string]$ExpectedUloopPath - ) - - $MachinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") - $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") - $ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(";", @($MachinePath, $UserPath))) - if (-not $ResolvedPath) { - return $false - } - - return [string]::Equals($ResolvedPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase) -} - function Report-PathShadowing { param( [string]$Directory, @@ -506,10 +442,7 @@ function Invoke-CompatibilityWindowsInstall { Write-Host "Configuring global uloop launcher..." Set-UserPathWithInstallDirectoryFirst -Directory $Directory - $LegacyCleanupSucceeded = Invoke-AllLegacyNpmPackageRemoval -ExpectedUloopPath $ExpectedUloopPath - if ((-not $LegacyCleanupSucceeded) -and (-not (Test-PersistedUloopCommandResolvesToExpectedPath -ExpectedUloopPath $ExpectedUloopPath))) { - throw "Failed to remove the legacy npm uloop-cli package." - } + Invoke-AllLegacyNpmPackageRemoval -ExpectedUloopPath $ExpectedUloopPath | Out-Null Report-PathShadowing -Directory $Directory -ExpectedUloopPath $ExpectedUloopPath Write-Host "The package-owned User PATH entry was configured." diff --git a/scripts/install.sh b/scripts/install.sh index de2426ee08..cf8549dbd4 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -96,25 +96,6 @@ is_legacy_npm_uloop_path() { return 1 } -print_legacy_npm_manual_removal() { - legacy_uloop=$1 - legacy_prefix=$2 - - echo "Could not remove the legacy npm package automatically." - if [ -n "$legacy_uloop" ]; then - echo "Legacy uloop command: $legacy_uloop" - fi - - if [ -n "$legacy_prefix" ]; then - echo "Run this manually if that command still shadows the native CLI:" - echo " npm uninstall -g --prefix \"$legacy_prefix\" uloop-cli" - return - fi - - echo "Run this manually if the old npm command still shadows the native CLI:" - echo " npm uninstall -g uloop-cli" -} - quote_for_single_quoted_shell() { printf "%s" "$1" | sed "s/'/'\\\\''/g" } @@ -239,26 +220,23 @@ try_remove_legacy_npm_package() { fi if [ -z "$legacy_prefix" ]; then - print_legacy_npm_manual_removal "$legacy_uloop" "" return fi if ! command -v npm >/dev/null 2>&1; then - print_legacy_npm_manual_removal "$legacy_uloop" "$legacy_prefix" return fi if [ -n "$legacy_prefix" ]; then if npm uninstall -g --prefix "$legacy_prefix" uloop-cli; then if [ -e "$legacy_uloop" ] || [ -L "$legacy_uloop" ]; then - print_legacy_npm_manual_removal "$legacy_uloop" "$legacy_prefix" + return else echo "Removed legacy npm package: uloop-cli" fi return fi - print_legacy_npm_manual_removal "$legacy_uloop" "$legacy_prefix" return fi } diff --git a/scripts/test-install-release-filter.sh b/scripts/test-install-release-filter.sh index 00a2816f8a..2975668677 100755 --- a/scripts/test-install-release-filter.sh +++ b/scripts/test-install-release-filter.sh @@ -769,7 +769,7 @@ test_posix_does_not_infer_npm_prefix_from_non_npm_command() { fi } -test_posix_prints_prefix_manual_cleanup_when_npm_is_unavailable() { +test_posix_silences_legacy_cleanup_when_npm_is_unavailable() { work_dir="$TMP_DIR/posix-no-npm" mock_bin="$work_dir/bin" tool_bin="$work_dir/tools" @@ -801,11 +801,12 @@ test_posix_prints_prefix_manual_cleanup_when_npm_is_unavailable() { LEGACY_ULOOP="$legacy_uloop" \ "$ROOT_DIR/scripts/install.sh" > "$work_dir/output.txt" 2> "$work_dir/stderr.txt" - assert_contains "$work_dir/output.txt" "npm uninstall -g --prefix \"$work_dir/npm-global\" uloop-cli" + assert_not_contains "$work_dir/output.txt" "Could not remove the legacy npm package automatically." + assert_not_contains "$work_dir/output.txt" "npm uninstall -g --prefix \"$work_dir/npm-global\" uloop-cli" assert_not_contains "$work_dir/output.txt" "Run this manually if the old npm command still shadows the native CLI:" } -test_posix_prints_manual_cleanup_when_npm_prefix_cannot_be_inferred() { +test_posix_silences_legacy_cleanup_when_npm_prefix_cannot_be_inferred() { work_dir="$TMP_DIR/posix-unknown-npm-prefix" mock_bin="$work_dir/bin" legacy_bin="$work_dir/custom-shims" @@ -834,9 +835,9 @@ test_posix_prints_manual_cleanup_when_npm_prefix_cannot_be_inferred() { assert_not_contains "$npm_log" "uninstall -g uloop-cli" assert_not_contains "$npm_log" "uninstall -g --prefix" - assert_contains "$work_dir/output.txt" "Could not remove the legacy npm package automatically." - assert_contains "$work_dir/output.txt" "Legacy uloop command: $legacy_uloop" - assert_contains "$work_dir/output.txt" "Run this manually if the old npm command still shadows the native CLI:" + assert_not_contains "$work_dir/output.txt" "Could not remove the legacy npm package automatically." + assert_not_contains "$work_dir/output.txt" "Legacy uloop command: $legacy_uloop" + assert_not_contains "$work_dir/output.txt" "Run this manually if the old npm command still shadows the native CLI:" } test_posix_removes_npm_package_before_replacing_same_bin_path() { @@ -959,8 +960,8 @@ test_posix_prints_fish_path_guidance_without_writing_profile test_posix_prints_generic_path_guidance_for_unknown_shell test_posix_skips_default_npm_cleanup_when_native_command_is_first test_posix_does_not_infer_npm_prefix_from_non_npm_command -test_posix_prints_prefix_manual_cleanup_when_npm_is_unavailable -test_posix_prints_manual_cleanup_when_npm_prefix_cannot_be_inferred +test_posix_silences_legacy_cleanup_when_npm_is_unavailable +test_posix_silences_legacy_cleanup_when_npm_prefix_cannot_be_inferred test_posix_removes_npm_package_before_replacing_same_bin_path test_powershell_latest_skips_prerelease_assets test_git_bash_latest_installs_windows_zip_asset From 9242799de48115cae7a0137e3aada93444f812e5 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 00:34:46 +0900 Subject: [PATCH 2/6] Preserve Windows path shadow warnings Keep legacy npm cleanup failures quiet while still checking the persisted Machine and User PATH that new Windows shells will use. This avoids reporting setup success when a remaining legacy shim shadows the native CLI. --- cli/internal/install/command.go | 30 ++++++++++++++++++---- cli/internal/install/command_test.go | 8 +++++- scripts/install.ps1 | 35 +++++++++++++++++++++++--- scripts/test-install-release-filter.sh | 8 ++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/cli/internal/install/command.go b/cli/internal/install/command.go index 1f6de59e91..b8889018f4 100644 --- a/cli/internal/install/command.go +++ b/cli/internal/install/command.go @@ -272,17 +272,37 @@ function Set-UserPathWithInstallDirectoryFirst { } $env:Path = [string]::Join(';', @($Directory) + $CurrentPathEntries) } +function Get-FirstUloopCommandFromPath { + param([string]$PathValue) + if (-not $PathValue) { + return $null + } + foreach ($PathEntry in ($PathValue -split ';')) { + if (-not $PathEntry) { + continue + } + foreach ($ShimName in @('uloop.exe', 'uloop.cmd', 'uloop.ps1', 'uloop')) { + $CandidatePath = Join-Path $PathEntry $ShimName + if (Test-Path $CandidatePath -PathType Leaf) { + return $CandidatePath + } + } + } + return $null +} function Report-PathShadowing { - $ResolvedCommand = Get-Command uloop -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $ResolvedCommand) { + $MachinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') + $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') + $ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(';', @($MachinePath, $UserPath))) + if (-not $ResolvedPath) { return } - if ([string]::Equals($ResolvedCommand.Source, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { + if ([string]::Equals($ResolvedPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { return } Write-Host "Installed uloop to $ExpectedUloopPath, but PATH resolves uloop to:" - Write-Host " $($ResolvedCommand.Source)" - if (Test-LegacyNpmUloopPath -CommandPath $ResolvedCommand.Source) { + Write-Host " $ResolvedPath" + if (Test-LegacyNpmUloopPath -CommandPath $ResolvedPath) { Write-LegacyNpmMultilineArgumentWarning } Write-Host "Move $InstallDir earlier in PATH, or remove the legacy installation if it owns that command." diff --git a/cli/internal/install/command_test.go b/cli/internal/install/command_test.go index 46b61ad758..0d3a5bba9f 100644 --- a/cli/internal/install/command_test.go +++ b/cli/internal/install/command_test.go @@ -36,8 +36,11 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { "$NpmArgs = @('uninstall', '-g', '--prefix', $LegacyPrefix, 'uloop-cli')", "$null = & $NpmCommand.Source @('uninstall', '-g', 'uloop-cli')", "Report-PathShadowing", + "function Get-FirstUloopCommandFromPath", + "$MachinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')", + "$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(';', @($MachinePath, $UserPath)))", "function Write-LegacyNpmMultilineArgumentWarning", - "if (Test-LegacyNpmUloopPath -CommandPath $ResolvedCommand.Source) {\n Write-LegacyNpmMultilineArgumentWarning\n }", + "if (Test-LegacyNpmUloopPath -CommandPath $ResolvedPath) {\n Write-LegacyNpmMultilineArgumentWarning\n }", "Legacy npm shims can alter multiline PowerShell arguments before the native CLI receives them.", } { if !strings.Contains(setupScript, expected) { @@ -65,6 +68,9 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { if strings.Contains(setupScript, "Failed to remove the legacy npm uloop-cli package.") { t.Fatal("legacy npm cleanup failure should not fail installation") } + if strings.Contains(setupScript, "Get-Command uloop -ErrorAction SilentlyContinue") { + t.Fatal("Windows path shadowing should inspect persisted PATH instead of the current process PATH") + } } func TestCommandForMacConfiguresShellPathAndLegacyCleanup(t *testing.T) { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 878a50fe48..539385bc5f 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -412,23 +412,50 @@ function Set-UserPathWithInstallDirectoryFirst { Set-CurrentPathWithInstallDirectoryFirst -Directory $Directory } +function Get-FirstUloopCommandFromPath { + param( + [string]$PathValue + ) + + if (-not $PathValue) { + return $null + } + + foreach ($PathEntry in ($PathValue -split ";")) { + if (-not $PathEntry) { + continue + } + + foreach ($ShimName in @("uloop.exe", "uloop.cmd", "uloop.ps1", "uloop")) { + $CandidatePath = Join-Path $PathEntry $ShimName + if (Test-Path $CandidatePath -PathType Leaf) { + return $CandidatePath + } + } + } + + return $null +} + function Report-PathShadowing { param( [string]$Directory, [string]$ExpectedUloopPath ) - $ResolvedCommand = Get-Command uloop -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $ResolvedCommand) { + $MachinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") + $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") + $ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(";", @($MachinePath, $UserPath))) + if (-not $ResolvedPath) { return } - if ([string]::Equals($ResolvedCommand.Source, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { + if ([string]::Equals($ResolvedPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { return } Write-Host "Installed uloop to $ExpectedUloopPath, but PATH resolves uloop to:" - Write-Host " $($ResolvedCommand.Source)" + Write-Host " $ResolvedPath" Write-Host "Move $Directory earlier in PATH, or remove the legacy installation if it owns that command." } diff --git a/scripts/test-install-release-filter.sh b/scripts/test-install-release-filter.sh index 2975668677..2e56bb2c0d 100755 --- a/scripts/test-install-release-filter.sh +++ b/scripts/test-install-release-filter.sh @@ -948,6 +948,13 @@ test_powershell_native_probe_restores_error_action_preference() { assert_contains "$ROOT_DIR/scripts/install.ps1" '$ErrorActionPreference = $PreviousErrorActionPreference' } +test_powershell_reports_persisted_path_shadowing() { + assert_contains "$ROOT_DIR/scripts/install.ps1" 'function Get-FirstUloopCommandFromPath' + assert_contains "$ROOT_DIR/scripts/install.ps1" '[Environment]::GetEnvironmentVariable("Path", "Machine")' + assert_contains "$ROOT_DIR/scripts/install.ps1" '$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(";", @($MachinePath, $UserPath)))' + assert_not_contains "$ROOT_DIR/scripts/install.ps1" 'Get-Command uloop -ErrorAction SilentlyContinue' +} + test_posix_latest_skips_prerelease_assets test_posix_latest_beta_selects_prerelease_assets test_posix_invokes_native_install_setup @@ -968,3 +975,4 @@ test_git_bash_latest_installs_windows_zip_asset test_powershell_installer_avoids_optional_archive_cmdlets test_powershell_installer_uses_non_installer_staged_executable_name test_powershell_native_probe_restores_error_action_preference +test_powershell_reports_persisted_path_shadowing From 3c652a060aa26fa0ee5ca73f4a23d0cc30131fa6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 00:51:09 +0900 Subject: [PATCH 3/6] Remove README cleanup guidance changes Leave the README wording unchanged because the installer behavior change should stay scoped to setup output and tests. --- README.md | 2 +- README_ja.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c86b9588f4..f52f3f8adf 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/insta ``` After installing the native CLI, the installer automatically tries to remove the old npm package with `npm uninstall -g uloop-cli`. -If the old npm command still shadows the native CLI, run this manually: +If npm is unavailable or the old command belongs to a different Node prefix, the installer prints the manual command to run: ```bash npm uninstall -g uloop-cli diff --git a/README_ja.md b/README_ja.md index 9b1b26d23c..95619b5455 100644 --- a/README_ja.md +++ b/README_ja.md @@ -94,7 +94,7 @@ irm https://raw.githubusercontent.com/hatayama/unity-cli-loop/main/scripts/insta ``` native CLI のインストール後、installer は古い npm package を `npm uninstall -g uloop-cli` で自動削除しようとします。 -古い npm command が native CLI より優先される場合は、手動で実行してください。 +npm が見つからない場合や、古い command が別の Node prefix に属している場合は、手動で実行する command を表示します。 ```bash npm uninstall -g uloop-cli From a4e82eab629e08140db02793b11cdc6e843ba077 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 01:01:20 +0900 Subject: [PATCH 4/6] Bump dispatcher release version Advance the dispatcher release version because this PR changes dispatcher release inputs in the installer scripts. --- cli/dispatcher-contract.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/dispatcher-contract.json b/cli/dispatcher-contract.json index a99c6b77df..efaceaec01 100644 --- a/cli/dispatcher-contract.json +++ b/cli/dispatcher-contract.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, - "dispatcherVersion": "3.0.1-beta.8", + "dispatcherVersion": "3.0.1-beta.9", "dispatcherContractVersion": 1 } From 09e73c660e93549b17d125bf445cd9378949b9e4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 01:08:02 +0900 Subject: [PATCH 5/6] Normalize Windows PATH shadow probes Normalize persisted Machine and User PATH entries before probing for uloop shims, and remove the unused aggregate cleanup result now that cleanup failures are intentionally silent. --- cli/internal/install/command.go | 16 ++++------------ cli/internal/install/command_test.go | 5 +++++ scripts/install.ps1 | 18 ++++-------------- scripts/test-install-release-filter.sh | 3 +++ 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/cli/internal/install/command.go b/cli/internal/install/command.go index b8889018f4..b46d9c80a1 100644 --- a/cli/internal/install/command.go +++ b/cli/internal/install/command.go @@ -236,19 +236,10 @@ function Get-LegacyNpmUloopPathsFromPath { } function Invoke-AllLegacyNpmPackageRemoval { param([string]$ExpectedUloopPath) - $RemovedAll = $true foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { - if (-not (Invoke-LegacyNpmPackageRemoval -LegacyUloopPath $LegacyUloopPath -ExpectedUloopPath $ExpectedUloopPath)) { - $RemovedAll = $false - } + Invoke-LegacyNpmPackageRemoval -LegacyUloopPath $LegacyUloopPath -ExpectedUloopPath $ExpectedUloopPath | Out-Null } Invoke-DefaultLegacyNpmPackageRemoval - foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { - if (-not [string]::Equals($LegacyUloopPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { - $RemovedAll = $false - } - } - return $RemovedAll } function Set-UserPathWithInstallDirectoryFirst { param([string]$Directory) @@ -278,11 +269,12 @@ function Get-FirstUloopCommandFromPath { return $null } foreach ($PathEntry in ($PathValue -split ';')) { - if (-not $PathEntry) { + $NormalizedPathEntry = & $NormalizePath $PathEntry + if (-not $NormalizedPathEntry) { continue } foreach ($ShimName in @('uloop.exe', 'uloop.cmd', 'uloop.ps1', 'uloop')) { - $CandidatePath = Join-Path $PathEntry $ShimName + $CandidatePath = Join-Path $NormalizedPathEntry $ShimName if (Test-Path $CandidatePath -PathType Leaf) { return $CandidatePath } diff --git a/cli/internal/install/command_test.go b/cli/internal/install/command_test.go index 0d3a5bba9f..d276de2a80 100644 --- a/cli/internal/install/command_test.go +++ b/cli/internal/install/command_test.go @@ -37,6 +37,8 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { "$null = & $NpmCommand.Source @('uninstall', '-g', 'uloop-cli')", "Report-PathShadowing", "function Get-FirstUloopCommandFromPath", + "$NormalizedPathEntry = & $NormalizePath $PathEntry", + "$CandidatePath = Join-Path $NormalizedPathEntry $ShimName", "$MachinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')", "$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(';', @($MachinePath, $UserPath)))", "function Write-LegacyNpmMultilineArgumentWarning", @@ -71,6 +73,9 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { if strings.Contains(setupScript, "Get-Command uloop -ErrorAction SilentlyContinue") { t.Fatal("Windows path shadowing should inspect persisted PATH instead of the current process PATH") } + if strings.Contains(setupScript, "$RemovedAll") { + t.Fatal("legacy npm cleanup aggregate result should not be tracked when it is intentionally ignored") + } } func TestCommandForMacConfiguresShellPathAndLegacyCleanup(t *testing.T) { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 539385bc5f..a32ad1e945 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -357,22 +357,11 @@ function Invoke-AllLegacyNpmPackageRemoval { [string]$ExpectedUloopPath ) - $RemovedAll = $true foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { - if (-not (Invoke-LegacyNpmPackageRemoval -LegacyUloopPath $LegacyUloopPath -ExpectedUloopPath $ExpectedUloopPath)) { - $RemovedAll = $false - } + Invoke-LegacyNpmPackageRemoval -LegacyUloopPath $LegacyUloopPath -ExpectedUloopPath $ExpectedUloopPath | Out-Null } Invoke-DefaultLegacyNpmPackageRemoval - - foreach ($LegacyUloopPath in (Get-LegacyNpmUloopPathsFromPath)) { - if (-not [string]::Equals($LegacyUloopPath, $ExpectedUloopPath, [System.StringComparison]::OrdinalIgnoreCase)) { - $RemovedAll = $false - } - } - - return $RemovedAll } function Set-CurrentPathWithInstallDirectoryFirst { @@ -422,12 +411,13 @@ function Get-FirstUloopCommandFromPath { } foreach ($PathEntry in ($PathValue -split ";")) { - if (-not $PathEntry) { + $NormalizedPathEntry = ConvertTo-NormalizedPath -Path $PathEntry + if (-not $NormalizedPathEntry) { continue } foreach ($ShimName in @("uloop.exe", "uloop.cmd", "uloop.ps1", "uloop")) { - $CandidatePath = Join-Path $PathEntry $ShimName + $CandidatePath = Join-Path $NormalizedPathEntry $ShimName if (Test-Path $CandidatePath -PathType Leaf) { return $CandidatePath } diff --git a/scripts/test-install-release-filter.sh b/scripts/test-install-release-filter.sh index 2e56bb2c0d..063156c5dc 100755 --- a/scripts/test-install-release-filter.sh +++ b/scripts/test-install-release-filter.sh @@ -950,9 +950,12 @@ test_powershell_native_probe_restores_error_action_preference() { test_powershell_reports_persisted_path_shadowing() { assert_contains "$ROOT_DIR/scripts/install.ps1" 'function Get-FirstUloopCommandFromPath' + assert_contains "$ROOT_DIR/scripts/install.ps1" '$NormalizedPathEntry = ConvertTo-NormalizedPath -Path $PathEntry' + assert_contains "$ROOT_DIR/scripts/install.ps1" '$CandidatePath = Join-Path $NormalizedPathEntry $ShimName' assert_contains "$ROOT_DIR/scripts/install.ps1" '[Environment]::GetEnvironmentVariable("Path", "Machine")' assert_contains "$ROOT_DIR/scripts/install.ps1" '$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(";", @($MachinePath, $UserPath)))' assert_not_contains "$ROOT_DIR/scripts/install.ps1" 'Get-Command uloop -ErrorAction SilentlyContinue' + assert_not_contains "$ROOT_DIR/scripts/install.ps1" '$RemovedAll' } test_posix_latest_skips_prerelease_assets From 0b21cdfe3f7b9149f8ab1e90a31fd47d6b70d5fd Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 2 Jul 2026 01:15:25 +0900 Subject: [PATCH 6/6] Preserve Windows drive-root PATH probes Keep normalized drive-root PATH entries usable for uloop shadow detection by restoring the trailing slash before filesystem probing. --- cli/internal/install/command.go | 3 +++ cli/internal/install/command_test.go | 1 + scripts/install.ps1 | 3 +++ scripts/test-install-release-filter.sh | 2 ++ 4 files changed, 9 insertions(+) diff --git a/cli/internal/install/command.go b/cli/internal/install/command.go index b46d9c80a1..01c32435e0 100644 --- a/cli/internal/install/command.go +++ b/cli/internal/install/command.go @@ -273,6 +273,9 @@ function Get-FirstUloopCommandFromPath { if (-not $NormalizedPathEntry) { continue } + if ($NormalizedPathEntry -match '^[A-Za-z]:$') { + $NormalizedPathEntry = $NormalizedPathEntry + '\' + } foreach ($ShimName in @('uloop.exe', 'uloop.cmd', 'uloop.ps1', 'uloop')) { $CandidatePath = Join-Path $NormalizedPathEntry $ShimName if (Test-Path $CandidatePath -PathType Leaf) { diff --git a/cli/internal/install/command_test.go b/cli/internal/install/command_test.go index d276de2a80..d5bb4e00e4 100644 --- a/cli/internal/install/command_test.go +++ b/cli/internal/install/command_test.go @@ -38,6 +38,7 @@ func TestCommandForWindowsConfiguresUserPathAndLegacyCleanup(t *testing.T) { "Report-PathShadowing", "function Get-FirstUloopCommandFromPath", "$NormalizedPathEntry = & $NormalizePath $PathEntry", + "if ($NormalizedPathEntry -match '^[A-Za-z]:$') {\n $NormalizedPathEntry = $NormalizedPathEntry + '\\'\n }", "$CandidatePath = Join-Path $NormalizedPathEntry $ShimName", "$MachinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')", "$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(';', @($MachinePath, $UserPath)))", diff --git a/scripts/install.ps1 b/scripts/install.ps1 index a32ad1e945..08fed7a511 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -415,6 +415,9 @@ function Get-FirstUloopCommandFromPath { if (-not $NormalizedPathEntry) { continue } + if ($NormalizedPathEntry -match "^[A-Za-z]:$") { + $NormalizedPathEntry = $NormalizedPathEntry + "\" + } foreach ($ShimName in @("uloop.exe", "uloop.cmd", "uloop.ps1", "uloop")) { $CandidatePath = Join-Path $NormalizedPathEntry $ShimName diff --git a/scripts/test-install-release-filter.sh b/scripts/test-install-release-filter.sh index 063156c5dc..3ab9ebc122 100755 --- a/scripts/test-install-release-filter.sh +++ b/scripts/test-install-release-filter.sh @@ -951,6 +951,8 @@ test_powershell_native_probe_restores_error_action_preference() { test_powershell_reports_persisted_path_shadowing() { assert_contains "$ROOT_DIR/scripts/install.ps1" 'function Get-FirstUloopCommandFromPath' assert_contains "$ROOT_DIR/scripts/install.ps1" '$NormalizedPathEntry = ConvertTo-NormalizedPath -Path $PathEntry' + assert_contains "$ROOT_DIR/scripts/install.ps1" 'if ($NormalizedPathEntry -match "^[A-Za-z]:$")' + assert_contains "$ROOT_DIR/scripts/install.ps1" '$NormalizedPathEntry = $NormalizedPathEntry + "\"' assert_contains "$ROOT_DIR/scripts/install.ps1" '$CandidatePath = Join-Path $NormalizedPathEntry $ShimName' assert_contains "$ROOT_DIR/scripts/install.ps1" '[Environment]::GetEnvironmentVariable("Path", "Machine")' assert_contains "$ROOT_DIR/scripts/install.ps1" '$ResolvedPath = Get-FirstUloopCommandFromPath -PathValue ([string]::Join(";", @($MachinePath, $UserPath)))'