diff --git a/CHANGELOG.md b/CHANGELOG.md index db237ac..319c1f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,13 @@ changes listed under Unreleased above as of this tag. the Homebrew tap and Scoop bucket. The `cli-v*` tags and GitHub releases are retired; `scripts/install.sh`, the formula and the Scoop manifest download from `https://git.kof22.com/QRun-IO/qqq-cli/releases`. +### Fixed + +- **Windows: the DPAPI keyring works when `qqq` is started from PowerShell 7.** The inherited `PSModulePath` + named PowerShell 7's modules, so Windows PowerShell 5.1 could not load `Microsoft.PowerShell.Security` and every + sign-in failed with "Windows credential protection failed". The keyring now starts PowerShell without it. + Found by the first native Windows release lane. + ## [1.0.0] - 2026-11-01 `qqq` 1.0: the command-line client and the full-screen terminal UI (`qqq ui`), shipped together as one diff --git a/packages/core/src/session/keyring.ts b/packages/core/src/session/keyring.ts index 25e4f80..8197e57 100644 --- a/packages/core/src/session/keyring.ts +++ b/packages/core/src/session/keyring.ts @@ -110,7 +110,10 @@ export class DpapiFileStore implements SecretStore { + '[Console]::Write((ConvertTo-Json -InputObject $qqqResult -Compress))' return new Promise((resolve, reject) => { const failed = () => reject(new Error('Windows credential protection failed; check PowerShell and your Windows profile')) - const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000 }, (error, stdout) => { + // Started from PowerShell 7, the inherited PSModulePath names PowerShell 7's modules, and Windows PowerShell 5.1 + // then cannot load Microsoft.PowerShell.Security (ConvertTo-SecureString). Without it, each shell uses its own. + const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'PSMODULEPATH')) + const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000, env }, (error, stdout) => { if (error) { failed(); return } try { const result: unknown = JSON.parse(stdout.trim())