Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Three developer setups live in this repo. Pick the one that matches what you wan
| --- | --- |
| A complete dev workstation: tools, OS settings, WSL, and terminal. One command, may reboot. | [Windows Dev Config](#%EF%B8%8F-windows-dev-config) |
| A polished WSL shell: zsh/bash, Starship, CLI tools, and a themed terminal profile. Interactive or unattended. | [WSL Comfort](#-wsl-comfort) |
| A single language toolchain: Node, Python, .NET, Rust, Go, Java, PHP, WinForms, or WinUI 3. One command each. | [Workloads](#-single-language-workloads) |
| A single language toolchain: Node, Python, PowerShell, .NET, Rust, Go, Java, PHP, WinForms, or WinUI 3. One command each. | [Workloads](#-single-language-workloads) |

Most of them use [`winget configure`](https://learn.microsoft.com/en-us/windows/package-manager/winget/configure). If you've never used it before, enable it once:

Expand Down Expand Up @@ -136,6 +136,7 @@ Just want one toolchain? Pick a row. Each workload ships a `configuration.winget
| Java | Microsoft Build of OpenJDK 25 LTS | `winget configure -f .\Workloads\java\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| Rust | Rust stable via rustup | `winget configure -f .\Workloads\rust\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| Python | Python 3.14 + uv | `winget configure -f .\Workloads\python\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| PowerShell | PowerShell 7 + VS Code PowerShell extensions + PSScriptAnalyzer settings | `winget configure -f .\Workloads\powershell\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| WinForms | .NET SDK 10 + Windows Forms desktop workload | `winget configure -f .\Workloads\winforms\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| WinUI 3 | .NET SDK 10 + Visual Studio Community + Windows App SDK / WinUI 3 + WinAppCLI | `winget configure -f .\Workloads\winui\configuration.winget --accept-configuration-agreements --disable-interactivity` |

Expand Down
323 changes: 323 additions & 0 deletions src/Workloads/powershell/configuration.winget
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
#
# winget configure --file scripts/windows/powershell/configuration.winget `
# --accept-configuration-agreements `
# --disable-interactivity
#
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
metadata:
winget:
processor: dscv3
resources:
- name: PowerShell
type: Microsoft.WinGet/Package
properties:
id: Microsoft.PowerShell
source: winget
acceptAgreements: true
metadata:
description: Install PowerShell 7
winget:
securityContext: elevated

- name: VSCode
type: Microsoft.WinGet/Package
dependsOn:
- PowerShell
properties:
id: Microsoft.VisualStudioCode
source: winget
acceptAgreements: true
metadata:
description: Install Visual Studio Code
winget:
securityContext: elevated

# TODO: When PowerShell 7+ ships Microsoft.PowerShell.PSResourceGet v1.3.0, change it for built-in DSC resource module
- name: InstallVSCodeDscModule
type: Microsoft.DSC.Transitional/PowerShellScript
dependsOn:
- PowerShell
metadata:
description: Install the Microsoft.VSCode.Dsc prerelease resource module
properties:
getScript: |
$module = Get-InstalledPSResource -Name Microsoft.VSCode.Dsc -ErrorAction SilentlyContinue |
Where-Object { $_.Version.ToString() -eq '0.1.6-alpha' -and $_.PrivateData.PSData.Prerelease -eq 'alpha' } |
Select-Object -First 1
$version = if ($module) { $module.Version.ToString() } else { $null }
return @{ installed = [bool]$module; version = $version }
testScript: |
$module = Get-InstalledPSResource -Name Microsoft.VSCode.Dsc -ErrorAction SilentlyContinue |
Where-Object { $_.Version.ToString() -eq '0.1.6-alpha' -and $_.PrivateData.PSData.Prerelease -eq 'alpha' } |
Select-Object -First 1
return [bool]$module
setScript: |
Install-PSResource -Name Microsoft.VSCode.Dsc `
-Version 0.1.6-alpha `
-Prerelease `
-Repository PSGallery `
-TrustRepository `
-AcceptLicense `
-Scope CurrentUser

- name: PowerShellVSCodeExtension
type: Microsoft.VSCode.Dsc/VSCodeExtension
dependsOn:
- VSCode
- InstallVSCodeDscModule
metadata:
description: Install the PowerShell VS Code extension
properties:
Name: ms-vscode.powershell
Exist: true

- name: PesterTestVSCodeExtension
type: Microsoft.VSCode.Dsc/VSCodeExtension
dependsOn:
- VSCode
- InstallVSCodeDscModule
metadata:
description: Install the Pester Test VS Code extension
properties:
Name: pspester.pester-test
Exist: true

# Captured from: https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules-recommendations?view=ps-modules
- name: ConfigurePowerShellScriptAnalyzer
type: Microsoft.DSC.Transitional/PowerShellScript
dependsOn:
- PowerShellVSCodeExtension
- PesterTestVSCodeExtension
metadata:
description: Enable PSScriptAnalyzer rules in VS Code user settings
properties:
getScript: |
$ruleNames = @(
'PSAvoidDefaultValueSwitchParameter',
'PSAvoidGlobalVars',
'PSAvoidUsingCmdletAliases',
'PSAvoidUsingComputerNameHardcoded',
'PSAvoidUsingConvertToSecureStringWithPlainText',
'PSAvoidUsingEmptyCatchBlock',
'PSAvoidUsingInvokeExpression',
'PSAvoidUsingPlainTextForPassword',
'PSAvoidUsingPositionalParameters',
'PSAvoidUsingUsernameAndPasswordParams',
'PSAvoidUsingWMICmdlet',
'PSAvoidUsingWriteHost',
'PSDSCDscExamplesPresent',
'PSDSCDscTestsPresent',
'PSDSCReturnCorrectTypesForDSCFunctions',
'PSDSCStandardDSCFunctionsInResource',
'PSDSCUseIdenticalMandatoryParametersForDSC',
'PSDSCUseIdenticalParametersForDSC',
'PSMissingModuleManifestField',
'PSProvideCommentHelp',
'PSReservedCmdletChar',
'PSReservedParams',
'PSShouldProcess',
'PSUseApprovedVerbs',
'PSUseCmdletCorrectly',
'PSUseDeclaredVarsMoreThanAssignments',
'PSUsePSCredentialType',
'PSUseShouldProcessForStateChangingFunctions',
'PSUseSingularNouns',
'PSUseSupportsShouldProcess'
)

function Get-VSCodeUserDirectory {
if ($IsWindows) { return Join-Path $env:APPDATA 'Code\User' }
if ($IsMacOS) { return Join-Path $HOME 'Library/Application Support/Code/User' }
return Join-Path $HOME '.config/Code/User'
}

function Read-VSCodeSettings {
param([Parameter(Mandatory)] [string] $Path)

if (-not (Test-Path -LiteralPath $Path)) { return [ordered]@{} }

$raw = Get-Content -LiteralPath $Path -Raw
$clean = [regex]::Replace($raw, '/\*[\s\S]*?\*/', '')
$clean = [regex]::Replace($clean, '(?m)^\s*//.*$', '')
if ([string]::IsNullOrWhiteSpace($clean)) { return [ordered]@{} }

return $clean | ConvertFrom-Json -AsHashtable
}

$userDirectory = Get-VSCodeUserDirectory
$settingsPath = Join-Path $userDirectory 'settings.json'
$analyzerSettingsPath = Join-Path $userDirectory 'PSScriptAnalyzerSettings.psd1'
$settings = Read-VSCodeSettings -Path $settingsPath
$configuredRuleCount = 0

if (Test-Path -LiteralPath $analyzerSettingsPath) {
try {
$analyzerSettings = Import-PowerShellDataFile -LiteralPath $analyzerSettingsPath
$configuredRuleCount = @($analyzerSettings.IncludeRules).Count
} catch {
$configuredRuleCount = 0
}
}

return @{
scriptAnalysisEnabled = [bool]$settings['powershell.scriptAnalysis.enable']
settingsPath = $settings['powershell.scriptAnalysis.settingsPath']
analyzerSettingsPresent = Test-Path -LiteralPath $analyzerSettingsPath
configuredRuleCount = $configuredRuleCount
expectedRuleCount = $ruleNames.Count
}
testScript: |
$ruleNames = @(
'PSAvoidDefaultValueSwitchParameter',
'PSAvoidGlobalVars',
'PSAvoidUsingCmdletAliases',
'PSAvoidUsingComputerNameHardcoded',
'PSAvoidUsingConvertToSecureStringWithPlainText',
'PSAvoidUsingEmptyCatchBlock',
'PSAvoidUsingInvokeExpression',
'PSAvoidUsingPlainTextForPassword',
'PSAvoidUsingPositionalParameters',
'PSAvoidUsingUsernameAndPasswordParams',
'PSAvoidUsingWMICmdlet',
'PSAvoidUsingWriteHost',
'PSDSCDscExamplesPresent',
'PSDSCDscTestsPresent',
'PSDSCReturnCorrectTypesForDSCFunctions',
'PSDSCStandardDSCFunctionsInResource',
'PSDSCUseIdenticalMandatoryParametersForDSC',
'PSDSCUseIdenticalParametersForDSC',
'PSMissingModuleManifestField',
'PSProvideCommentHelp',
'PSReservedCmdletChar',
'PSReservedParams',
'PSShouldProcess',
'PSUseApprovedVerbs',
'PSUseCmdletCorrectly',
'PSUseDeclaredVarsMoreThanAssignments',
'PSUsePSCredentialType',
'PSUseShouldProcessForStateChangingFunctions',
'PSUseSingularNouns',
'PSUseSupportsShouldProcess'
)

function Get-VSCodeUserDirectory {
if ($IsWindows) { return Join-Path $env:APPDATA 'Code\User' }
if ($IsMacOS) { return Join-Path $HOME 'Library/Application Support/Code/User' }
return Join-Path $HOME '.config/Code/User'
}

function Read-VSCodeSettings {
param([Parameter(Mandatory)] [string] $Path)

if (-not (Test-Path -LiteralPath $Path)) { return [ordered]@{} }

$raw = Get-Content -LiteralPath $Path -Raw
$clean = [regex]::Replace($raw, '/\*[\s\S]*?\*/', '')
$clean = [regex]::Replace($clean, '(?m)^\s*//.*$', '')
if ([string]::IsNullOrWhiteSpace($clean)) { return [ordered]@{} }

return $clean | ConvertFrom-Json -AsHashtable
}

$userDirectory = Get-VSCodeUserDirectory
$settingsPath = Join-Path $userDirectory 'settings.json'
$analyzerSettingsPath = Join-Path $userDirectory 'PSScriptAnalyzerSettings.psd1'
$settings = Read-VSCodeSettings -Path $settingsPath

if ($settings['powershell.scriptAnalysis.enable'] -ne $true) { return $false }
if ($settings['powershell.scriptAnalysis.settingsPath'] -ne $analyzerSettingsPath) { return $false }
if (-not (Test-Path -LiteralPath $analyzerSettingsPath)) { return $false }

try {
$analyzerSettings = Import-PowerShellDataFile -LiteralPath $analyzerSettingsPath
} catch {
return $false
}

if ($analyzerSettings.IncludeDefaultRules -ne $true) { return $false }

$configuredRules = @($analyzerSettings.IncludeRules)
foreach ($ruleName in $ruleNames) {
if ($ruleName -notin $configuredRules) { return $false }
}

return $true
setScript: |
$ruleNames = @(
'PSAvoidDefaultValueSwitchParameter',
'PSAvoidGlobalVars',
'PSAvoidUsingCmdletAliases',
'PSAvoidUsingComputerNameHardcoded',
'PSAvoidUsingConvertToSecureStringWithPlainText',
'PSAvoidUsingEmptyCatchBlock',
'PSAvoidUsingInvokeExpression',
'PSAvoidUsingPlainTextForPassword',
'PSAvoidUsingPositionalParameters',
'PSAvoidUsingUsernameAndPasswordParams',
'PSAvoidUsingWMICmdlet',
'PSAvoidUsingWriteHost',
'PSDSCDscExamplesPresent',
'PSDSCDscTestsPresent',
'PSDSCReturnCorrectTypesForDSCFunctions',
'PSDSCStandardDSCFunctionsInResource',
'PSDSCUseIdenticalMandatoryParametersForDSC',
'PSDSCUseIdenticalParametersForDSC',
'PSMissingModuleManifestField',
'PSProvideCommentHelp',
'PSReservedCmdletChar',
'PSReservedParams',
'PSShouldProcess',
'PSUseApprovedVerbs',
'PSUseCmdletCorrectly',
'PSUseDeclaredVarsMoreThanAssignments',
'PSUsePSCredentialType',
'PSUseShouldProcessForStateChangingFunctions',
'PSUseSingularNouns',
'PSUseSupportsShouldProcess'
)

function Get-VSCodeUserDirectory {
if ($IsWindows) { return Join-Path $env:APPDATA 'Code\User' }
if ($IsMacOS) { return Join-Path $HOME 'Library/Application Support/Code/User' }
return Join-Path $HOME '.config/Code/User'
}

function Read-VSCodeSettings {
param([Parameter(Mandatory)] [string] $Path)

if (-not (Test-Path -LiteralPath $Path)) { return [ordered]@{} }

$raw = Get-Content -LiteralPath $Path -Raw
$clean = [regex]::Replace($raw, '/\*[\s\S]*?\*/', '')
$clean = [regex]::Replace($clean, '(?m)^\s*//.*$', '')
if ([string]::IsNullOrWhiteSpace($clean)) { return [ordered]@{} }

return $clean | ConvertFrom-Json -AsHashtable
}

$userDirectory = Get-VSCodeUserDirectory
New-Item -ItemType Directory -Path $userDirectory -Force | Out-Null

$settingsPath = Join-Path $userDirectory 'settings.json'
$analyzerSettingsPath = Join-Path $userDirectory 'PSScriptAnalyzerSettings.psd1'

$analyzerSettingsLines = [System.Collections.Generic.List[string]]::new()
[void]$analyzerSettingsLines.Add('@{')
[void]$analyzerSettingsLines.Add(' IncludeDefaultRules = $true')
[void]$analyzerSettingsLines.Add(' IncludeRules = @(')
foreach ($ruleName in $ruleNames) {
[void]$analyzerSettingsLines.Add(" '$ruleName'")
}
[void]$analyzerSettingsLines.Add(' )')
[void]$analyzerSettingsLines.Add('}')
$analyzerSettingsContent = $analyzerSettingsLines -join [Environment]::NewLine

$analyzerSettingsContent | Set-Content -LiteralPath $analyzerSettingsPath -Encoding utf8

$settings = Read-VSCodeSettings -Path $settingsPath
$settings['powershell.scriptAnalysis.enable'] = $true
$settings['powershell.scriptAnalysis.settingsPath'] = $analyzerSettingsPath
$settings | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $settingsPath -Encoding utf8

# TODO: Add other PowerShell configurations like formatting (https://poshcode.gitbook.io/powershell-practice-and-style)
21 changes: 21 additions & 0 deletions src/Workloads/powershell/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<#
.SYNOPSIS
Apply the PowerShell winget DSC configuration on Windows.

.DESCRIPTION
This script is a thin CI/dev shim. The core artifact for the PowerShell flow
is `configuration.winget` in this directory, which declaratively installs
PowerShell 7, VS Code, PowerShell-focused VS Code extensions, and user-level
PSScriptAnalyzer settings.
#>

[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

& (Join-Path $PSScriptRoot '..\_common\apply-configuration.ps1') `
-Id 'powershell' `
-ConfigFile (Join-Path $PSScriptRoot 'configuration.winget') `
-RequireCommands @('pwsh', 'code')
1 change: 1 addition & 0 deletions src/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extension.
| Java | ✅ automated | `Microsoft.OpenJDK.25` |
| Rust | ✅ automated | `Rustlang.Rustup` (then `rustup default stable`) |
| Python | ✅ automated | `Python.Python.3.14`, `astral-sh.uv` |
| PowerShell | ✅ automated | `Microsoft.PowerShell`, `Microsoft.VisualStudioCode`, VS Code PowerShell/Pester extensions + PSScriptAnalyzer settings |
| WinForms | 🙋 manual | `Microsoft.DotNet.SDK.10` + the .NET desktop workload (multi-GB; manual to spare CI minutes) |
| WinUI 3 | 🙋 manual | `Microsoft.DotNet.SDK.10`, `Microsoft.VisualStudio.Community`, `Microsoft.WinAppCLI` + WinUI/Universal/ManagedDesktop VS workloads |
| Calm OS | 🙋 manual | A full distraction-free workstation: apps + ~24 registry tweaks + WSL + Ubuntu (see [`windows-dev-config/README.md`](../windows-dev-config/README.md)) |
Expand Down
16 changes: 16 additions & 0 deletions src/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ flows:
expected: src/tests/python/expected.txt
version: "python --version; uv --version"

- id: powershell
name: PowerShell
description: PowerShell 7 + VS Code PowerShell tooling and script analysis
category: languages
tags: [powershell, pwsh, pester, psscriptanalyzer, vscode]
icon: 💙
onboardingUrl: https://learn.microsoft.com/powershell/scripting/overview
os: [windows]
windows:
install: Workloads/powershell/install.ps1
configuration: Workloads/powershell/configuration.winget
build: ""
run: pwsh -NoProfile -File src/tests/powershell/hello.ps1
expected: src/tests/powershell/expected.txt
version: "pwsh --version; code --version"

- id: comfort-shell
name: Comfort Shell
description: Cozy WSL shell setup — zsh/bash, starship, modern CLI tools, and shims
Expand Down
1 change: 1 addition & 0 deletions src/tests/powershell/expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello, world!
Loading
Loading