@cats-inc/cats-platform 0.2.8 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +17 -0
  2. package/build/desktop/bootstrapPage.d.ts.map +1 -1
  3. package/build/desktop/bootstrapPage.js +26 -129
  4. package/build/desktop/bootstrapPage.js.map +1 -1
  5. package/build/desktop/cliInventoryProbe.d.ts +6 -0
  6. package/build/desktop/cliInventoryProbe.d.ts.map +1 -1
  7. package/build/desktop/cliInventoryProbe.js +6 -3
  8. package/build/desktop/cliInventoryProbe.js.map +1 -1
  9. package/build/desktop/contracts.d.ts +2 -0
  10. package/build/desktop/contracts.d.ts.map +1 -1
  11. package/build/desktop/contracts.js.map +1 -1
  12. package/build/desktop/main.js +95 -41
  13. package/build/desktop/main.js.map +1 -1
  14. package/build/desktop/preload.cjs +8 -6
  15. package/build/desktop/preload.cjs.map +1 -1
  16. package/build/desktop/providerManager.d.ts +46 -0
  17. package/build/desktop/providerManager.d.ts.map +1 -0
  18. package/build/desktop/providerManager.js +220 -0
  19. package/build/desktop/providerManager.js.map +1 -0
  20. package/build/desktop/providerSelection.d.ts +1 -0
  21. package/build/desktop/providerSelection.d.ts.map +1 -1
  22. package/build/desktop/providerSelection.js +7 -1
  23. package/build/desktop/providerSelection.js.map +1 -1
  24. package/build/desktop/setupAssets.d.ts.map +1 -1
  25. package/build/desktop/setupAssets.js +12 -0
  26. package/build/desktop/setupAssets.js.map +1 -1
  27. package/build/desktop/setupAudit.d.ts +3 -1
  28. package/build/desktop/setupAudit.d.ts.map +1 -1
  29. package/build/desktop/setupAudit.js +12 -8
  30. package/build/desktop/setupAudit.js.map +1 -1
  31. package/build/desktop/setupBridge.js +1 -1
  32. package/build/desktop/setupBridge.js.map +1 -1
  33. package/build/desktop/setupPrerequisites.d.ts +11 -0
  34. package/build/desktop/setupPrerequisites.d.ts.map +1 -0
  35. package/build/desktop/setupPrerequisites.js +47 -0
  36. package/build/desktop/setupPrerequisites.js.map +1 -0
  37. package/build/renderer/assets/ActiveSessionPermissionChip-B49WcZBJ.js +1 -0
  38. package/build/renderer/assets/{App-BZA23O2J.js → App-BpxZB5ym.js} +1 -1
  39. package/build/renderer/assets/{App-rGw9J8PZ.js → App-DYTUfAl6.js} +1 -1
  40. package/build/renderer/assets/{App-YqprEL2n.js → App-boB4WrmU.js} +1 -1
  41. package/build/renderer/assets/{WorkspaceAppRoutes-BCHCikmJ.js → WorkspaceAppRoutes-CPqz85iw.js} +1 -1
  42. package/build/renderer/assets/app-C52wwCGz.js +85 -0
  43. package/build/renderer/assets/app-CqnsY5ed.css +1 -0
  44. package/build/renderer/assets/{client-BdGHAUoK.js → client-DREGSZqQ.js} +1 -1
  45. package/build/renderer/assets/conversationSidebarRecentEntries-DK0iWS73.js +6 -0
  46. package/build/renderer/assets/{screenshotOverlay-DfBcolX-.js → screenshotOverlay-EF07cXvw.js} +1 -1
  47. package/build/renderer/assets/{useDraftSessionChips-D9BX6X92.js → useDraftSessionChips-BdZ_B1gy.js} +1 -1
  48. package/build/renderer/desktop/overlay/index.html +2 -2
  49. package/build/renderer/index.html +3 -3
  50. package/build/server/app/renderer/auth/api.d.ts.map +1 -1
  51. package/build/server/app/renderer/auth/api.js +4 -1
  52. package/build/server/app/renderer/auth/api.js.map +1 -1
  53. package/build/server/app/renderer/providerCatalogClient.d.ts.map +1 -1
  54. package/build/server/app/renderer/providerCatalogClient.js +19 -6
  55. package/build/server/app/renderer/providerCatalogClient.js.map +1 -1
  56. package/build/server/app/renderer/providerClientInvalidation.d.ts +6 -0
  57. package/build/server/app/renderer/providerClientInvalidation.d.ts.map +1 -0
  58. package/build/server/app/renderer/providerClientInvalidation.js +12 -0
  59. package/build/server/app/renderer/providerClientInvalidation.js.map +1 -0
  60. package/build/server/app/renderer/providerReadLoop.d.ts +5 -0
  61. package/build/server/app/renderer/providerReadLoop.d.ts.map +1 -0
  62. package/build/server/app/renderer/providerReadLoop.js +52 -0
  63. package/build/server/app/renderer/providerReadLoop.js.map +1 -0
  64. package/build/server/app/renderer/providerRegistryClient.d.ts +2 -1
  65. package/build/server/app/renderer/providerRegistryClient.d.ts.map +1 -1
  66. package/build/server/app/renderer/providerRegistryClient.js +33 -17
  67. package/build/server/app/renderer/providerRegistryClient.js.map +1 -1
  68. package/build/server/app/server/authGatePolicy.js +7 -0
  69. package/build/server/app/server/authGatePolicy.js.map +1 -1
  70. package/build/server/app/server/subscribeRoutes.d.ts.map +1 -1
  71. package/build/server/app/server/subscribeRoutes.js +5 -3
  72. package/build/server/app/server/subscribeRoutes.js.map +1 -1
  73. package/build/server/design/components/providerModelFieldsSupport.d.ts +2 -21
  74. package/build/server/design/components/providerModelFieldsSupport.d.ts.map +1 -1
  75. package/build/server/design/components/providerModelFieldsSupport.js +32 -36
  76. package/build/server/design/components/providerModelFieldsSupport.js.map +1 -1
  77. package/build/server/design/components/useProviderCatalogState.d.ts +1 -2
  78. package/build/server/design/components/useProviderCatalogState.d.ts.map +1 -1
  79. package/build/server/design/components/useProviderCatalogState.js +71 -152
  80. package/build/server/design/components/useProviderCatalogState.js.map +1 -1
  81. package/build/server/design/components/useProviderModelFieldActions.js +2 -2
  82. package/build/server/design/components/useProviderModelFieldActions.js.map +1 -1
  83. package/build/server/design/components/useProviderRegistryState.d.ts +1 -7
  84. package/build/server/design/components/useProviderRegistryState.d.ts.map +1 -1
  85. package/build/server/design/components/useProviderRegistryState.js +41 -91
  86. package/build/server/design/components/useProviderRegistryState.js.map +1 -1
  87. package/build/server/design/components/useProviderTargetReconciliation.d.ts +1 -0
  88. package/build/server/design/components/useProviderTargetReconciliation.d.ts.map +1 -1
  89. package/build/server/design/components/useProviderTargetReconciliation.js +1 -1
  90. package/build/server/design/components/useProviderTargetReconciliation.js.map +1 -1
  91. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts +2 -1
  92. package/build/server/platform/orchestration/entitySubscriptions/channel.d.ts.map +1 -1
  93. package/build/server/platform/orchestration/entitySubscriptions/channel.js +3 -18
  94. package/build/server/platform/orchestration/entitySubscriptions/channel.js.map +1 -1
  95. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts +1 -0
  96. package/build/server/platform/orchestration/entitySubscriptions/index.d.ts.map +1 -1
  97. package/build/server/platform/orchestration/entitySubscriptions/index.js.map +1 -1
  98. package/build/server/products/chat/api/routeSupport.d.ts +9 -1
  99. package/build/server/products/chat/api/routeSupport.d.ts.map +1 -1
  100. package/build/server/products/chat/api/routeSupport.js +22 -0
  101. package/build/server/products/chat/api/routeSupport.js.map +1 -1
  102. package/build/server/products/chat/state/model/readModels.d.ts +1 -0
  103. package/build/server/products/chat/state/model/readModels.d.ts.map +1 -1
  104. package/build/server/products/chat/state/model/readModels.js +2 -1
  105. package/build/server/products/chat/state/model/readModels.js.map +1 -1
  106. package/build/server/products/shared/renderer/api/appShell.d.ts +1 -0
  107. package/build/server/products/shared/renderer/api/appShell.d.ts.map +1 -1
  108. package/build/server/products/shared/renderer/api/appShell.js +9 -0
  109. package/build/server/products/shared/renderer/api/appShell.js.map +1 -1
  110. package/build/server/runtime/client.d.ts.map +1 -1
  111. package/build/server/runtime/client.js +5 -1
  112. package/build/server/runtime/client.js.map +1 -1
  113. package/build/server/runtime/setup.d.ts +4 -0
  114. package/build/server/runtime/setup.d.ts.map +1 -1
  115. package/build/server/runtime/setup.js +3 -1
  116. package/build/server/runtime/setup.js.map +1 -1
  117. package/build/server/server/routes/providers.d.ts.map +1 -1
  118. package/build/server/server/routes/providers.js +21 -50
  119. package/build/server/server/routes/providers.js.map +1 -1
  120. package/build/server/shared/desktopRecoveryBridge.d.ts +2 -1
  121. package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
  122. package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
  123. package/build/server/shared/providerCatalogData.d.ts +128 -57
  124. package/build/server/shared/providerCatalogData.d.ts.map +1 -1
  125. package/build/server/shared/providerCatalogData.js +51 -26
  126. package/build/server/shared/providerCatalogData.js.map +1 -1
  127. package/build/server/shared/providerRegistryWarnings.d.ts +4 -0
  128. package/build/server/shared/providerRegistryWarnings.d.ts.map +1 -1
  129. package/build/server/shared/providerRegistryWarnings.js +7 -0
  130. package/build/server/shared/providerRegistryWarnings.js.map +1 -1
  131. package/package.json +12 -5
  132. package/packages/provider-setup/manager.d.ts +79 -0
  133. package/packages/provider-setup/manager.js +441 -0
  134. package/scripts/README.md +23 -0
  135. package/scripts/build-desktop-installer.mjs +124 -16
  136. package/scripts/linux/install-github-cli.sh +5 -0
  137. package/scripts/linux/install-node.sh +9 -3
  138. package/scripts/linux/node-cli-common.sh +87 -48
  139. package/scripts/linux/provider-cli-common.sh +80 -105
  140. package/scripts/linux/provider-version-common.sh +247 -0
  141. package/scripts/linux/test-linux-package-smoke.sh +2 -0
  142. package/scripts/linux/verified-npm-update.sh +20 -0
  143. package/scripts/macos/install-github-cli.sh +5 -0
  144. package/scripts/macos/install-node.sh +9 -3
  145. package/scripts/macos/node-cli-common.sh +87 -48
  146. package/scripts/macos/provider-cli-common.sh +80 -105
  147. package/scripts/macos/provider-version-common.sh +247 -0
  148. package/scripts/macos/test-macos-package-smoke.sh +2 -0
  149. package/scripts/macos/verified-npm-update.sh +20 -0
  150. package/scripts/testing/conversation-navigation-smoke-electron.cjs +134 -0
  151. package/scripts/testing/conversation-navigation-smoke-renderer.tsx +102 -0
  152. package/scripts/testing/conversation-navigation-smoke.mjs +67 -0
  153. package/scripts/validate-release-assets.mjs +4 -1
  154. package/scripts/windows/Install-Antigravity.ps1 +26 -3
  155. package/scripts/windows/Install-ClaudeCode.ps1 +25 -7
  156. package/scripts/windows/Install-CursorAgent.ps1 +45 -30
  157. package/scripts/windows/Install-Devin.ps1 +16 -47
  158. package/scripts/windows/Install-GitHubCli.ps1 +4 -0
  159. package/scripts/windows/Install-Goose.ps1 +18 -1
  160. package/scripts/windows/Install-Grok.ps1 +14 -1
  161. package/scripts/windows/Install-Junie.ps1 +32 -11
  162. package/scripts/windows/Install-KiroCli.ps1 +37 -13
  163. package/scripts/windows/Install-MetaCli.ps1 +8 -1
  164. package/scripts/windows/Install-Node.ps1 +6 -1
  165. package/scripts/windows/Setup-NodeGlobalPrefix.ps1 +10 -0
  166. package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +2 -0
  167. package/scripts/windows/_HiddenProcess.ps1 +14 -3
  168. package/scripts/windows/_NativeInstallerSupport.ps1 +308 -0
  169. package/scripts/windows/_NpmCliInstaller.ps1 +35 -14
  170. package/scripts/windows/_PackagedUninstall.ps1 +9 -1
  171. package/scripts/windows/_VerifiedNpmUpdate.ps1 +25 -0
  172. package/build/renderer/assets/ActiveSessionPermissionChip-CAgN-tXv.js +0 -1
  173. package/build/renderer/assets/app-BV3UAQkR.css +0 -1
  174. package/build/renderer/assets/app-CVPqqfZo.js +0 -45
  175. package/build/renderer/assets/conversationSidebarRecentEntries-iqaLqA1n.js +0 -6
  176. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts +0 -48
  177. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.d.ts.map +0 -1
  178. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js +0 -535
  179. package/build/server/app/renderer/settings/runtimeLifecycleHelpers.js.map +0 -1
  180. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts +0 -13
  181. package/build/server/design/components/useProviderRegistryAutoRecheck.d.ts.map +0 -1
  182. package/build/server/design/components/useProviderRegistryAutoRecheck.js +0 -70
  183. package/build/server/design/components/useProviderRegistryAutoRecheck.js.map +0 -1
  184. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts +0 -19
  185. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.d.ts.map +0 -1
  186. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js +0 -53
  187. package/build/server/products/shared/renderer/api/providerCatalogRefreshStore.js.map +0 -1
  188. package/build/server/products/shared/renderer/api/providers.d.ts +0 -17
  189. package/build/server/products/shared/renderer/api/providers.d.ts.map +0 -1
  190. package/build/server/products/shared/renderer/api/providers.js +0 -50
  191. package/build/server/products/shared/renderer/api/providers.js.map +0 -1
