From b46fd63e16a2457ee807fb7851450cd872929017 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 1 Jul 2026 15:39:36 +0200 Subject: [PATCH 1/3] Replace abandoned winreg module with @vscode/windows-registry winreg is abandoned and its 1.2.5 release has a known bug we pinned around. Switch the Windows CLI installation manager to Microsoft's maintained @vscode/windows-registry for reads, and write PATH via PowerShell SetEnvironmentVariable (which also broadcasts WM_SETTINGCHANGE so open shells refresh without a re-login). Co-Authored-By: Claude Opus 4.8 --- apps/studio/package.json | 3 +- .../cli/lib/windows-installation-manager.ts | 44 ++++---- package-lock.json | 106 ++---------------- 3 files changed, 28 insertions(+), 125 deletions(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index b4367f27d1..9da4c8c6df 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -33,6 +33,7 @@ "@sentry/electron": "^7.13.0", "@studio/common": "file:../../packages/common", "@vscode/sudo-prompt": "^9.3.1", + "@vscode/windows-registry": "1.2.0", "@wordpress/i18n": "^6.20.0", "atomically": "^2.1.0", "compressible": "2.0.18", @@ -52,7 +53,6 @@ "strip-ansi": "^7.1.2", "tar": "^7.5.16", "tus-js-client": "^4.3.1", - "winreg": "1.2.4", "yauzl": "^3.2.1", "zod": "^4.0.0" }, @@ -81,7 +81,6 @@ "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/shell-quote": "^1.7.5", - "@types/winreg": "^1.2.36", "@types/yauzl": "^2.10.3", "@vitejs/plugin-react": "^6.0.2", "@wordpress/components": "34.0.0", diff --git a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts index e140cc8f18..f73bd5c002 100644 --- a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts +++ b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts @@ -1,10 +1,11 @@ import { app, dialog } from 'electron'; import { mkdir, rm, writeFile } from 'fs/promises'; +import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import path from 'path'; import * as Sentry from '@sentry/electron/main'; +import { GetStringRegKey } from '@vscode/windows-registry'; import { __ } from '@wordpress/i18n'; -import Registry from 'winreg'; // don't update winreg to 1.2.5 - https://github.com/fresc81/node-winreg/issues/65 import { getMainWindow } from 'src/main-window'; import { StudioCliInstallationManager } from 'src/modules/cli/lib/ipc-handlers'; import { loadUserData, updateAppdata } from 'src/storage/user-data'; @@ -12,13 +13,6 @@ import { loadUserData, updateAppdata } from 'src/storage/user-data'; // `STABLE_BIN_DIR_PATH` resolves to C:\Users\\AppData\Local\studio\bin export const STABLE_BIN_DIR_PATH = path.resolve( path.dirname( app.getPath( 'exe' ) ), '../bin' ); -const PATH_KEY = 'Path'; - -const currentUserRegistry = new Registry( { - hive: Registry.HKCU, - key: '\\Environment', -} ); - export class WindowsCliInstallationManager implements StudioCliInstallationManager { constructor() { if ( process.platform !== 'win32' ) { @@ -118,27 +112,27 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag } } - private getPathFromRegistry(): Promise< string > { - return new Promise( ( resolve, reject ) => { - currentUserRegistry.get( PATH_KEY, ( error, item ) => { - if ( error ) { - return reject( error ); - } - - resolve( item?.value || '' ); - } ); - } ); + private async getPathFromRegistry(): Promise< string > { + // @vscode/windows-registry is read-only; reads the user PATH synchronously. + return GetStringRegKey( 'HKEY_CURRENT_USER', 'Environment', 'Path' ) ?? ''; } private setPathInRegistry( updatedPath: string ): Promise< void > { return new Promise( ( resolve, reject ) => { - currentUserRegistry.set( PATH_KEY, Registry.REG_EXPAND_SZ, updatedPath, ( error ) => { - if ( error ) { - return reject( error ); - } - - resolve(); - } ); + // @vscode/windows-registry is read-only, so write PATH via PowerShell. + // SetEnvironmentVariable(..., 'User') also broadcasts WM_SETTINGCHANGE + // so open shells pick up the new PATH without a re-login. + const escaped = updatedPath.replace( /'/g, "''" ); + const script = `[Environment]::SetEnvironmentVariable('PATH', '${ escaped }', 'User')`; + const child = spawn( + 'powershell.exe', + [ '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script ], + { windowsHide: true } + ); + child.on( 'error', reject ); + child.on( 'exit', ( code ) => + code === 0 ? resolve() : reject( new Error( `PowerShell exited with code ${ code }` ) ) + ); } ); } diff --git a/package-lock.json b/package-lock.json index 48d0665f82..e784252a6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -524,6 +524,7 @@ "@sentry/electron": "^7.13.0", "@studio/common": "file:../../packages/common", "@vscode/sudo-prompt": "^9.3.1", + "@vscode/windows-registry": "1.2.0", "@wordpress/i18n": "^6.20.0", "atomically": "^2.1.0", "compressible": "2.0.18", @@ -543,7 +544,6 @@ "strip-ansi": "^7.1.2", "tar": "^7.5.16", "tus-js-client": "^4.3.1", - "winreg": "1.2.4", "yauzl": "^3.2.1", "zod": "^4.0.0" }, @@ -572,7 +572,6 @@ "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/shell-quote": "^1.7.5", - "@types/winreg": "^1.2.36", "@types/yauzl": "^2.10.3", "@vitejs/plugin-react": "^6.0.2", "@wordpress/components": "34.0.0", @@ -4788,9 +4787,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4805,9 +4801,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4822,9 +4815,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4839,9 +4829,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4856,9 +4843,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11517,9 +11501,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11537,9 +11518,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11557,9 +11535,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11577,9 +11552,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11597,9 +11569,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -11617,9 +11586,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11718,9 +11684,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13029,11 +12992,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/winreg": { - "version": "1.2.36", - "dev": true, - "license": "MIT" - }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "dev": true, @@ -13418,9 +13376,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13435,9 +13390,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -13452,9 +13404,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13469,9 +13418,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13486,9 +13432,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -13503,9 +13446,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13520,9 +13460,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -13537,9 +13474,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -13755,6 +13689,13 @@ "version": "9.3.2", "license": "MIT" }, + "node_modules/@vscode/windows-registry": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.2.0.tgz", + "integrity": "sha512-IouvAGIIjPDKsLlBkdjVgfHrE+s4aESXzyQWwEPzbzHumKbCqIL5n54PNX0R4PHkYQlbinVrXn282KGSO4cr9A==", + "hasInstallScript": true, + "license": "MIT" + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "dev": true, @@ -23221,9 +23162,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -23245,9 +23183,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -23269,9 +23204,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -23293,9 +23225,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -31623,10 +31552,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/winreg": { - "version": "1.2.4", - "license": "BSD-2-Clause" - }, "node_modules/word-wrap": { "version": "1.2.5", "license": "MIT", @@ -32794,9 +32719,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -32814,9 +32736,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -32834,9 +32753,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -32854,9 +32770,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -32874,9 +32787,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From 235c247cdf0b16a15203c81ea61008aa335422e5 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Thu, 2 Jul 2026 08:43:08 +0200 Subject: [PATCH 2/3] Read PATH via PowerShell instead of native @vscode/windows-registry @vscode/windows-registry is a native module with no prebuilt binaries, so it ran node-gyp on install and failed to compile on Windows CI. The original winreg was pure JS. Drop the native dependency entirely and read the user PATH via PowerShell (raw, unexpanded, matching uninstall.ts), sharing one PowerShell runner with the existing write. No native build step, works everywhere PowerShell does. Co-Authored-By: Claude Opus 4.8 --- apps/studio/package.json | 1 - .../cli/lib/windows-installation-manager.ts | 53 ++++++++++++++----- package-lock.json | 8 --- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/apps/studio/package.json b/apps/studio/package.json index 9da4c8c6df..4fd005291b 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -33,7 +33,6 @@ "@sentry/electron": "^7.13.0", "@studio/common": "file:../../packages/common", "@vscode/sudo-prompt": "^9.3.1", - "@vscode/windows-registry": "1.2.0", "@wordpress/i18n": "^6.20.0", "atomically": "^2.1.0", "compressible": "2.0.18", diff --git a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts index f73bd5c002..4ba93b3f22 100644 --- a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts +++ b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts @@ -4,7 +4,6 @@ import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import path from 'path'; import * as Sentry from '@sentry/electron/main'; -import { GetStringRegKey } from '@vscode/windows-registry'; import { __ } from '@wordpress/i18n'; import { getMainWindow } from 'src/main-window'; import { StudioCliInstallationManager } from 'src/modules/cli/lib/ipc-handlers'; @@ -112,30 +111,58 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag } } - private async getPathFromRegistry(): Promise< string > { - // @vscode/windows-registry is read-only; reads the user PATH synchronously. - return GetStringRegKey( 'HKEY_CURRENT_USER', 'Environment', 'Path' ) ?? ''; - } - - private setPathInRegistry( updatedPath: string ): Promise< void > { + /** + * Run a PowerShell script and resolve with its stdout. + * + * We read and write the user PATH via PowerShell rather than a native + * registry module: the previous `winreg` dependency is abandoned, and its + * maintained native replacements compile from source (node-gyp), which is a + * poor fit for our cross-platform build. PowerShell is always present on + * Windows and needs no build step. This mirrors the raw-registry approach in + * `apps/cli/commands/uninstall.ts`. + */ + private runPowerShell( script: string ): Promise< string > { return new Promise( ( resolve, reject ) => { - // @vscode/windows-registry is read-only, so write PATH via PowerShell. - // SetEnvironmentVariable(..., 'User') also broadcasts WM_SETTINGCHANGE - // so open shells pick up the new PATH without a re-login. - const escaped = updatedPath.replace( /'/g, "''" ); - const script = `[Environment]::SetEnvironmentVariable('PATH', '${ escaped }', 'User')`; const child = spawn( 'powershell.exe', [ '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script ], { windowsHide: true } ); + + let stdout = ''; + child.stdout.on( 'data', ( chunk ) => { + stdout += chunk.toString(); + } ); child.on( 'error', reject ); child.on( 'exit', ( code ) => - code === 0 ? resolve() : reject( new Error( `PowerShell exited with code ${ code }` ) ) + code === 0 + ? resolve( stdout ) + : reject( new Error( `PowerShell exited with code ${ code }` ) ) ); } ); } + private async getPathFromRegistry(): Promise< string > { + // Read the raw, unexpanded value (DoNotExpandEnvironmentNames) so that any + // %VAR% entries survive a read/modify/write round-trip untouched. + const script = ` +$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment') +if ($key) { + [Console]::Out.Write([string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)) + $key.Close() +}`; + return ( await this.runPowerShell( script ) ).trim(); + } + + private async setPathInRegistry( updatedPath: string ): Promise< void > { + // SetEnvironmentVariable(..., 'User') also broadcasts WM_SETTINGCHANGE so + // open shells pick up the new PATH without a re-login. + const escaped = updatedPath.replace( /'/g, "''" ); + await this.runPowerShell( + `[Environment]::SetEnvironmentVariable('PATH', '${ escaped }', 'User')` + ); + } + private async isStudioCliDirInPath(): Promise< boolean > { let studioCliDir = STABLE_BIN_DIR_PATH; diff --git a/package-lock.json b/package-lock.json index e784252a6c..ba81ca218a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -524,7 +524,6 @@ "@sentry/electron": "^7.13.0", "@studio/common": "file:../../packages/common", "@vscode/sudo-prompt": "^9.3.1", - "@vscode/windows-registry": "1.2.0", "@wordpress/i18n": "^6.20.0", "atomically": "^2.1.0", "compressible": "2.0.18", @@ -13689,13 +13688,6 @@ "version": "9.3.2", "license": "MIT" }, - "node_modules/@vscode/windows-registry": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.2.0.tgz", - "integrity": "sha512-IouvAGIIjPDKsLlBkdjVgfHrE+s4aESXzyQWwEPzbzHumKbCqIL5n54PNX0R4PHkYQlbinVrXn282KGSO4cr9A==", - "hasInstallScript": true, - "license": "MIT" - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "dev": true, From 4e783f95558e206d73532477f1f939a38036d398 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 7 Jul 2026 17:49:44 +0200 Subject: [PATCH 3/3] Remove PR-narrating comment from runPowerShell --- .../modules/cli/lib/windows-installation-manager.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts index 4ba93b3f22..b1fb02a627 100644 --- a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts +++ b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts @@ -111,16 +111,6 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag } } - /** - * Run a PowerShell script and resolve with its stdout. - * - * We read and write the user PATH via PowerShell rather than a native - * registry module: the previous `winreg` dependency is abandoned, and its - * maintained native replacements compile from source (node-gyp), which is a - * poor fit for our cross-platform build. PowerShell is always present on - * Windows and needs no build step. This mirrors the raw-registry approach in - * `apps/cli/commands/uninstall.ts`. - */ private runPowerShell( script: string ): Promise< string > { return new Promise( ( resolve, reject ) => { const child = spawn(