@@ -8,6 +8,21 @@ import type { ILogger } from "../logging";
88import type { IPowerShellVersionDetails } from "../session" ;
99import { changeSetting } from "../settings" ;
1010
11+ /** The WinGet package identifier for PowerShell. */
12+ const WINGET_PACKAGE = "Microsoft.PowerShell" ;
13+
14+ /** The WinGet command that upgrades PowerShell. */
15+ const WINGET_UPGRADE = `winget update --id ${ WINGET_PACKAGE } -e -s winget` ;
16+
17+ /** The GitHub repository that hosts the WinGet package manifests. */
18+ const WINGET_PKGS_REPO = "microsoft/winget-pkgs" ;
19+
20+ /**
21+ * The first PowerShell Core release. Anything older is Windows PowerShell,
22+ * which is neither checked nor prompted for updates.
23+ */
24+ export const FIRST_CORE_RELEASE = "6.0.0" ;
25+
1126async function fetchJSON < T > ( url : string ) : Promise < T | undefined > {
1227 const response = await fetch ( url ) ;
1328 if ( ! response . ok ) return undefined ;
@@ -41,6 +56,15 @@ export function getLatestWinGetVersion(
4156 . sort ( ( a , b ) => new SemVer ( b ) . compare ( a ) ) [ 0 ] ;
4257}
4358
59+ /**
60+ * Whether the error means there is no `winget` executable to run. Every other
61+ * failure (a source lookup, a network problem, and so on) leaves WinGet itself
62+ * installed, it just did not answer.
63+ */
64+ export function isWinGetMissing ( error : unknown ) : boolean {
65+ return ( error as { code ?: unknown } | undefined ) ?. code === "ENOENT" ;
66+ }
67+
4468/** What WinGet has for PowerShell on this machine, if anything. */
4569export interface IWinGetStatus {
4670 /** Whether the `winget` CLI was found. */
@@ -72,18 +96,29 @@ export function buildUpdatePrompt(
7296 winget : IWinGetStatus ,
7397) : IUpdatePrompt {
7498 const releaseVersion = new SemVer ( releaseTag ) ;
99+ // WinGet can report a version while missing (it is what it would install),
100+ // so only use a version that comes from an installed WinGet.
101+ const installedVersion =
102+ winget . installed && winget . version
103+ ? new SemVer ( winget . version )
104+ : undefined ;
75105 const options : IUpdatePromptOption [ ] = [ ] ;
76106
77107 // WinGet only exists on Windows, so don't offer it elsewhere.
78108 if ( isWindows ) {
79- options . push ( {
80- id : "winget" ,
81- title : winget . installed ? "Upgrade with WinGet" : "Install WinGet" ,
82- } ) ;
109+ if ( ! winget . installed ) {
110+ options . push ( { id : "winget" , title : "Install WinGet" } ) ;
111+ } else if (
112+ installedVersion === undefined ||
113+ installedVersion . compare ( localVersion ) > 0
114+ ) {
115+ // Only offer the upgrade when WinGet actually has something newer,
116+ // otherwise `winget update` is a guaranteed no-op.
117+ options . push ( { id : "winget" , title : "Upgrade with WinGet" } ) ;
118+ }
83119 if (
84- winget . installed &&
85- winget . version &&
86- new SemVer ( winget . version ) . compare ( releaseVersion . version ) < 0
120+ installedVersion &&
121+ installedVersion . compare ( releaseVersion . version ) < 0
87122 ) {
88123 options . push ( {
89124 id : "winget-progress" ,
@@ -101,11 +136,10 @@ export function buildUpdatePrompt(
101136 `PowerShell v${ localVersion } is out-of-date.\n` +
102137 `The latest version is v${ releaseVersion . version } .` ;
103138 // Note when WinGet is relevant but not (yet) useful.
104- if ( winget . installed && winget . version ) {
105- const wingetVersion = new SemVer ( winget . version ) ;
106- if ( wingetVersion . compare ( localVersion ) <= 0 ) {
139+ if ( installedVersion ) {
140+ if ( installedVersion . compare ( localVersion ) <= 0 ) {
107141 message += `\n(WinGet hasn't caught up yet — currently v${ winget . version } .)` ;
108- } else if ( wingetVersion . compare ( releaseVersion . version ) < 0 ) {
142+ } else if ( installedVersion . compare ( releaseVersion . version ) < 0 ) {
109143 message += `\n(WinGet currently has v${ winget . version } .)` ;
110144 }
111145 } else if ( isWindows ) {
@@ -118,6 +152,20 @@ export function buildUpdatePrompt(
118152 return { message, options } ;
119153}
120154
155+ /** Open a terminal, show it, and run the given command. */
156+ function runInTerminal (
157+ name : string ,
158+ command : string ,
159+ shellPath ?: string ,
160+ ) : void {
161+ // Pin the shell only when asked, else use the user's default terminal.
162+ const terminal = shellPath
163+ ? vscode . window . createTerminal ( { name, shellPath } )
164+ : vscode . window . createTerminal ( name ) ;
165+ terminal . show ( ) ;
166+ terminal . sendText ( command ) ;
167+ }
168+
121169/** Await a value/promise, and if non-nullish, pass it to `fn`. */
122170async function whenSome < T > (
123171 value : T | undefined | null | Promise < T | undefined | null > ,
@@ -127,6 +175,25 @@ async function whenSome<T>(
127175 if ( resolved != null ) await fn ( resolved ) ;
128176}
129177
178+ /** The highest-versioned WinGet package match seen so far, with its link. */
179+ interface IBestMatch {
180+ version : SemVer ;
181+ url : string ;
182+ }
183+
184+ /** The given match, or `best` when it already refers to a higher version. */
185+ function keepHighestMatch (
186+ best : IBestMatch | undefined ,
187+ version : string ,
188+ url : string ,
189+ ) : IBestMatch {
190+ const candidate = new SemVer ( version ) ;
191+ if ( best === undefined || candidate . compare ( best . version ) > 0 ) {
192+ return { version : candidate , url } ;
193+ }
194+ return best ;
195+ }
196+
130197// This attempts to mirror PowerShell's `UpdatesNotification.cs` logic as much as
131198// possibly, documented at:
132199// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_update_notifications
@@ -164,7 +231,7 @@ export class UpdatePowerShell {
164231 ) ;
165232
166233 // Skip prompting when using Windows PowerShell for now.
167- if ( this . localVersion . compare ( "6.0.0" ) === - 1 )
234+ if ( this . localVersion . compare ( FIRST_CORE_RELEASE ) === - 1 )
168235 // TODO: Maybe we should announce PowerShell Core?
169236 return this . skip ( "Not prompting to update Windows PowerShell." ) ;
170237
@@ -269,7 +336,7 @@ export class UpdatePowerShell {
269336 const { stdout } = await promisify ( execFile ) ( "winget" , [
270337 "show" ,
271338 "--id" ,
272- "Microsoft.PowerShell" ,
339+ WINGET_PACKAGE ,
273340 "-s" ,
274341 "winget" ,
275342 "--accept-source-agreements" ,
@@ -278,24 +345,33 @@ export class UpdatePowerShell {
278345 if ( version !== undefined ) {
279346 winget = { installed : true , version } ;
280347 }
281- } catch {
282- // WinGet may not be installed — fall back to the GitHub API to
283- // ask which version it would offer.
284- try {
285- const entries = await fetchJSON < IWinGetManifestEntry [ ] > (
286- "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/m/Microsoft/PowerShell?per_page=100" ,
287- ) ;
288- const version = entries
289- ? getLatestWinGetVersion ( entries )
290- : undefined ;
348+ } catch ( error ) {
349+ if ( isWinGetMissing ( error ) ) {
350+ // WinGet is not installed — ask the winget-pkgs listing
351+ // which version it would offer.
352+ try {
353+ const entries = await fetchJSON < IWinGetManifestEntry [ ] > (
354+ `https://api.github.com/repos/${ WINGET_PKGS_REPO } /contents/manifests/m/Microsoft/PowerShell?per_page=100` ,
355+ ) ;
356+ const version = entries
357+ ? getLatestWinGetVersion ( entries )
358+ : undefined ;
359+ this . logger . writeDebug (
360+ `WinGet repo latest: ${ version ?? "not found" } ` ,
361+ ) ;
362+ if ( version !== undefined ) {
363+ winget = { installed : false , version } ;
364+ }
365+ } catch {
366+ // Best effort.
367+ }
368+ } else {
369+ // WinGet is installed, it just did not answer (a source or
370+ // network problem), so keep it and leave the version unknown.
291371 this . logger . writeDebug (
292- `WinGet repo latest : ${ version ?? "not found "} ` ,
372+ `Could not get the WinGet version : ${ error instanceof Error ? error . message : "unknown "} ` ,
293373 ) ;
294- if ( version !== undefined ) {
295- winget = { installed : false , version } ;
296- }
297- } catch {
298- // Best effort.
374+ winget = { installed : true } ;
299375 }
300376 }
301377 }
@@ -324,34 +400,36 @@ export class UpdatePowerShell {
324400 case "winget" :
325401 if ( winget . installed ) {
326402 this . logger . write ( "Upgrading PowerShell via WinGet..." ) ;
327- vscode . window
328- . createTerminal ( "PowerShell Upgrade (WinGet)" )
329- . sendText (
330- "winget update --id Microsoft.PowerShell -e -s winget" ,
331- ) ;
403+ runInTerminal (
404+ "PowerShell Upgrade (WinGet)" ,
405+ WINGET_UPGRADE ,
406+ ) ;
332407 } else {
333408 // From: https://aka.ms/winget-docs
334409 this . logger . write (
335410 "Installing WinGet and upgrading PowerShell..." ,
336411 ) ;
337- vscode . window
338- . createTerminal ( "Install WinGet & Upgrade PowerShell" )
339- . sendText (
340- "$result = Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -ErrorAction SilentlyContinue; if ($?) { winget update --id Microsoft.PowerShell -e -s winget } else { Write-Warning 'Failed to install WinGet. See https://aka.ms/winget-docs' }" ,
341- ) ;
412+ // The command is PowerShell (Add-AppxPackage is a cmdlet),
413+ // so don't run it in whatever profile happens to be the
414+ // user's default terminal.
415+ runInTerminal (
416+ "Install WinGet & Upgrade PowerShell" ,
417+ "$result = Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -ErrorAction SilentlyContinue; " +
418+ `if ($?) { ${ WINGET_UPGRADE } } else { Write-Warning 'Failed to install WinGet. See https://aka.ms/winget-docs' }` ,
419+ "powershell.exe" ,
420+ ) ;
342421 }
343422 break ;
344423 case "winget-progress" : {
345424 // Open the winget-pkgs issue or PR for the version WinGet is
346425 // missing; find the open issue/PR mentioning the highest
347426 // PowerShell version.
348427 let statusUrl =
349- "https://github.com/microsoft/winget-pkgs/pulls?q=Microsoft.PowerShell+is:open" ;
428+ `https://github.com/${ WINGET_PKGS_REPO } ` +
429+ `/pulls?q=${ WINGET_PACKAGE } +is:open` ;
350430 try {
351- let bestPR : string | undefined ;
352- let bestIssue : string | undefined ;
353- let bestPRVer : string | undefined ;
354- let bestIssueVer : string | undefined ;
431+ let bestPR : IBestMatch | undefined ;
432+ let bestIssue : IBestMatch | undefined ;
355433 for ( const {
356434 ver,
357435 html_url,
@@ -366,7 +444,7 @@ export class UpdatePowerShell {
366444 pull_request ?: unknown ;
367445 } [ ] ;
368446 } > (
369- " https://api.github.com/search/issues?q=Microsoft.PowerShell +repo:microsoft/winget-pkgs +is:open&sort=created&order=desc&per_page=30" ,
447+ ` https://api.github.com/search/issues?q=${ WINGET_PACKAGE } +repo:${ WINGET_PKGS_REPO } +is:open&sort=created&order=desc&per_page=30` ,
370448 )
371449 ) ?. items ?? [ ]
372450 ) . map (
@@ -404,35 +482,30 @@ export class UpdatePowerShell {
404482 ) ) {
405483 if ( ! ver ) continue ;
406484 if ( pull_request ) {
407- if (
408- ! bestPRVer ||
409- new SemVer ( ver ) . compare ( bestPRVer ) > 0
410- ) {
411- bestPRVer = ver ;
412- bestPR = html_url ;
413- }
414- } else if (
415- ! bestIssueVer ||
416- new SemVer ( ver ) . compare ( bestIssueVer ) > 0
417- ) {
418- bestIssueVer = ver ;
419- bestIssue = html_url ;
485+ bestPR = keepHighestMatch ( bestPR , ver , html_url ) ;
486+ } else {
487+ bestIssue = keepHighestMatch (
488+ bestIssue ,
489+ ver ,
490+ html_url ,
491+ ) ;
420492 }
421493 }
422494 // Prefer issue when linked to the PR for the same version.
423- const prVer = bestPRVer ? new SemVer ( bestPRVer ) : undefined ;
424- const issueVer = bestIssueVer
425- ? new SemVer ( bestIssueVer )
426- : undefined ;
427- if ( prVer && issueVer && prVer . compare ( issueVer ) === 0 ) {
428- statusUrl = bestIssue ! ;
495+ if (
496+ bestPR &&
497+ bestIssue &&
498+ bestPR . version . compare ( bestIssue . version ) === 0
499+ ) {
500+ statusUrl = bestIssue . url ;
429501 } else if (
430- prVer &&
431- ( ! issueVer || prVer . compare ( issueVer ) > 0 )
502+ bestPR &&
503+ ( ! bestIssue ||
504+ bestPR . version . compare ( bestIssue . version ) > 0 )
432505 ) {
433- statusUrl = bestPR ! ;
434- } else if ( issueVer ) {
435- statusUrl = bestIssue ! ;
506+ statusUrl = bestPR . url ;
507+ } else if ( bestIssue ) {
508+ statusUrl = bestIssue . url ;
436509 }
437510 } catch {
438511 // Fall back to generic search.
0 commit comments