@@ -43,6 +43,7 @@ param(
43
43
  [switch]$Apply,
44
44
  [switch]$Upgrade,
45
45
  [switch]$Force,
46
+ [switch]$DryRun,
46
47
  [switch]$Json,
47
48
  [switch]$AllowAdmin,
48
49
  [ValidateSet('auto', 'installed', 'missing')]
@@ -55,6 +56,7 @@ Set-StrictMode -Version Latest
55
56
  $ErrorActionPreference = 'Stop'
56
57
 
57
58
  . (Join-Path $PSScriptRoot '_HiddenProcess.ps1')
59
+ . (Join-Path $PSScriptRoot '_NativeInstallerSupport.ps1')
58
60
 
59
61
  function Write-StructuredResult {
60
62
  param(
@@ -62,6 +64,10 @@ function Write-StructuredResult {
62
64
  [int]$ExitCode
63
65
  )
64
66
 
67
+ if (Get-Variable -Name catsInitialObservation -Scope Script -ErrorAction SilentlyContinue) {
68
+ Add-CatsNativeObservation -Result $Result -Before $script:catsInitialObservation -Attempted ([bool]$script:shouldInstall) -Skipped ([bool]$SkipInstaller)
69
+ }
70
+
65
71
  if ($Json) {
66
72
  $Result | ConvertTo-Json -Depth 10
67
73
  } else {
@@ -86,7 +92,11 @@ function Write-StructuredResult {
86
92
  }
87
93
 
88
94
  function Resolve-KiroExecutablePath {
89
- return Join-Path $env:ProgramFiles 'Kiro-Cli\kiro-cli.exe'
95
+ $perUser = Join-Path $env:LOCALAPPDATA 'Kiro-Cli\kiro-cli.exe'
96
+ $machine = Join-Path $env:ProgramFiles 'Kiro-Cli\kiro-cli.exe'
97
+ if (Test-Path -LiteralPath $perUser -PathType Leaf) { return $perUser }
98
+ if (Test-Path -LiteralPath $machine -PathType Leaf) { return $machine }
99
+ return $perUser
90
100
  }
91
101
 
92
102
  function Refresh-UserPath {
@@ -145,12 +155,8 @@ function Invoke-KiroInstaller {
145
155
  }
146
156
  }
147
157
 
148
- $installScript = Invoke-RestMethod 'https://cli.kiro.dev/install.ps1'
149
- Invoke-Expression $installScript
150
-
151
- return [pscustomobject]@{
152
- skipped = $false
153
- }
158
+ $installScript = Invoke-RestMethod -TimeoutSec 30 -UseBasicParsing 'https://cli.kiro.dev/install.ps1'
159
+ return Invoke-CatsRemoteInstaller -ScriptText $installScript
154
160
  }
155
161
 
156
162
  if (-not $CheckOnly -and -not $Apply -and -not $Upgrade -and -not $Force) {
@@ -205,9 +211,26 @@ if ($CheckOnly) {
205
211
  Write-StructuredResult -Result $result -ExitCode 0
206
212
  }
207
213
 
214
+ if ($DryRun) {
215
+ Write-CatsNativePreview -Helper 'windows-kiro-native-installer' -Mode $executionMode -Detected $detected -Actions $plannedActions.ToArray() -EmitJson ([bool]$Json)
216
+ }
217
+
218
+ $installFailed = $false
219
+ $catsInitialObservation = $detected.PSObject.Copy()
208
220
  $shouldInstall = $Force -or $Upgrade -or -not $detected.installed
221
+ if ($Upgrade -and -not $Force -and $detected.installed -and -not $SkipInstaller -and -not $installFailed) {
222
+ $versionGate = Test-CatsNativeUpgrade -Provider 'kiro' -InstalledVersion $detected.detectedVersion
223
+ $shouldInstall = $versionGate.shouldInstall
224
+ if (-not $versionGate.known) { $warnings.Add('Could not compare the published version; the official installer will verify the update.') }
225
+ if (-not $shouldInstall) { $plannedActions.Clear() }
226
+ }
227
+ $installFailed = $false
209
228
  if ($shouldInstall) {
210
- $installResult = Invoke-KiroInstaller
229
+ $installResult = try { Invoke-KiroInstaller } catch { [pscustomobject]@{ skipped = $false; success = $false; stderr = [string]$_.Exception.Message; usedPowerShell51Fallback = $false; usedWingetFallback = $false } }
230
+ if ($installResult.PSObject.Properties['success'] -and -not $installResult.success) {
231
+ $installFailed = $true
232
+ $warnings.Add($installResult.stderr)
233
+ }
211
234
  if ($installResult.skipped) {
212
235
  $warnings.Add('Installer invocation was skipped by request.')
213
236
  }
@@ -215,8 +238,9 @@ if ($shouldInstall) {
215
238
  Start-Sleep -Seconds 3
216
239
  Refresh-UserPath
217
240
  $detected = Detect-KiroInstall
218
- if (-not $detected.installed -and -not $SkipInstaller) {
219
- throw 'Kiro CLI installation completed but kiro-cli was still not detected.'
241
+ if (-not $detected.installed -and -not $SkipInstaller -and -not $installFailed) {
242
+ $warnings.Add('Kiro CLI installation completed but kiro-cli was still not detected.')
243
+ $installFailed = $true
220
244
  }
221
245
 
222
246
  if ($Force) {
@@ -232,7 +256,7 @@ $interruptions = [System.Collections.Generic.List[object]]::new()
232
256
  if ($shouldInstall) {
233
257
  $interruptions.Add([pscustomobject]@{
234
258
  kind = 'relaunch_required'
235
- summary = 'Relaunch Cats Desktop Host after the Kiro CLI install step, then rerun the packaged setup check.'
259
+ summary = 'Run Detect Again after the Kiro CLI install step if the command is not visible yet.'
236
260
  resumable = $true
237
261
  requiresRestart = $false
238
262
  requiresElevation = $false
@@ -242,7 +266,7 @@ if ($shouldInstall) {
242
266
  $result = [pscustomobject]@{
243
267
  helper = 'windows-kiro-native-installer'
244
268
  mode = $executionMode
245
- status = if ($interruptions.Count -gt 0) { [string]$interruptions[0].kind } else { 'ready' }
269
+ status = if ($installFailed) { 'failed' } elseif ($interruptions.Count -gt 0) { [string]$interruptions[0].kind } else { 'ready' }
246
270
  installed = [bool]$detected.installed
247
271
  detectedVersion = if ($detected.detectedVersion) { $detected.detectedVersion } else { $null }
248
272
  commandPath = $detected.commandPath
@@ -253,4 +277,4 @@ $result = [pscustomobject]@{
253
277
  manualSteps = $manualSteps.ToArray()
254
278
  interruptions = $interruptions.ToArray()
255
279
  }
256
- Write-StructuredResult -Result $result -ExitCode 0
280
+ Write-StructuredResult -Result $result -ExitCode $(if ($installFailed) { 1 } else { 0 })
@@ -42,6 +42,7 @@ Set-StrictMode -Version Latest
42
42
  $ErrorActionPreference = 'Stop'
43
43
 
44
44
  . (Join-Path $PSScriptRoot '_HiddenProcess.ps1')
45
+ . (Join-Path $PSScriptRoot '_NativeInstallerSupport.ps1')
45
46
  . (Join-Path $PSScriptRoot '_PackagedUninstall.ps1')
46
47
 
47
48
  function Write-StructuredResult {
@@ -50,6 +51,10 @@ function Write-StructuredResult {
50
51
  [int]$ExitCode
51
52
  )
52
53
 
54
+ if (Get-Variable -Name catsInitialObservation -Scope Script -ErrorAction SilentlyContinue) {
55
+ Add-CatsNativeObservation -Result $Result -Before $script:catsInitialObservation -Attempted ([bool]$script:shouldInstall) -Skipped ([bool]$SkipInstaller)
56
+ }
57
+
53
58
  if ($Json) {
54
59
  $Result | ConvertTo-Json -Depth 10
55
60
  } else {
@@ -364,6 +369,8 @@ if ($CheckOnly) {
364
369
  }) -ExitCode 0
365
370
  }
366
371
 
372
+ $installFailed = $false
373
+ $catsInitialObservation = $detected.PSObject.Copy()
367
374
  $shouldInstall = $Force -or $Upgrade -or -not $detected.installed
368
375
  $installFailed = $false
369
376
  $installSkipped = $false
@@ -423,7 +430,7 @@ $interruptions = [System.Collections.Generic.List[object]]::new()
423
430
  if ($shouldInstall -and -not $installFailed -and -not $DryRun) {
424
431
  $interruptions.Add([pscustomobject]@{
425
432
  kind = 'relaunch_required'
426
- summary = 'Relaunch Cats Desktop Host after the Meta Muse install step, then rerun the packaged setup check.'
433
+ summary = 'Run Detect Again after the Meta Muse install step if the command is not visible yet.'
427
434
  resumable = $true
428
435
  requiresRestart = $false
429
436
  requiresElevation = $false
@@ -155,7 +155,8 @@ function Test-NodeInstalled {
155
155
  return $false
156
156
  }
157
157
 
158
- return [bool](Get-Command node -ErrorAction SilentlyContinue)
158
+ return [bool](Get-Command node -ErrorAction SilentlyContinue) -and
159
+ [bool](Get-Command npm -ErrorAction SilentlyContinue)
159
160
  }
160
161
 
161
162
  if (-not $CheckOnly -and -not $Apply -and -not $Upgrade -and -not $Force) {
@@ -180,6 +181,10 @@ $mode = if ($CheckOnly) {
180
181
  'apply'
181
182
  }
182
183
 
184
+ # Each invocation inherits the long-lived Desktop host's environment. Include
185
+ # newly installed system/user commands before check-only detection as well.
186
+ $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
187
+ [System.Environment]::GetEnvironmentVariable('Path', 'User') + ';' + $env:Path
183
188
  $installed = Test-NodeInstalled -Mode $mode
184
189
  $detected = Get-NodeDetectedVersion -Override $DetectedVersion
185
190
  $commandPath = if ($SkipNodeProbe) { '' } else { Resolve-NodeCommandPath }
@@ -215,6 +215,16 @@ if (-not $SkipNodeCheck) {
215
215
  }
216
216
  }
217
217
 
218
+ if (-not $PSBoundParameters.ContainsKey('DesiredPrefix') -and $prefixProbe) {
219
+ $existingPrefix = [IO.Path]::GetFullPath($prefixProbe)
220
+ $userRoot = [IO.Path]::GetFullPath($UserHome).TrimEnd('\') + '\'
221
+ $npmrc = if ($env:NPM_CONFIG_USERCONFIG) { $env:NPM_CONFIG_USERCONFIG } else { Join-Path $UserHome '.npmrc' }
222
+ $explicitPrefix = $env:NPM_CONFIG_PREFIX -or ((Test-Path -LiteralPath $npmrc -PathType Leaf) -and
223
+ (Select-String -LiteralPath $npmrc -Pattern '^\s*prefix\s*=' -Quiet))
224
+ if ($explicitPrefix -or $existingPrefix.StartsWith($userRoot, [StringComparison]::OrdinalIgnoreCase)) {
225
+ $desiredPrefixPath = $existingPrefix
226
+ }
227
+ }
218
228
  $pathEntries = Get-PathEntries -PathValue $pathProbe
219
229
  $needsDirectory = -not (Test-Path -LiteralPath $desiredPrefixPath -PathType Container)
220
230
  $needsPrefixUpdate = (Normalize-PathValue -Path $prefixProbe).ToLowerInvariant() -ne
@@ -190,6 +190,8 @@ $requiredFiles = @(
190
190
  @{ Path = (Join-Path $resourcesRoot 'cats-runtime\config\providers.yaml.example'); Label = 'bundled cats-runtime provider config example' },
191
191
  @{ Path = (Join-Path $resourcesRoot 'cats-runtime\config\curated-model-catalogs.yaml.example'); Label = 'bundled cats-runtime curated model catalog example' },
192
192
  @{ Path = (Join-Path $resourcesRoot 'cats-runtime\node_modules\playwright-core\package.json'); Label = 'bundled cats-runtime external dependency marker' },
193
+ @{ Path = (Join-Path $resourcesRoot 'desktop\setup-assets\windows\_NativeInstallerSupport.ps1'); Label = 'bundled native version/installer support' },
194
+ @{ Path = (Join-Path $resourcesRoot 'desktop\setup-assets\windows\_VerifiedNpmUpdate.ps1'); Label = 'bundled verified npm update support' },
193
195
  @{ Path = (Join-Path $resourcesRoot 'desktop\setup-assets\windows\Install-Node.ps1'); Label = 'bundled Windows Node.js LTS host installer' },
194
196
  @{ Path = (Join-Path $resourcesRoot 'desktop\setup-assets\windows\Install-GitHubCli.ps1'); Label = 'bundled Windows GitHub CLI host installer' },
195
197
  @{ Path = (Join-Path $resourcesRoot 'desktop\setup-assets\windows\Setup-NodeGlobalPrefix.ps1'); Label = 'bundled Windows npm prefix helper' },
@@ -15,7 +15,8 @@ function Invoke-HiddenCommand {
15
15
  [Parameter(Mandatory)]
16
16
  [string]$FileName,
17
17
 
18
- [string[]]$ArgumentList = @()
18
+ [string[]]$ArgumentList = @(),
19
+ [int]$TimeoutMs = 0
19
20
  )
20
21
 
21
22
  $psi = New-Object System.Diagnostics.ProcessStartInfo
@@ -36,9 +37,19 @@ function Invoke-HiddenCommand {
36
37
  }
37
38
 
38
39
  $proc = [System.Diagnostics.Process]::Start($psi)
39
- $output = $proc.StandardOutput.ReadToEnd()
40
- $errorOutput = $proc.StandardError.ReadToEnd()
40
+ $outputTask = $proc.StandardOutput.ReadToEndAsync()
41
+ $errorTask = $proc.StandardError.ReadToEndAsync()
42
+ if ($TimeoutMs -gt 0 -and -not $proc.WaitForExit($TimeoutMs)) {
43
+ # taskkill /T works in Windows PowerShell 5.1 as well as PowerShell 7;
44
+ # Process.Kill(true) is unavailable in the former. Only this owned tree is killed.
45
+ $null = Invoke-HiddenCommand -FileName (Join-Path $env:SystemRoot 'System32\taskkill.exe') -ArgumentList @('/PID', [string]$proc.Id, '/T', '/F')
46
+ if (-not $proc.HasExited) { $proc.Kill() }
47
+ $proc.WaitForExit()
48
+ return [pscustomobject]@{ ExitCode = -1; Output = ''; ErrorOutput = 'Command timed out.' }
49
+ }
41
50
  $proc.WaitForExit()
51
+ $output = $outputTask.GetAwaiter().GetResult()
52
+ $errorOutput = $errorTask.GetAwaiter().GetResult()
42
53
 
43
54
  return [pscustomobject]@{
44
55
  ExitCode = $proc.ExitCode
@@ -0,0 +1,308 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Shared native installer version gates, isolated child execution and observed results.
4
+ .DESCRIPTION
5
+ Dot-source from the packaged native helpers. Metadata parsing is adapted from
6
+ environment-bootstrap at 752dc13; no query occurs during check-only actions.
7
+ Vendor scripts execute in a hidden child so an upstream exit cannot end our JSON response.
8
+ .EXAMPLE
9
+ . "$PSScriptRoot\_NativeInstallerSupport.ps1"
10
+ Test-CatsNativeUpgrade -Provider grok -InstalledVersion '1.0.0'
11
+ #>
12
+ . (Join-Path $PSScriptRoot '_HiddenProcess.ps1')
13
+
14
+ function Get-CatsNativeLatestVersion {
15
+ param([string]$Provider)
16
+ try {
17
+ switch ($Provider) {
18
+ antigravity { return Get-AntigravityLatestVersion }
19
+ cursor { return Get-CursorAgentLatestVersion -InstallerText (Invoke-RestMethod 'https://cursor.com/install?win32=true' -TimeoutSec 20 -UseBasicParsing) }
20
+ junie { return Get-JunieLatestBuild }
21
+ devin { return Get-DevinLatestVersion }
22
+ grok { return Get-GrokLatestVersion }
23
+ kiro { return [string](Invoke-RestMethod 'https://prod.download.cli.kiro.dev/stable/latest/manifest.json' -TimeoutSec 20 -UseBasicParsing).version }
24
+ goose { return [string](Invoke-RestMethod 'https://api.github.com/repos/aaif-goose/goose/releases/latest' -TimeoutSec 20 -UseBasicParsing -Headers @{ 'User-Agent' = 'Cats-Desktop'; Accept = 'application/vnd.github+json' }).tag_name }
25
+ default { return $null }
26
+ }
27
+ } catch { return $null }
28
+ }
29
+
30
+ function Get-CatsNativeVersionToken {
31
+ param([string]$Provider, [string]$Version)
32
+ if ($Provider -eq 'junie') {
33
+ $match = [regex]::Match($Version, '\((\d+(?:\.\d+)+)\)')
34
+ if ($match.Success) { return $match.Groups[1].Value }
35
+ return ''
36
+ }
37
+ return [regex]::Match($Version, '\d+(?:\.\d+)+(?:-[a-zA-Z0-9.]+)?').Value
38
+ }
39
+
40
+ function Test-CatsNativeUpgrade {
41
+ param([string]$Provider, [string]$InstalledVersion, [AllowNull()][string]$LatestVersion = $null)
42
+ if (-not $PSBoundParameters.ContainsKey('LatestVersion')) { $LatestVersion = Get-CatsNativeLatestVersion $Provider }
43
+ $current = Get-CatsNativeVersionToken $Provider $InstalledVersion
44
+ $comparison = Compare-ToolVersion -Installed $current -Latest $LatestVersion
45
+ $known = $null -ne $comparison
46
+ $currentRelease = $known -and ($comparison -ge 0)
47
+ if ($Provider -eq 'cursor' -and $comparison -eq 0 -and $current -ne $LatestVersion) { $currentRelease = $false }
48
+ return [pscustomobject]@{ shouldInstall = -not $currentRelease; latestVersion = $LatestVersion; known = $known }
49
+ }
50
+
51
+ function Invoke-CatsRemoteInstaller {
52
+ param([Parameter(Mandatory)][string]$ScriptText, [switch]$RetrySharingViolation)
53
+ $temporary = Join-Path ([System.IO.Path]::GetTempPath()) ('cats-native-' + [guid]::NewGuid().ToString('N') + '.ps1')
54
+ try {
55
+ if ([string]::IsNullOrWhiteSpace($ScriptText)) { throw 'Vendor returned an empty installer.' }
56
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [ref]$null, [ref]$null)
57
+ $offsets = @(0)
58
+ if ($ast.ParamBlock) { $offsets += $ast.ParamBlock.Extent.EndOffset }
59
+ foreach ($usingStatement in $ast.UsingStatements) { $offsets += $usingStatement.Extent.EndOffset }
60
+ $offset = ($offsets | Measure-Object -Maximum).Maximum
61
+ $prologue = '$env:PSModulePath = (@([System.Environment]::GetEnvironmentVariable(''PSModulePath'',''User''), [System.Environment]::GetEnvironmentVariable(''PSModulePath'',''Machine''), (Join-Path $PSHOME ''Modules'')) | Where-Object { $_ }) -join '';'''
62
+ $body = $ScriptText.Insert($offset, "`n$prologue`n")
63
+ if ($RetrySharingViolation) {
64
+ $encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($body))
65
+ $body = @'
66
+ $ErrorActionPreference = 'Stop'
67
+ $installer = [scriptblock]::Create([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('__SOURCE__')))
68
+ for ($attempt = 1; $attempt -le 3; $attempt++) {
69
+ try { & $installer; exit 0 }
70
+ catch {
71
+ $exception = $_.Exception
72
+ while ($exception.InnerException) { $exception = $exception.InnerException }
73
+ if ($attempt -eq 3 -or -not ($exception -is [IO.IOException]) -or ($exception.HResult -band 0xffff) -ne 32) { throw }
74
+ Start-Sleep -Seconds 5
75
+ }
76
+ }
77
+ '@
78
+ $body = $body.Replace('__SOURCE__', $encoded)
79
+ }
80
+ [IO.File]::WriteAllText($temporary, $body, [Text.UTF8Encoding]::new($true))
81
+ $hostExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
82
+ $result = Invoke-HiddenCommand -FileName $hostExe -ArgumentList @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $temporary)
83
+ return [pscustomobject]@{ skipped = $false; success = $result.ExitCode -eq 0; exitCode = $result.ExitCode; stderr = $result.ErrorOutput; stdout = $result.Output }
84
+ } catch {
85
+ return [pscustomobject]@{ skipped = $false; success = $false; exitCode = -1; stderr = [string]$_.Exception.Message }
86
+ } finally {
87
+ # This is one uniquely-created file; never recursively remove a computed directory.
88
+ Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
89
+ }
90
+ }
91
+
92
+ function Add-CatsNativeObservation {
93
+ param([pscustomobject]$Result, [AllowNull()]$Before, [bool]$Attempted, [bool]$Skipped)
94
+ if ($Result.mode -in @('check', 'uninstall') -or $null -eq $Before) { return }
95
+ $change = if ($Result.status -eq 'failed') { 'failed' }
96
+ elseif ($Skipped -or $Result.status -eq 'preview' -or -not $Attempted) { 'unchanged' }
97
+ elseif (-not $Result.installed) { 'failed' }
98
+ elseif (-not $Before.installed) { 'installed' }
99
+ elseif ($Before.detectedVersion -and $Result.detectedVersion -and $Before.detectedVersion -ne $Result.detectedVersion) {
100
+ $comparison = Compare-ToolVersion -Installed $Result.detectedVersion -Latest $Before.detectedVersion
101
+ if ($comparison -gt 0) { 'upgraded' } elseif ($comparison -lt 0) { 'downgraded' } else { 'version_changed' }
102
+ }
103
+ else { 'unchanged' }
104
+ if ($Result.mode -in @('upgrade', 'force') -and -not $Skipped -and $Result.status -ne 'preview' -and $change -ne 'failed' -and $Result.installed -and $Result.detectedVersion) {
105
+ $provider = $Result.helper -replace '^windows-', '' -replace '-native-installer$', ''
106
+ if ($provider -in @('cursor', 'devin', 'junie')) {
107
+ try { Remove-CatsStaleNativeVersions -Provider $provider -Version $Result.detectedVersion } catch { Write-Warning 'Old-version cleanup was skipped.' }
108
+ }
109
+ }
110
+ $Result | Add-Member -NotePropertyName observedChange -NotePropertyValue $change -Force
111
+ $Result | Add-Member -NotePropertyName previousVersion -NotePropertyValue $Before.detectedVersion -Force
112
+ $description = "$change"
113
+ if ($change -in @('upgraded', 'downgraded', 'version_changed')) { $description += ": $($Before.detectedVersion) -> $($Result.detectedVersion)" }
114
+ elseif ($Result.detectedVersion) { $description += ": $($Result.detectedVersion)" }
115
+ $Result | Add-Member -NotePropertyName summary -NotePropertyValue $description -Force
116
+ if ($change -in @('failed', 'unchanged')) { $Result.appliedChanges = @() }
117
+ else { $Result.appliedChanges = @("${change}_native_installation") }
118
+ }
119
+
120
+ function Write-CatsNativePreview {
121
+ param([string]$Helper, [string]$Mode, $Detected, [string[]]$Actions, [bool]$EmitJson)
122
+ $result = [pscustomobject]@{
123
+ helper = $Helper; mode = $Mode; status = 'preview'; installed = [bool]$Detected.installed
124
+ detectedVersion = $Detected.detectedVersion; commandPath = $Detected.commandPath
125
+ restartRequired = $false; plannedActions = @($Actions); appliedChanges = @()
126
+ warnings = @(); manualSteps = @(); interruptions = @(); observedChange = 'unchanged'
127
+ }
128
+ if ($EmitJson) { $result | ConvertTo-Json -Depth 10 } else { Write-Host "Preview: $($Actions -join ', ')" }
129
+ exit 0
130
+ }
131
+
132
+ function Get-CatsShimVersionText {
133
+ param([string]$Path)
134
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
135
+ $literal = "'" + $Path.Replace("'", "''") + "'"
136
+ $script = "& $literal --version; exit " + '$LASTEXITCODE'
137
+ $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($script))
138
+ $hostExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
139
+ $result = Invoke-HiddenCommand -FileName $hostExe -ArgumentList @('-NoProfile', '-NonInteractive', '-EncodedCommand', $encoded) -TimeoutMs 15000
140
+ if ($result.ExitCode -ne 0) { return '' }
141
+ return $result.Output.Trim()
142
+ }
143
+
144
+ function Remove-CatsStaleNativeVersions {
145
+ param([string]$Provider, [string]$Version)
146
+ $keep = Get-CatsNativeVersionToken $Provider $Version
147
+ if (-not $keep) { return }
148
+ $data = switch ($Provider) {
149
+ cursor { Join-Path $env:LOCALAPPDATA 'cursor-agent' }
150
+ devin { Join-Path $env:LOCALAPPDATA 'devin\cli' }
151
+ junie {
152
+ if ($env:JUNIE_DATA_DIR) { $env:JUNIE_DATA_DIR } else { Join-Path $env:USERPROFILE '.local\share\junie' }
153
+ }
154
+ default { return }
155
+ }
156
+ $versions = [IO.Path]::GetFullPath((Join-Path $data $(if ($Provider -eq 'devin') { '_versions' } else { 'versions' })))
157
+ if ($Provider -eq 'junie') {
158
+ if (Test-Path -LiteralPath (Join-Path $data 'updates\pending-update.json')) { return }
159
+ $currentFile = Join-Path $data 'current'
160
+ if (-not (Test-Path -LiteralPath $currentFile -PathType Leaf)) { return }
161
+ if ((Get-Content -LiteralPath $currentFile -Raw).Trim() -ne $keep) { return }
162
+ }
163
+ # Only known user roots; do not follow a junction in any ancestor or delete
164
+ # current/newer/pending builds. A custom directory outside those roots is kept.
165
+ if (-not (Test-PackagedProviderPathRemovable $versions)) { return }
166
+ $ancestor = Get-Item -LiteralPath $versions
167
+ while ($ancestor) {
168
+ if ($ancestor.Attributes -band [IO.FileAttributes]::ReparsePoint) { return }
169
+ $ancestor = $ancestor.Parent
170
+ }
171
+ foreach ($directory in (Get-ChildItem -LiteralPath $versions -Directory)) {
172
+ if ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
173
+ if ($directory.Name -notmatch '^\d+(\.\d+)+(-[a-zA-Z0-9]+)?$') { continue }
174
+ if ((Compare-ToolVersion -Installed $directory.Name -Latest $keep) -ne -1) { continue }
175
+ $resolved = [IO.Path]::GetFullPath($directory.FullName)
176
+ if (-not $resolved.StartsWith($versions.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { continue }
177
+ if (Get-ChildItem -LiteralPath $resolved -Recurse -Force | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } | Select-Object -First 1) { continue }
178
+ try { Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction Stop } catch { Write-Warning "Could not remove old $Provider version $($directory.Name)." }
179
+ }
180
+ }
181
+ function ConvertTo-ComparableVersion {
182
+ param([string]$Version)
183
+
184
+ if ([string]::IsNullOrWhiteSpace($Version)) { return $null }
185
+
186
+ # 第一個版本樣式的 token;額外吃下 Git for Windows 的 .windows.N 尾段
187
+ $match = [regex]::Match($Version, '\d+(?:\.\d+)+(?:\.windows\.\d+)?')
188
+ if (-not $match.Success) { return $null }
189
+
190
+ $parts = ($match.Value -replace '\.windows\.', '.') -split '\.' |
191
+ Select-Object -First 4
192
+
193
+ while ($parts.Count -lt 4) { $parts += '0' }
194
+
195
+ try {
196
+ return [version]($parts -join '.')
197
+ } catch {
198
+ return $null
199
+ }
200
+ }
201
+
202
+ function Compare-ToolVersion {
203
+ param(
204
+ [string]$Installed,
205
+ [string]$Latest
206
+ )
207
+
208
+ $installedVersion = ConvertTo-ComparableVersion $Installed
209
+ $latestVersion = ConvertTo-ComparableVersion $Latest
210
+
211
+ if (-not $installedVersion -or -not $latestVersion) { return $null }
212
+
213
+ return $installedVersion.CompareTo($latestVersion)
214
+ }
215
+
216
+ function Get-AntigravityLatestVersion {
217
+ param([string]$InstallerText)
218
+
219
+ $baseUrl = 'https://antigravity-cli-auto-updater-974169037036.us-central1.run.app'
220
+ if ($InstallerText) {
221
+ $m = [regex]::Match($InstallerText, '\$DOWNLOAD_BASE_URL\s*=\s*"(https://[^"]+)"')
222
+ if ($m.Success) { $baseUrl = $m.Groups[1].Value }
223
+ }
224
+
225
+ $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
226
+ $platform = if ($arch -eq 'ARM64') { 'windows_arm64' } else { 'windows_amd64' }
227
+
228
+ try {
229
+ $manifest = Invoke-RestMethod -Uri "$baseUrl/manifests/$platform.json" -TimeoutSec 20 -UseBasicParsing -ErrorAction Stop
230
+ if (-not $manifest.version) { return $null }
231
+ return "$($manifest.version)"
232
+ } catch {
233
+ return $null
234
+ }
235
+ }
236
+
237
+ function Get-CursorAgentLatestVersion {
238
+ param(
239
+ [Parameter(Mandatory=$true)]
240
+ [string]$InstallerText
241
+ )
242
+
243
+ $m = [regex]::Match($InstallerText, "(?m)^\s*\`$version\s*=\s*'([^']+)'")
244
+ if (-not $m.Success) { return $null }
245
+ return $m.Groups[1].Value
246
+ }
247
+
248
+ function Get-JunieLatestBuild {
249
+ $archName = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'aarch64' } else { 'amd64' }
250
+ $platform = "windows-$archName"
251
+
252
+ try {
253
+ $jsonl = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/jetbrains-junie/junie/main/update-info.jsonl' `
254
+ -TimeoutSec 20 -UseBasicParsing -ErrorAction Stop
255
+ } catch {
256
+ return $null
257
+ }
258
+
259
+ $best = $null
260
+ foreach ($line in ("$jsonl" -split "`n")) {
261
+ if ($line -notmatch "`"platform`":`"$platform`"") { continue }
262
+ try { $entry = $line | ConvertFrom-Json } catch { continue }
263
+ if (-not $entry.version) { continue }
264
+ $candidate = ConvertTo-ComparableVersion "$($entry.version)"
265
+ if (-not $candidate) { continue }
266
+ if ($null -eq $best -or $candidate -gt $best.Comparable) {
267
+ $best = @{ Build = "$($entry.version)"; Comparable = $candidate }
268
+ }
269
+ }
270
+
271
+ if ($null -eq $best) { return $null }
272
+ return $best.Build
273
+ }
274
+
275
+ function Get-DevinLatestVersion {
276
+ try {
277
+ $manifest = Invoke-RestMethod -Uri 'https://static.devin.ai/cli/current/manifest.json' -TimeoutSec 20 -UseBasicParsing -ErrorAction Stop
278
+ if (-not $manifest.version) { return $null }
279
+ return "$($manifest.version)"
280
+ } catch {
281
+ return $null
282
+ }
283
+ }
284
+
285
+ function Get-GrokLatestVersion {
286
+ param([string]$Channel)
287
+
288
+ if (-not $Channel) {
289
+ $Channel = if ($env:GROK_CHANNEL) { $env:GROK_CHANNEL } else { 'stable' }
290
+ }
291
+
292
+ $urls = @(
293
+ "https://x.ai/cli/$Channel",
294
+ "https://storage.googleapis.com/grok-build-public-artifacts/cli/$Channel"
295
+ )
296
+ foreach ($url in $urls) {
297
+ try {
298
+ $text = Invoke-RestMethod -Uri $url -TimeoutSec 20 -UseBasicParsing -ErrorAction Stop
299
+ } catch {
300
+ continue
301
+ }
302
+ $line = @("$text" -split "`r?`n")[0]
303
+ if ($null -eq $line) { continue }
304
+ $line = $line.Trim()
305
+ if ($line -match '^\d+\.\d+\.\d+(-[A-Za-z0-9._]+)?$') { return $line }
306
+ }
307
+ return $null
308
+ }
@@ -23,6 +23,8 @@
23
23
  can run it explicitly when needed.
24
24
  #>
25
25
 
26
+ . (Join-Path $PSScriptRoot '_VerifiedNpmUpdate.ps1')
27
+
26
28
  function Test-NpmCliInstallerAdminGuard {
27
29
  param(
28
30
  [Parameter(Mandatory = $true)]
@@ -163,11 +165,16 @@ function Test-NpmCliPackageOutdated {
163
165
  [string]$PackageName
164
166
  )
165
167
 
166
- $outdatedRaw = (& npm outdated -g --json $PackageName 2>$null) | Out-String
167
- if ([string]::IsNullOrWhiteSpace($outdatedRaw)) {
168
- return $false
169
- }
170
- return $outdatedRaw.Contains("`"$PackageName`"")
168
+ $latest = ((& npm view "$PackageName@latest" version --fetch-retries=0 --fetch-timeout=10000 --loglevel=error) | Out-String).Trim()
169
+ if ($LASTEXITCODE -ne 0 -or $latest -notmatch '^\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?$') { throw 'Provider version query failed; update status is unknown.' }
170
+ $installed = Get-NpmCliPackageVersion -PackageName $PackageName
171
+ if (-not $installed) { return $true }
172
+ $currentBase = [version]($installed -split '-')[0]
173
+ $latestBase = [version]($latest -split '-')[0]
174
+ # Preserve a newer local/pre-release build. Equal numeric prerelease advances
175
+ # to the stable release; Repair remains the explicit reinstall action.
176
+ return $currentBase -lt $latestBase -or ($currentBase -eq $latestBase -and $installed.Contains('-') -and -not $latest.Contains('-'))
177
+
171
178
  }
172
179
 
173
180
  function Resolve-NpmCliCommandPath {
@@ -216,6 +223,7 @@ function Remove-SupersededNpmPackages {
216
223
 
217
224
  Write-Host "Removing superseded package $legacy (replaced by $PackageName)."
218
225
  & npm uninstall -g $legacy 2>$null | Out-Null
226
+ if ($LASTEXITCODE -ne 0) { throw "Could not remove superseded package $legacy." }
219
227
  if ($AppliedChanges) {
220
228
  $AppliedChanges.Add("${legacy}:removed-superseded") | Out-Null
221
229
  }
@@ -315,6 +323,12 @@ function Invoke-PackagedNpmCliInstall {
315
323
  $null
316
324
  }
317
325
  $commandPath = if ($installed) { Resolve-NpmCliCommandPath -CommandName $CommandName } else { $null }
326
+ if ($installed -and -not $detectedVersion -and -not $SkipNpmInvocation) {
327
+ foreach ($legacy in (Get-SupersededNpmPackages -PackageName $PackageName)) {
328
+ & npm list -g --depth=0 $legacy 2>$null | Out-Null
329
+ if ($LASTEXITCODE -eq 0) { $installed = $false; break }
330
+ }
331
+ }
318
332
 
319
333
  $plannedActions = [System.Collections.Generic.List[string]]::new()
320
334
  $appliedChanges = [System.Collections.Generic.List[string]]::new()
@@ -444,6 +458,17 @@ function Invoke-PackagedNpmCliInstall {
444
458
  exit 0
445
459
  }
446
460
 
461
+ if ($DryRun -and -not $CheckOnly) {
462
+ $preview = [pscustomobject]@{
463
+ helper = $HelperId; mode = $mode; status = 'preview'; installed = [bool]$installed
464
+ detectedVersion = $detectedVersion; commandPath = $commandPath; restartRequired = $false
465
+ plannedActions = @("${PackageName}:${mode}"); appliedChanges = @(); warnings = @(); manualSteps = @(); interruptions = @()
466
+ }
467
+ if ($Json) { $preview | ConvertTo-Json -Depth 10 } else { Write-Host "Preview: ${PackageName}:${mode}" }
468
+ exit 0
469
+ }
470
+ if ($Upgrade -and -not $CheckOnly -and -not $SkipNpmInvocation) { Update-CatsNpm }
471
+
447
472
  $isOutdated = if (-not $Upgrade -and -not $Force) {
448
473
  $false
449
474
  } else {
@@ -565,15 +590,9 @@ function Invoke-PackagedNpmCliInstall {
565
590
  # Without it, packages like @openai/codex skip their platform-specific
566
591
  # binaries (codex-win32-x64, codex-darwin-arm64, ...), leaving a shim that
567
592
  # silently exits when invoked. Pure-JS CLIs such as copilot don't notice.
568
- $arguments = @('install', '-g', '--include=optional')
569
- if ($plannedAction -eq 'upgrade') {
570
- $arguments += "$PackageName@latest"
571
- } else {
572
- $arguments += $PackageName
573
- }
574
- if ($plannedAction -eq 'reinstall') {
575
- $arguments += '--force'
576
- }
593
+ $expectedVersion = ((& npm view "$PackageName@latest" version --fetch-retries=0 --fetch-timeout=10000 --loglevel=error) | Out-String).Trim()
594
+ if ($LASTEXITCODE -ne 0 -or $expectedVersion -notmatch '^\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?$') { throw 'Could not resolve the provider version before installing.' }
595
+ $arguments = @('install', '-g', '--include=optional', '--engine-strict', '--fetch-retries=0', '--fetch-timeout=30000', "$PackageName@$expectedVersion")
577
596
 
578
597
  & npm @arguments | Out-Null
579
598
  if ($LASTEXITCODE -ne 0) {
@@ -585,6 +604,8 @@ function Invoke-PackagedNpmCliInstall {
585
604
  $finalCommandPath = if ($finalInstalled) { Resolve-NpmCliCommandPath -CommandName $CommandName } else { $null }
586
605
  $finalVersion = if ($finalInstalled) { Get-NpmCliPackageVersion -PackageName $PackageName } else { $null }
587
606
 
607
+ if (-not $finalInstalled -or -not $finalCommandPath -or $finalVersion -ne $expectedVersion) { throw 'Provider installation verification failed. Check npm prefix and command PATH.' }
608
+
588
609
  $result = [pscustomobject]@{
589
610
  helper = $HelperId
590
611
  mode = $mode
@@ -24,7 +24,15 @@ if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.Utility)) {
24
24
  function Test-PackagedProviderPathRemovable {
25
25
  param([string]$Path)
26
26
  if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
27
- return (Test-Path -LiteralPath $Path)
27
+ $fullPath = [IO.Path]::GetFullPath($Path).TrimEnd('\', '/')
28
+ $withinUserRoot = $false
29
+ foreach ($base in @($env:USERPROFILE, $env:LOCALAPPDATA)) {
30
+ if (-not $base) { continue }
31
+ $root = [IO.Path]::GetFullPath($base).TrimEnd('\', '/') + '\'
32
+ if ($fullPath.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { $withinUserRoot = $true }
33
+ }
34
+ if (-not $withinUserRoot) { return $false }
35
+ return (Test-Path -LiteralPath $fullPath)
28
36
  }
29
37
 
30
38
  function Test-DetectedPathInPlannedRemovals {