diff --git a/.agents/skills/NOTICE.md b/.agents/skills/NOTICE.md new file mode 100644 index 0000000..c83b55e --- /dev/null +++ b/.agents/skills/NOTICE.md @@ -0,0 +1,14 @@ +# Vendored Agent Skills — Attribution + +The skills under this directory are vendored (copied verbatim) from an upstream project and are +redistributed under their original license. + +- Source: +- Version: v2.2.0 +- License: MIT (see ) +- Skills: + - `psake` — from `plugins/psake/skills/psake` + - `powershellbuild` — from `plugins/powershellbuild/skills/powershellbuild` + +Do not edit the vendored copies in place; re-sync from upstream instead. Provenance and pinned +versions are tracked in `aim.config.json` under `skills`. diff --git a/.agents/skills/powershellbuild/SKILL.md b/.agents/skills/powershellbuild/SKILL.md new file mode 100644 index 0000000..2295ef1 --- /dev/null +++ b/.agents/skills/powershellbuild/SKILL.md @@ -0,0 +1,170 @@ +--- +name: powershellbuild +description: This skill should be used when the user asks to "set up PowerShellBuild", "create a psakeFile with -FromModule", "configure PSBPreference", "publish a PowerShell module to PSGallery", "set up Pester tests for a module", or mentions PowerShellBuild, PSBPreference, -FromModule PowerShellBuild, PowerShellBuild.IB.Tasks, PSScriptAnalyzer integration, PlatyPS help generation, code coverage thresholds, or PowerShell module build/test/publish pipelines. +--- + +# PowerShellBuild + +PowerShellBuild provides standardized build, test, and publish tasks for PowerShell modules. It works with both **psake** (≥ 4.8.0) and **Invoke-Build** (≥ 5.8.1). + +## Decision Tree + +**Which task runner are you using?** +- **psake** → use `task -FromModule PowerShellBuild` pattern +- **Invoke-Build** → use `. PowerShellBuild.IB.Tasks` pattern +- **Not sure / starting fresh** → default to psake (simpler syntax) + +**What do you need?** +- Set up a new module project → See `references/complete-example.md` +- Override build behavior → [Configuration ($PSBPreference)](#configuration-psbpreference) +- Customize task dependencies → [Modifying Task Dependencies](#modifying-task-dependencies) +- CI/CD setup → See `references/ci-cd.md` + +## Quick Start + +```powershell +Install-Module -Name PowerShellBuild -Repository PSGallery -Scope CurrentUser +Install-Module -Name psake -MinimumVersion 4.8.0 -Repository PSGallery -Scope CurrentUser +``` + +### Minimal psakeFile.ps1 + +> **Do NOT `Import-Module PowerShellBuild` in psakeFile.ps1** — `-FromModule` loads the module automatically when psake parses the task definitions. + +```powershell +properties { + $PSBPreference.Test.ScriptAnalysis.Enabled = $true + $PSBPreference.Test.CodeCoverage.Enabled = $false +} + +task default -depends Test + +task Test -FromModule PowerShellBuild +task Publish -FromModule PowerShellBuild +``` + +This gives you: `Init → Clean → StageFiles → BuildHelp → Build → Analyze → Pester → Test → Publish` + +## Project Structure + +``` +MyModule/ +├── build.ps1 # Entry point +├── psakeFile.ps1 # Build tasks (psake) +├── .build.ps1 # Build tasks (Invoke-Build) +├── requirements.psd1 # Dependencies +├── MyModule/ # Source directory +│ ├── MyModule.psd1 # Module manifest +│ ├── MyModule.psm1 # Module root +│ ├── Public/ # Exported functions +│ └── Private/ # Internal functions +├── tests/ +│ └── MyModule.Tests.ps1 +└── Output/ # Build output (auto-generated) +``` + +## Available Tasks + +### Primary Tasks + +| Task | Depends On | Description | +|---------|---------------------|------------------------------------| +| Init | — | Initialize build environment | +| Clean | Init | Remove output directory | +| Build | StageFiles, BuildHelp | Compile module to output | +| Analyze | Build | Run PSScriptAnalyzer | +| Pester | Build | Run Pester tests | +| Test | Analyze, Pester | Run all quality checks | +| Publish | Test | Publish to PowerShell Gallery | + +### Secondary Tasks + +| Task | Description | +|--------------------|--------------------------------------| +| StageFiles | Copy source files to output | +| GenerateMarkdown | Generate PlatyPS markdown help | +| GenerateMAML | Convert markdown to MAML help | +| BuildHelp | Run all help generation | + +## Configuration ($PSBPreference) + +Set these in your `properties` block before referencing PowerShellBuild tasks. + +### Build + +```powershell +$PSBPreference.General.ModuleName = 'MyModule' # auto-detected from manifest +$PSBPreference.General.SrcRootDir = './MyModule' # default: project root +$PSBPreference.Build.OutDir = './Output' +$PSBPreference.Build.CompileModule = $true # merge into single PSM1 +$PSBPreference.Build.CompileDirectories = @('Enum', 'Classes', 'Private', 'Public') +$PSBPreference.Build.CopyDirectories = @('Data') # copy as-is (no compile) +$PSBPreference.Build.Exclude = @('*.Tests.ps1') +``` + +### Test + +```powershell +$PSBPreference.Test.Enabled = $true +$PSBPreference.Test.RootDir = './tests' +$PSBPreference.Test.OutputFile = 'TestResults.xml' +$PSBPreference.Test.OutputFormat = 'NUnitXml' +$PSBPreference.Test.ScriptAnalysis.Enabled = $true +$PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel = 'Error' +$PSBPreference.Test.CodeCoverage.Enabled = $true +$PSBPreference.Test.CodeCoverage.Threshold = 0.75 # 0.0–1.0 +``` + +### Help & Docs + +```powershell +$PSBPreference.Help.DefaultLocale = 'en-US' +$PSBPreference.Help.ConvertReadMeToAboutHelp = $false +$PSBPreference.Docs.RootDir = './docs' +``` + +### Publish + +```powershell +$PSBPreference.Publish.PSRepository = 'PSGallery' +$PSBPreference.Publish.PSRepositoryApiKey = $env:PSGALLERY_API_KEY +``` + +## Modifying Task Dependencies + +Set these variables **before** the `-FromModule` references take effect: + +```powershell +$PSBBuildDependency = 'StageFiles' # skip help generation +$PSBTestDependency = 'Pester' # skip analysis +$PSBPublishDependency = 'Build' # publish without tests (not recommended) +``` + +## Invoke-Build Alternative + +```powershell +. PowerShellBuild.IB.Tasks + +$PSBPreference.Build.CompileModule = $true +$PSBPreference.Test.CodeCoverage.Enabled = $true +$PSBPreference.Test.CodeCoverage.Threshold = 0.75 + +task . Build +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| "Task 'Build' not found" | Ensure psake ≥ 4.8.0 for `-FromModule` support | +| Module not found | Run `./build.ps1 -Bootstrap` first | +| BuildHelp fails | Install PlatyPS: `Install-Module platyPS` | +| Tests not found | Check `$PSBPreference.Test.RootDir` matches your `tests/` path | +| ScriptAnalyzer fails build | Fix violations or set `FailBuildOnSeverityLevel = 'Warning'` | +| Code coverage below threshold | Raise `CodeCoverage.Threshold` or add tests | +| Publish fails | Verify `PSGALLERY_API_KEY` env var is set | + +## References + +- **`references/complete-example.md`** - Full project scaffold: build.ps1, requirements.psd1, psakeFile.ps1 with all tasks +- **`references/ci-cd.md`** - GitHub Actions workflow for test and publish pipelines diff --git a/.agents/skills/powershellbuild/references/ci-cd.md b/.agents/skills/powershellbuild/references/ci-cd.md new file mode 100644 index 0000000..2cdb6e3 --- /dev/null +++ b/.agents/skills/powershellbuild/references/ci-cd.md @@ -0,0 +1,37 @@ +# CI/CD Integration with PowerShellBuild + +## GitHub Actions + +```yaml +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Test + shell: pwsh + run: ./build.ps1 -Task Test -Bootstrap + + publish: + needs: test + runs-on: windows-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - name: Publish + shell: pwsh + run: ./build.ps1 -Task Publish -Bootstrap + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} +``` + +### Key Points + +- Always use `./build.ps1` as the entry point, not `Invoke-psake` directly — `build.ps1` handles bootstrapping dependencies and setting up the build environment. +- Use `-Bootstrap` on the first run (or always in CI) to install dependencies from `requirements.psd1`. +- Pass secrets as environment variables, not as parameters. +- Publish job should depend on the test job (`needs: test`) and only run on main branch. diff --git a/.agents/skills/powershellbuild/references/complete-example.md b/.agents/skills/powershellbuild/references/complete-example.md new file mode 100644 index 0000000..2711100 --- /dev/null +++ b/.agents/skills/powershellbuild/references/complete-example.md @@ -0,0 +1,94 @@ +# Complete PowerShellBuild Example + +## Project Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.psd1 +│ ├── MyModule.psm1 +│ ├── Private/ +│ │ └── HelperFunction.ps1 +│ └── Public/ +│ └── Get-Something.ps1 +├── tests/ +│ └── MyModule.Tests.ps1 +├── docs/ +├── build.ps1 +├── psakeFile.ps1 +└── requirements.psd1 +``` + +## build.ps1 + +```powershell +[cmdletbinding(DefaultParameterSetName = 'Task')] +param( + [parameter(ParameterSetName = 'Task', position = 0)] + [string[]]$Task = 'default', + + [switch]$Bootstrap, + + [parameter(ParameterSetName = 'Help')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +if ($Bootstrap.IsPresent) { + Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + if (-not (Get-Module -Name PSDepend -ListAvailable)) { + Install-Module -Name PSDepend -Repository PSGallery -Scope CurrentUser + } + Import-Module -Name PSDepend -Verbose:$false + Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue +} + +$psakeFile = './psakeFile.ps1' +if ($PSCmdlet.ParameterSetName -eq 'Help') { + Get-PSakeScriptTasks -buildFile $psakeFile | Format-Table -Property Name, Description +} else { + Set-BuildEnvironment -Force + Invoke-psake -buildFile $psakeFile -taskList $Task -Verbose:$VerbosePreference + exit ([int](-not $psake.build_success)) +} +``` + +## requirements.psd1 + +```powershell +@{ + PSDependOptions = @{ Target = 'CurrentUser' } + psake = '4.9.0' + PowerShellBuild = 'latest' + Pester = @{ + MinimumVersion = '5.6.1' + Parameters = @{ SkipPublisherCheck = $true } + } + PSScriptAnalyzer = '1.24.0' + platyPS = '0.14.2' +} +``` + +## psakeFile.ps1 (full) + +```powershell +properties { + $PSBPreference.Build.CompileModule = $true + $PSBPreference.Build.CompileDirectories = @('Enum', 'Classes', 'Private', 'Public') + $PSBPreference.Test.ScriptAnalysis.Enabled = $true + $PSBPreference.Test.CodeCoverage.Enabled = $true + $PSBPreference.Test.CodeCoverage.Threshold = 0.80 + $PSBPreference.Publish.PSRepositoryApiKey = $env:PSGALLERY_API_KEY +} + +task default -depends Test + +task Clean -FromModule PowerShellBuild +task Build -FromModule PowerShellBuild +task Analyze -FromModule PowerShellBuild +task Pester -FromModule PowerShellBuild +task Test -FromModule PowerShellBuild +task Publish -FromModule PowerShellBuild +``` diff --git a/.agents/skills/psake/SKILL.md b/.agents/skills/psake/SKILL.md new file mode 100644 index 0000000..889db50 --- /dev/null +++ b/.agents/skills/psake/SKILL.md @@ -0,0 +1,451 @@ +--- +name: psake +description: This skill should be used when the user asks to "create a psakefile", "set up a psake build", "add psake caching", "migrate to psake v5", "troubleshoot a psake build", or mentions psake, psakefile.ps1, Invoke-psake, build task dependencies, exec blocks, PsakeBuildResult, Get-PsakeBuildPlan, or PowerShell build automation for .NET, Node.js, or Docker projects. Also triggers on requests to set up CI/CD pipelines (GitHub Actions, Azure Pipelines, GitLab CI) using psake. +--- + +# psake Build Automation + +psake is a PowerShell build automation tool using a DSL for task-based builds with dependencies. psake v5 introduces a two-phase compile/run model, declarative syntax, file-based caching, and structured output. + +## Decision Tree + +**What kind of build are you creating?** + +1. **PowerShell module** → Use PowerShellBuild module (see references/powershell-modules.md) +2. **.NET/Node.js/Docker** → See references/build-types.md +3. **Simple custom build** → Continue below for core psake patterns + +**Build complexity?** + +- **Simple** (< 5 tasks, single project) → Use patterns in this file +- **Complex** (CI/CD, multiple environments, dynamic tasks) → See references/advanced.md + +## Quick Start + +```powershell +# Install +Install-Module -Name psake -Scope CurrentUser -Force + +# Run — interactive (prints formatted output to console) +Invoke-psake # Run 'Default' task +Invoke-psake -taskList Build, Test # Run specific tasks +Invoke-psake -docs # Show task documentation + +# Run — programmatic: LLM agents, CI scripts, build.ps1 wrappers +# -Quiet suppresses all console output and returns a PsakeBuildResult object. +# Always use this form when you need to check success or read task results. +$result = Invoke-psake -taskList Build, Test -Quiet +$result.Success # $true / $false — check this, don't parse console text +$result.ErrorMessage # populated when Success is $false +$result.Tasks # PsakeTaskResult[] — Name, Status, Duration, Cached +``` + +## Minimal psakefile.ps1 + +```powershell +Version 5 + +Properties { + $BuildDir = Join-Path $PSScriptRoot 'build' +} + +Task Default -depends Build + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } + New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null +} + +Task Build -depends Clean { + exec { dotnet build -o $BuildDir } +} + +Task Test -depends Build { + exec { dotnet test } +} +``` + +## Programmatic Invocation + +**Always use `-Quiet` when invoking psake from a script, CI step, or LLM agent.** Without it, psake streams formatted text to the console — noisy and unparseable. With it, psake returns a `PsakeBuildResult` object and produces no console output. + +```powershell +$result = Invoke-psake -buildFile ./psakefile.ps1 -taskList Build, Test -Quiet + +if (-not $result.Success) { + Write-Error $result.ErrorMessage + exit 1 +} + +# Inspect task-level results +$result.Tasks | ForEach-Object { + "$($_.Name): $($_.Status) ($($_.Duration.TotalSeconds)s)" +} +``` + +### build.ps1 Entry-Point Template + +Projects often have a thin `build.ps1` wrapper that bootstraps dependencies and delegates to psake. Generate it with the `-Quiet` pattern so any caller — human or LLM — gets structured results: + +```powershell +# build.ps1 +# +# Usage (interactive): ./build.ps1 # default task +# ./build.ps1 Build, Test # specific tasks +# ./build.ps1 -Bootstrap # install deps first +# +# Usage (programmatic): Invoke-psake -buildFile ./psakefile.ps1 -Quiet +# -Quiet returns a PsakeBuildResult (.Success, .Tasks, .ErrorMessage). +# Use that form directly in CI steps and LLM agents — skip this script. + +[CmdletBinding()] +param( + [ArgumentCompleter({ + param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) + try { + Get-PSakeScriptTasks -BuildFile (Join-Path $PSScriptRoot 'psakefile.ps1') -ErrorAction Stop | + Where-Object { $_.Name -like "$WordToComplete*" } | + Select-Object -ExpandProperty Name + } catch { @() } + })] + [string[]]$Task = 'Default', + + [switch]$Bootstrap +) + +$ErrorActionPreference = 'Stop' + +if ($Bootstrap) { + if (-not (Get-Module -ListAvailable -Name PSDepend)) { + Install-Module -Name PSDepend -Scope CurrentUser -Force -AllowClobber + } + $psDependArgs = @{ + Path = $PSScriptRoot + Recurse = $false + Install = $true + Import = $true + Force = $true + } + Invoke-PSDepend @psDependArgs +} else { + # Try importing cached modules first — avoids file-lock contention when CI + # jobs share a module cache and one job is mid-install. + $psDependArgs = @{ + Path = $PSScriptRoot + Recurse = $false + Import = $true + Force = $true + WarningAction = 'SilentlyContinue' + } + $imported = $false + try { Invoke-PSDepend @psDependArgs; $imported = $true } catch {} + + if (-not $imported) { + $psDependArgs['Install'] = $true + try { + Invoke-PSDepend @psDependArgs + } catch { + throw "Dependency install failed. If modules are locked, restart the build environment or re-run with -Bootstrap." + } + } +} + +$psakeArgs = @{ + buildFile = Join-Path $PSScriptRoot 'psakefile.ps1' + taskList = $Task + Quiet = $true +} +$result = Invoke-psake @psakeArgs + +if (-not $result.Success) { + Write-Error $result.ErrorMessage + exit 1 +} +``` + +> **For LLM agents:** skip `build.ps1` entirely. Call `Invoke-psake -Quiet` directly and inspect the returned `PsakeBuildResult` — you get structured data without spawning a child process or parsing output. + +## Core Commands + +### Task + +Two equivalent syntaxes — use whichever reads better for your build: + +```powershell +# Classic syntax (works in v4 and v5) +Task Build -depends Clean -description "Compile project" { + exec { dotnet build } +} + +# Declarative syntax (v5 — hashtable with validated keys) +Task 'Build' @{ + DependsOn = 'Clean' + Description = 'Compile project' + Action = { exec { dotnet build } } +} +``` + +The declarative syntax validates keys at parse time — typos like `DependOn` throw immediately. Valid keys: `Action`, `DependsOn`, `Inputs`, `Outputs`, `PreAction`, `PostAction`, `PreCondition`, `PostCondition`, `ContinueOnError`, `Description`, `Alias`, `RequiredVariables`. + +#### Task with Caching + +Tasks with `Inputs` and `Outputs` are content-addressed cached in `.psake/cache/`. If input file hashes haven't changed and output files exist, the task is skipped. + +```powershell +Task 'Build' @{ + DependsOn = 'Clean' + Inputs = 'src/**/*.cs', 'src/**/*.csproj' + Outputs = 'bin/**/*.dll' + Action = { exec { dotnet build -c $Configuration } } +} +``` + +Inputs/Outputs also accept scriptblocks for dynamic file resolution: + +```powershell +Task 'Build' @{ + Inputs = { Get-ChildItem src -Recurse -Include *.cs } + Outputs = { Get-ChildItem bin -Recurse -Include *.dll -ErrorAction SilentlyContinue } + Action = { exec { dotnet build } } +} +``` + +Use `Clear-PsakeCache` to force a full rebuild, or `Invoke-psake -NoCache` for a single run. + +#### Conditional Execution + +```powershell +Task Deploy -precondition { $env:CI -eq 'true' } -description "Deploy to prod" { + exec { ./deploy.ps1 } +} +``` + +### Properties + +Variables available to all tasks. Can be overridden via `-properties` parameter. + +```powershell +# Scriptblock syntax +Properties { + $Configuration = 'Release' + $Version = '1.0.0' +} + +# Hashtable syntax (v5) +Properties @{ + Configuration = 'Release' + Version = '1.0.0' +} +``` + +Override: `Invoke-psake -properties @{ Configuration = 'Debug' }` + +### Version + +Pin your build script to a psake major version. The compile phase rejects version mismatches. + +```powershell +Version 5 +``` + +### exec + +Runs external commands, fails build on non-zero exit: + +```powershell +exec { dotnet build } # Basic +exec { npm install } "npm install failed" # Custom error +exec { nuget restore } -maxRetries 3 # Retry flaky ops +exec { npm test } -workingDirectory './frontend' # Different directory +exec { ./slow-build.ps1 } -TimeoutSeconds 600 # Timeout (v5) +``` + +### Assert + +```powershell +Assert (Test-Path $SrcDir) "Source directory not found" +Assert (-not [string]::IsNullOrEmpty($ApiKey)) "API key required" +``` + +### Include + +```powershell +Include "./shared/common-tasks.ps1" +``` + +### FormatTaskName + +```powershell +FormatTaskName "▶ {0}" +# Or with scriptblock: +FormatTaskName { param($taskName) Write-Host "[$taskName]" -ForegroundColor Cyan } +``` + +### TaskSetup / TaskTearDown + +```powershell +TaskSetup { Write-Host "Starting: $($psake.context.currentTaskName)" } +TaskTearDown { Write-Host "Finished: $($psake.context.currentTaskName)" } +``` + +## Structured Output + +`Invoke-psake` returns a `PsakeBuildResult` object: + +```powershell +$result = Invoke-psake -Quiet +$result.Success # $true / $false +$result.Duration # TimeSpan +$result.Tasks # PsakeTaskResult[] with Name, Status, Duration, Cached +$result.ErrorMessage # Error details if failed +``` + +The `$psake.build_success` variable is still set after each build for backward compatibility. + +For CI pipelines, use JSON output: + +```powershell +Invoke-psake -OutputFormat JSON +``` + +## Invoke-psake Parameters + +| Parameter | Description | +|-----------|-------------| +| `-buildFile` | Path to build script (default: psakefile.ps1) | +| `-taskList` | Tasks to execute (default: 'Default') | +| `-parameters` | Hashtable passed to build script (set before Properties) | +| `-properties` | Hashtable to override Properties block (set after Properties) | +| `-docs` | Display task documentation | +| `-nologo` | Suppress banner | +| `-OutputFormat` | `Default`, `JSON`, or `GitHubActions` (v5) | +| `-NoCache` | Bypass task caching for this run (v5) | +| `-CompileOnly` | Return build plan without executing (v5) | +| `-Quiet` | Suppress console output; still returns PsakeBuildResult (v5) | + +## Testability APIs + +### Inspect the Build Plan + +```powershell +$plan = Get-PsakeBuildPlan -BuildFile './psakefile.ps1' +$plan.ExecutionOrder # ['Clean', 'Build', 'Test', 'Default'] +$plan.TaskMap['build'].DependsOn # ['Clean'] +$plan.IsValid # $true +$plan.ValidationErrors # @() +``` + +The plan can also be piped into `Invoke-psake`: + +```powershell +Get-PsakeBuildPlan | Invoke-psake +``` + +### Test a Task in Isolation + +```powershell +$result = Test-PsakeTask -BuildFile './psakefile.ps1' -TaskName 'Build' -Variables @{ + Configuration = 'Debug' +} +$result.Status # 'Executed' +$result.Duration # TimeSpan +``` + +## Common Patterns + +### Environment-Specific + +```powershell +Properties { + $Env = if ($env:ENVIRONMENT) { $env:ENVIRONMENT } else { 'Development' } +} + +Task Deploy { + switch ($Env) { + 'Production' { exec { ./deploy-prod.ps1 } } + default { Write-Host "Skipping deploy for $Env" } + } +} +``` + +### Multi-Project + +```powershell +Task BuildAll -depends BuildBackend, BuildFrontend + +Task BuildBackend { + Push-Location ./backend + try { exec { dotnet build } } + finally { Pop-Location } +} + +Task BuildFrontend { + Push-Location ./frontend + try { exec { npm run build } } + finally { Pop-Location } +} +``` + +### Variable Scoping Between Tasks + +Tasks don't share local variables. Use `$script:` scope to pass data between dependent tasks: + +```powershell +Task GetFiles { + $script:Files = Get-ChildItem -Path $SrcDir -Filter *.ps1 +} + +Task ProcessFiles -depends GetFiles { + foreach ($file in $script:Files) { + # Process each file + } +} +``` + +> **Note:** psake tasks don't have return values. `$script:` scoped variables are the recommended approach for task-to-task data sharing. + +## Validating a psakefile + +### Using Get-PsakeBuildPlan (recommended) + +The compile phase catches circular dependencies, missing tasks, and version mismatches before any task runs: + +```powershell +$plan = Get-PsakeBuildPlan -BuildFile './psakefile.ps1' +if (-not $plan.IsValid) { + $plan.ValidationErrors | ForEach-Object { Write-Error $_ } +} else { + Write-Host "✓ Build plan valid — execution order: $($plan.ExecutionOrder -join ' → ')" +} +``` + +### Syntax Check + +```powershell +$errors = $null +$null = [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path 'psakefile.ps1'), [ref]$null, [ref]$errors +) +if ($errors) { $errors | ForEach-Object { Write-Error $_.ToString() } } +else { Write-Host "✓ Syntax valid" -ForegroundColor Green } +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Build fails but CI shows success | Use `exec { }` for all external commands | +| Cross-platform path issues | Use `Join-Path` instead of `\` or `/` | +| Module not found in CI | `Install-Module -Name psake -Scope CurrentUser -Force` | +| Properties not overriding | Use `-properties` (not `-parameters`) to override Properties block | +| Variable undefined in dependent task | Use `$script:VarName` to share data between tasks | +| Circular dependency error | Check `Get-PsakeBuildPlan` output for `ValidationErrors` | +| Task skipped unexpectedly | May be cached — run with `-NoCache` or `Clear-PsakeCache` | +| `default.ps1` not found | v5 removed `default.ps1` fallback — rename to `psakefile.ps1` | + +## References + +- **references/upgrading-to-v5.md** - Migration guide, caching for faster builds, structured output, testability APIs +- **references/powershell-modules.md** - PowerShellBuild module for PS module development +- **references/build-types.md** - .NET, Node.js, Docker build patterns +- **references/advanced.md** - Dynamic tasks, CI/CD integration, $psake reference diff --git a/.agents/skills/psake/references/advanced.md b/.agents/skills/psake/references/advanced.md new file mode 100644 index 0000000..83c2eb9 --- /dev/null +++ b/.agents/skills/psake/references/advanced.md @@ -0,0 +1,306 @@ +# Advanced psake Patterns + +## Contents + +- Dynamic Task Generation (from package.json, directories, config files) +- CI/CD Integration (GitHub Actions, Azure Pipelines, GitLab CI) +- Nested Builds +- Error Handling Patterns +- $psake Variable Reference + +## Dynamic Task Generation + +Generate tasks from external sources at runtime. + +### From package.json Scripts + +```powershell +# Read package.json and create tasks for each npm script +$packageJson = Get-Content './package.json' | ConvertFrom-Json +$npmScripts = $packageJson.scripts.PSObject.Properties + +foreach ($script in $npmScripts) { + $taskName = "npm:$($script.Name)" + $scriptName = $script.Name + + Task $taskName -description "Run npm script: $scriptName" { + exec { npm run $scriptName } + }.GetNewClosure() +} + +Task NpmAll -depends ($npmScripts | ForEach-Object { "npm:$($_.Name)" }) +``` + +### From Directory Contents + +```powershell +# Create a task for each project in a monorepo +$projects = Get-ChildItem './packages' -Directory + +foreach ($project in $projects) { + $projectName = $project.Name + $projectPath = $project.FullName + + Task "build:$projectName" -description "Build $projectName" { + Push-Location $projectPath + try { exec { npm run build } } + finally { Pop-Location } + }.GetNewClosure() +} + +Task BuildAll -depends ($projects | ForEach-Object { "build:$($_.Name)" }) +``` + +### From Configuration File + +```powershell +# build-config.psd1 +@{ + Projects = @( + @{ Name = 'Api'; Path = './src/Api'; Type = 'dotnet' } + @{ Name = 'Web'; Path = './src/Web'; Type = 'npm' } + ) +} +``` + +```powershell +$config = Import-PowerShellDataFile './build-config.psd1' + +foreach ($project in $config.Projects) { + $name = $project.Name + $path = $project.Path + $type = $project.Type + + Task "build:$name" { + Push-Location $path + try { + switch ($type) { + 'dotnet' { exec { dotnet build } } + 'npm' { exec { npm run build } } + } + } + finally { Pop-Location } + }.GetNewClosure() +} +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Build + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Cache PowerShell modules + uses: actions/cache@v4 + with: + path: | + ~/.local/share/powershell/Modules + ~/Documents/PowerShell/Modules + key: ${{ runner.os }}-psake + + - name: Install psake + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module psake -Scope CurrentUser -Force + + - name: Build and Test + shell: pwsh + run: Invoke-psake -taskList Build, Test -OutputFormat GitHubActions + + - name: Publish + if: github.ref == 'refs/heads/main' && matrix.os == 'ubuntu-latest' + shell: pwsh + run: Invoke-psake -taskList Publish + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} +``` + +`-OutputFormat GitHubActions` emits `::error::`, `::warning::`, and `::debug::` annotations that show inline in PR diffs. + +### Azure Pipelines + +```yaml +trigger: + branches: + include: [main] + +pool: + vmImage: 'windows-latest' + +variables: + - group: BuildSecrets + +stages: + - stage: Build + jobs: + - job: BuildJob + steps: + - pwsh: | + Install-Module psake -Scope CurrentUser -Force + $psakeArgs = @{ + taskList = 'Build', 'Test' + parameters = @{ BuildNumber = '$(Build.BuildNumber)' } + Quiet = $true + } + $result = Invoke-psake @psakeArgs + if (-not $result.Success) { exit 1 } + displayName: 'Build and Test' + + - publish: $(System.DefaultWorkingDirectory)/build + artifact: BuildOutput + + - stage: Deploy + condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') + jobs: + - deployment: DeployJob + environment: production + strategy: + runOnce: + deploy: + steps: + - pwsh: Invoke-psake -taskList Publish + env: + NUGET_API_KEY: $(NuGetApiKey) +``` + +### GitLab CI/CD + +```yaml +image: mcr.microsoft.com/powershell:latest + +stages: + - build + - test + - deploy + +variables: + PSMODULE_CACHE: "$CI_PROJECT_DIR/.psmodules" + +cache: + key: psake-modules + paths: + - .psmodules/ + +before_script: + - pwsh -c "Install-Module psake -Scope CurrentUser -Force" + +build: + stage: build + script: + - pwsh -c "Invoke-psake -taskList Build" + artifacts: + paths: + - build/ + +test: + stage: test + script: + - pwsh -c "Invoke-psake -taskList Test" + artifacts: + reports: + junit: TestResults/*.xml + +deploy: + stage: deploy + script: + - pwsh -c "Invoke-psake -taskList Publish" + environment: + name: production + only: + - main + when: manual +``` + +## Nested Builds + +Call psake from within a task: + +```powershell +Task BuildSubProject { + $result = Invoke-psake -buildFile './subproject/psakefile.ps1' -taskList Build -Quiet + + if (-not $result.Success) { + throw "Subproject build failed: $($result.ErrorMessage)" + } +} +``` + +## Error Handling Patterns + +### Continue on Error + +```powershell +Task OptionalCleanup -continueOnError { + Remove-Item './temp' -Recurse -Force -ErrorAction Stop +} +``` + +### Custom Error Recovery + +```powershell +Task DeployWithRollback -depends Build { + $deployed = $false + try { + exec { ./deploy.ps1 } + $deployed = $true + } catch { + if ($deployed) { + Write-Host "Deployment failed, rolling back..." + exec { ./rollback.ps1 } + } + throw + } +} +``` + +### Retry with Backoff + +```powershell +Task FlakyOperation { + $maxRetries = 3 + $retryDelay = 5 + + for ($i = 1; $i -le $maxRetries; $i++) { + try { + exec { ./flaky-script.ps1 } + break + } catch { + if ($i -eq $maxRetries) { throw } + Write-Host "Attempt $i failed, retrying in ${retryDelay}s..." + Start-Sleep -Seconds $retryDelay + $retryDelay *= 2 + } + } +} +``` + +## $psake Variable Reference + +Access build context: + +```powershell +$psake.build_script_file # Full path to current build script +$psake.build_script_dir # Directory of build script +$psake.version # psake version +$psake.context # Current execution context +$psake.context.currentTaskName # Name of executing task +$psake.build_success # $true if build succeeded (check after Invoke-psake) +``` diff --git a/.agents/skills/psake/references/build-types.md b/.agents/skills/psake/references/build-types.md new file mode 100644 index 0000000..b22c1d3 --- /dev/null +++ b/.agents/skills/psake/references/build-types.md @@ -0,0 +1,264 @@ +# Build Type Patterns + +## Contents + +- .NET Projects (Modern SDK-style, Legacy .NET Framework) +- Node.js Projects (npm, TypeScript) +- Docker Builds (Basic, Compose) +- Multi-Technology Stack + +## .NET Projects + +### Modern .NET (SDK-style) + +```powershell +Version 5 + +Properties @{ + SrcDir = Join-Path $PSScriptRoot 'src' + BuildDir = Join-Path $PSScriptRoot 'build' + Configuration = 'Release' + Version = '1.0.0' +} + +Task Default -depends Test + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } + New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null +} + +Task Restore { + exec { dotnet restore $SrcDir } +} + +Task 'Build' @{ + DependsOn = 'Clean', 'Restore' + Inputs = 'src/**/*.cs', 'src/**/*.csproj' + Outputs = 'build/**/*.dll' + Action = { exec { dotnet build $SrcDir -c $Configuration -o $BuildDir --no-restore /p:Version=$Version } } +} + +Task Test -depends Build { + exec { + dotnet test $SrcDir -c $Configuration --no-build ` + --logger "trx;LogFileName=results.trx" ` + --results-directory "$BuildDir/TestResults" + } +} + +Task Pack -depends Test { + exec { dotnet pack $SrcDir -c $Configuration -o $BuildDir --no-build /p:Version=$Version } +} + +Task Publish -depends Pack { + $apiKey = $env:NUGET_API_KEY + Assert (-not [string]::IsNullOrEmpty($apiKey)) "NUGET_API_KEY required" + + Get-ChildItem "$BuildDir/*.nupkg" | ForEach-Object { + exec { dotnet nuget push $_.FullName --api-key $apiKey --source nuget.org } + } +} +``` + +### Legacy .NET Framework (MSBuild) + +```powershell +Framework "4.7.2" + +Properties { + $Solution = Join-Path $PSScriptRoot 'MySolution.sln' + $BuildDir = Join-Path $PSScriptRoot 'build' +} + +Task Default -depends Build + +Task Clean { + exec { msbuild $Solution /t:Clean /p:Configuration=Release /v:minimal } +} + +Task Build -depends Clean { + exec { msbuild $Solution /t:Build /p:Configuration=Release /p:OutDir=$BuildDir /v:minimal } +} +``` + +> **Note:** psake v5 requires Framework 4.0 or higher. The default is 4.7.2. + +## Node.js Projects + +### Basic npm Build + +```powershell +Properties { + $ProjectDir = $PSScriptRoot + $BuildDir = Join-Path $ProjectDir 'dist' +} + +Task Default -depends Test + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } +} + +Task Install { + if ($env:CI) { + exec { npm ci } + } else { + exec { npm install } + } +} + +Task Lint -depends Install { + exec { npm run lint } +} + +Task Build -depends Install, Clean { + exec { npm run build } +} + +Task Test -depends Build { + exec { npm test } +} + +Task Publish -depends Test { + $token = $env:NPM_TOKEN + Assert (-not [string]::IsNullOrEmpty($token)) "NPM_TOKEN required" + + exec { npm config set //registry.npmjs.org/:_authToken $token } + try { + exec { npm publish --access public } + } finally { + exec { npm config delete //registry.npmjs.org/:_authToken } + } +} +``` + +### TypeScript + +```powershell +Task TypeCheck -depends Install { + exec { npx tsc --noEmit } +} + +Task Build -depends TypeCheck, Clean { + exec { npx tsc } +} +``` + +## Docker Builds + +### Basic Docker + +```powershell +Properties { + $ImageName = 'myapp' + $ImageTag = if ($env:BUILD_NUMBER) { "1.0.$env:BUILD_NUMBER" } else { 'latest' } + $Registry = $env:DOCKER_REGISTRY +} + +Task Default -depends Build + +Task Verify { + exec { docker --version } | Out-Null + Assert (Test-Path 'Dockerfile') "Dockerfile not found" +} + +Task Build -depends Verify { + exec { docker build -t "${ImageName}:${ImageTag}" . } +} + +Task Run -depends Build { + exec { docker run -d -p 8080:80 --name $ImageName "${ImageName}:${ImageTag}" } +} + +Task Stop { + docker stop $ImageName 2>$null + docker rm $ImageName 2>$null +} + +Task Push -depends Build { + Assert (-not [string]::IsNullOrEmpty($Registry)) "DOCKER_REGISTRY required" + Assert (-not [string]::IsNullOrEmpty($env:DOCKER_TOKEN)) "DOCKER_TOKEN required" + + $fullTag = "${Registry}/${ImageName}:${ImageTag}" + + $env:DOCKER_TOKEN | docker login $Registry -u $env:DOCKER_USERNAME --password-stdin + exec { docker tag "${ImageName}:${ImageTag}" $fullTag } + exec { docker push $fullTag } +} +``` + +### Docker Compose + +```powershell +Properties { + $ComposeFile = Join-Path $PSScriptRoot 'docker-compose.yml' + $ProjectName = 'myapp' +} + +Task Up { + $env:COMPOSE_PROJECT_NAME = $ProjectName + exec { docker compose -f $ComposeFile up -d } +} + +Task Down { + $env:COMPOSE_PROJECT_NAME = $ProjectName + exec { docker compose -f $ComposeFile down } +} + +Task Logs { + $env:COMPOSE_PROJECT_NAME = $ProjectName + exec { docker compose -f $ComposeFile logs -f } +} +``` + +## Multi-Technology Stack + +```powershell +Properties { + $BackendDir = Join-Path $PSScriptRoot 'backend' + $FrontendDir = Join-Path $PSScriptRoot 'frontend' + $BuildDir = Join-Path $PSScriptRoot 'build' +} + +Task Default -depends BuildAll + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } + New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null +} + +Task BuildBackend -depends Clean { + Push-Location $BackendDir + try { + exec { dotnet build -c Release -o "$BuildDir/api" } + } finally { + Pop-Location + } +} + +Task BuildFrontend -depends Clean { + Push-Location $FrontendDir + try { + exec { npm ci } + exec { npm run build } + Copy-Item ./dist/* "$BuildDir/web" -Recurse + } finally { + Pop-Location + } +} + +Task BuildAll -depends BuildBackend, BuildFrontend + +Task TestAll { + Push-Location $BackendDir + try { exec { dotnet test } } finally { Pop-Location } + + Push-Location $FrontendDir + try { exec { npm test } } finally { Pop-Location } +} + +Task DockerBuild -depends BuildAll { + exec { docker build -t myapp:latest . } +} +``` diff --git a/.agents/skills/psake/references/powershell-modules.md b/.agents/skills/psake/references/powershell-modules.md new file mode 100644 index 0000000..fc4bbf7 --- /dev/null +++ b/.agents/skills/psake/references/powershell-modules.md @@ -0,0 +1,230 @@ +# PowerShell Module Builds + +## Contents + +- Quick Start +- Available Tasks (Primary, Secondary) +- Configuration ($PSBPreference) +- Modifying Task Dependencies +- Complete Example (project structure, build.ps1, requirements.psd1, psakeFile.ps1) +- CI/CD Integration +- Troubleshooting + +For PowerShell module development, use **PowerShellBuild** - a collection of pre-built psake tasks. + +## Quick Start + +```powershell +# Install +Install-Module -Name PowerShellBuild -Scope CurrentUser -Force +Install-Module -Name psake -Scope CurrentUser -Force +``` + +### Minimal psakeFile.ps1 + +```powershell +properties { + $PSBPreference.Test.ScriptAnalysis.Enabled = $true + $PSBPreference.Test.CodeCoverage.Enabled = $false +} + +task default -depends Test + +task Test -FromModule PowerShellBuild +task Publish -FromModule PowerShellBuild +``` + +This single file gives you: Init → Clean → StageFiles → BuildHelp → Build → Analyze → Pester → Test → Publish + +## Available Tasks + +### Primary Tasks + +| Task | Dependencies | Description | +|------|--------------|-------------| +| Init | none | Initialize psake and task variables | +| Clean | Init | Clean output directory | +| Build | StageFiles, BuildHelp | Build module in output directory | +| Analyze | Build | Run PSScriptAnalyzer | +| Pester | Build | Run Pester tests | +| Test | Analyze, Pester | Run all tests | +| Publish | Test | Publish to PowerShell repository | + +### Secondary Tasks + +| Task | Description | +|------|-------------| +| StageFiles | Copy module files to output | +| GenerateMarkdown | Build PlatyPS markdown help | +| GenerateMAML | Build MAML help from markdown | +| BuildHelp | Build all help files | + +## Configuration ($PSBPreference) + +Override in your psakeFile.ps1 `properties` block: + +### Build Settings + +```powershell +$PSBPreference.General.ModuleName = 'MyModule' +$PSBPreference.General.SrcRootDir = './src' +$PSBPreference.Build.OutDir = './Output' +$PSBPreference.Build.CompileModule = $true # Combine into single PSM1 +$PSBPreference.Build.CompileDirectories = @('Enum', 'Classes', 'Private', 'Public') +$PSBPreference.Build.CopyDirectories = @('Data') # Copy as-is +``` + +### Test Settings + +```powershell +$PSBPreference.Test.Enabled = $true +$PSBPreference.Test.RootDir = './tests' +$PSBPreference.Test.ScriptAnalysis.Enabled = $true +$PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel = 'Error' +$PSBPreference.Test.CodeCoverage.Enabled = $true +$PSBPreference.Test.CodeCoverage.Threshold = 0.75 +``` + +### Publish Settings + +```powershell +$PSBPreference.Publish.PSRepository = 'PSGallery' +$PSBPreference.Publish.PSRepositoryApiKey = $env:PSGALLERY_API_KEY +``` + +## Modifying Task Dependencies + +Set before referencing PowerShellBuild tasks: + +```powershell +# Skip help generation +$PSBBuildDependency = 'StageFiles' + +# Skip analysis in Test +$PSBTestDependency = 'Pester' + +# Publish without testing (not recommended) +$PSBPublishDependency = 'Build' +``` + +## Complete Example + +### Project Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.psd1 +│ ├── MyModule.psm1 +│ ├── Private/ +│ │ └── HelperFunction.ps1 +│ └── Public/ +│ └── Get-Something.ps1 +├── tests/ +│ └── MyModule.Tests.ps1 +├── docs/ +├── build.ps1 +├── psakeFile.ps1 +└── requirements.psd1 +``` + +### build.ps1 (Entry Point) + +```powershell +[cmdletbinding(DefaultParameterSetName = 'Task')] +param( + [parameter(ParameterSetName = 'Task', position = 0)] + [string[]]$Task = 'default', + + [switch]$Bootstrap, + + [parameter(ParameterSetName = 'Help')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +if ($Bootstrap.IsPresent) { + Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + if (-not (Get-Module -Name PSDepend -ListAvailable)) { + Install-Module -Name PSDepend -Repository PSGallery -Scope CurrentUser + } + Import-Module -Name PSDepend -Verbose:$false + Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue +} + +$psakeFile = './psakeFile.ps1' +if ($PSCmdlet.ParameterSetName -eq 'Help') { + Get-PSakeScriptTasks -buildFile $psakeFile | Format-Table -Property Name, Description +} else { + Set-BuildEnvironment -Force + Invoke-psake -buildFile $psakeFile -taskList $Task -Verbose:$VerbosePreference + exit ([int](-not $psake.build_success)) +} +``` + +### requirements.psd1 + +```powershell +@{ + psake = '5.0.0' + PowerShellBuild = '0.7.0' + Pester = '5.6.1' + PSScriptAnalyzer = '1.24.0' + PlatyPS = '0.14.2' +} +``` + +### psakeFile.ps1 + +```powershell +properties { + # Override defaults + $PSBPreference.Build.CompileModule = $true + $PSBPreference.Test.CodeCoverage.Enabled = $true + $PSBPreference.Test.CodeCoverage.Threshold = 0.80 +} + +task default -depends Test + +task Test -FromModule PowerShellBuild +task Build -FromModule PowerShellBuild +task Publish -FromModule PowerShellBuild +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build and Test + shell: pwsh + run: ./build.ps1 -Task Test + + - name: Publish + if: github.ref == 'refs/heads/main' + shell: pwsh + run: ./build.ps1 -Task Publish + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| "Task 'Build' not found" | Ensure psake ≥ 4.8.0 for `-FromModule` support | +| BuildHelp fails | Install PlatyPS: `Install-Module PlatyPS` | +| Tests not running | Check `$PSBPreference.Test.RootDir` path | +| ScriptAnalyzer fails | Create `ScriptAnalyzerSettings.psd1` or disable | diff --git a/.agents/skills/psake/references/upgrading-to-v5.md b/.agents/skills/psake/references/upgrading-to-v5.md new file mode 100644 index 0000000..7156c3d --- /dev/null +++ b/.agents/skills/psake/references/upgrading-to-v5.md @@ -0,0 +1,314 @@ +# Upgrading to psake v5 + +## Contents + +- Quick Compatibility Check +- Breaking Changes +- Step-by-Step Migration +- Speed Up Builds with Caching +- Structured Output for CI +- Testability APIs +- Before/After Examples + +## Quick Compatibility Check + +Most v4 build scripts work in v5 without changes. Check these three things: + +1. **Build file name**: If using `default.ps1`, rename to `psakefile.ps1` +2. **Runner scripts**: If calling `psake.ps1` or `psake.cmd`, switch to `Import-Module psake; Invoke-psake` +3. **Framework**: If using `Framework '3.5'` or lower, update to `'4.0'` or higher + +If none of these apply, your build script already works on v5. + +## Breaking Changes + +| What changed | v4 | v5 | Action needed | +|---|---|---|---| +| Min PowerShell | 3.0 | 5.1 | Upgrade PowerShell | +| Build file discovery | `default.ps1` fallback | `psakefile.ps1` only | Rename file | +| Runner scripts | `psake.ps1`, `psake.cmd` | Removed | Use `Invoke-psake` | +| .NET Framework | 1.0–4.8 | 4.0–4.8 only | Update `Framework` calls | +| `$framework` global | Set framework version | Removed | Use `Framework '4.7.2'` function | +| Output handlers | `psake-config.ps1` overrides | Removed | Use `-OutputFormat` parameter | +| Return value | Nothing (check `$psake.build_success`) | `PsakeBuildResult` object | Optional — `$psake.build_success` still works | + +## Step-by-Step Migration + +### 1. Add Version Declaration (optional but recommended) + +```powershell +Version 5 + +Properties { ... } +Task Default -depends Build +``` + +This pins the build script to psake v5 and gives a clear error if someone runs it with v4. + +### 2. Rename default.ps1 + +```powershell +# If your build file is named default.ps1: +Rename-Item default.ps1 psakefile.ps1 +``` + +### 3. Update Runner Scripts + +```powershell +# Before (v4) +& ./psake.ps1 Build + +# After (v5) +Import-Module psake +Invoke-psake -taskList Build +``` + +### 4. Replace Output Handler Customization + +```powershell +# Before: psake-config.ps1 +$config.outputHandlers.writeOutput = { param($message, $type) ... } + +# After: Use built-in output formats +Invoke-psake -OutputFormat GitHubActions # CI annotations +Invoke-psake -OutputFormat JSON # Machine-readable +Invoke-psake -Quiet # Suppress output, still returns result +``` + +To suppress colored output, set the `NO_COLOR` environment variable. + +### 5. Update CI Scripts That Check Build Success + +```powershell +# Before — still works, no change needed +Invoke-psake +if (!$psake.build_success) { exit 1 } + +# After — cleaner with structured result +$result = Invoke-psake -Quiet +if (-not $result.Success) { + Write-Error $result.ErrorMessage + exit 1 +} +``` + +## Speed Up Builds with Caching + +The biggest performance win in v5 is file-based task caching. Tasks with `Inputs` and `Outputs` are skipped when their input files haven't changed. + +### Adding Caching to Existing Tasks + +Identify tasks that transform files (compile, transpile, copy) and add `Inputs`/`Outputs`: + +```powershell +# Before: runs every time +Task Build -depends Clean { + exec { dotnet build -c $Configuration } +} + +# After: skipped when source hasn't changed +Task 'Build' @{ + DependsOn = 'Clean' + Inputs = 'src/**/*.cs', 'src/**/*.csproj' + Outputs = 'bin/**/*.dll' + Action = { exec { dotnet build -c $Configuration } } +} +``` + +psake computes a SHA256 hash of all input files plus the Action scriptblock text. On repeat runs, if the hash matches and outputs exist, the task prints "Skipped (cached)" and moves on. + +### Good Candidates for Caching + +| Task type | Inputs | Outputs | +|-----------|--------|---------| +| .NET build | `src/**/*.cs`, `*.csproj` | `bin/**/*.dll` | +| npm build | `src/**/*.ts`, `package-lock.json` | `dist/**/*` | +| SASS/CSS | `styles/**/*.scss` | `dist/**/*.css` | +| Docker build | `Dockerfile`, `src/**/*` | (skip — Docker has its own cache) | +| Tests | Don't cache — tests should always run | | +| Clean | Don't cache — always runs | | + +### Dynamic File Lists + +When glob patterns aren't enough, use scriptblocks: + +```powershell +Task 'Build' @{ + Inputs = { + Get-ChildItem src -Recurse -Include *.cs | + Where-Object { $_.Name -notmatch '\.generated\.' } + } + Outputs = { + Get-ChildItem bin -Recurse -Include *.dll -ErrorAction SilentlyContinue + } + Action = { exec { dotnet build } } +} +``` + +### Cache Management + +```powershell +# Force full rebuild (single run) +Invoke-psake -NoCache + +# Clear all cached state +Clear-PsakeCache + +# Clear cache for one task +Clear-PsakeCache -TaskName 'Build' +``` + +Cache files live in `.psake/cache/` — add `.psake/` to your `.gitignore`. + +### Verifying Cache Hits + +The Build Time Report now shows a `Cached` column: + +``` +Build Time Report +---------------------------------------------------------------------- +Name Duration Cached +---- -------- ------ +Clean 00:00:00.012 False +Build 00:00:00.001 True ← skipped, served from cache +Test 00:00:01.340 False +Total: 00:00:01.353 +``` + +Or use `-OutputFormat JSON` and check the `Cached` property on each task result. + +## Structured Output for CI + +### PsakeBuildResult + +`Invoke-psake` now returns a `PsakeBuildResult`: + +```powershell +$result = Invoke-psake -Quiet + +$result.Success # $true / $false +$result.Duration # [TimeSpan] +$result.BuildFile # Path to build script +$result.ErrorMessage # Error details if failed +$result.Tasks # PsakeTaskResult[] array +``` + +Each task result contains: + +```powershell +$result.Tasks[0].Name # 'Build' +$result.Tasks[0].Status # 'Executed', 'Skipped', 'Failed', 'Cached' +$result.Tasks[0].Duration # [TimeSpan] +$result.Tasks[0].Cached # $true / $false +``` + +### JSON Output + +```powershell +# Pipe JSON to a file for CI artifacts +Invoke-psake -OutputFormat JSON > build-result.json +``` + +### GitHub Actions Annotations + +```powershell +Invoke-psake -OutputFormat GitHubActions +``` + +Errors and warnings appear as inline annotations on the PR diff. + +## Testability APIs + +### Validate a Build Plan Without Running + +```powershell +$plan = Get-PsakeBuildPlan -BuildFile './psakefile.ps1' + +# Check structure +$plan.IsValid # $true +$plan.ValidationErrors # @() — empty if valid +$plan.ExecutionOrder # @('Clean', 'Build', 'Test', 'Default') +$plan.TaskMap # Hashtable of task name → task object + +# Inspect dependencies +$plan.TaskMap['build'].DependsOn # @('Clean') +``` + +This catches circular dependencies, missing tasks, and version mismatches at compile time — before any task runs. + +### Test a Single Task in Isolation + +```powershell +$result = Test-PsakeTask -TaskName 'Build' -Variables @{ + Configuration = 'Debug' + OutputDir = './test-output' +} +$result.Status # 'Executed' +$result.Duration # TimeSpan +``` + +Dependencies are NOT executed — only the named task's Action runs. This enables unit-testing individual tasks in Pester. + +### Compile-Only Mode + +```powershell +$plan = Invoke-psake -CompileOnly +# Same as Get-PsakeBuildPlan but through the Invoke-psake entry point +``` + +## Before/After Examples + +### Full v4 → v5 Upgrade + +**Before (v4):** + +```powershell +Properties { + $Configuration = 'Release' + $BuildDir = './build' +} + +Task Default -depends Test + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } +} + +Task Build -depends Clean { + exec { dotnet build -c $Configuration -o $BuildDir } +} + +Task Test -depends Build { + exec { dotnet test } +} +``` + +**After (v5 with caching and structured output):** + +```powershell +Version 5 + +Properties @{ + Configuration = 'Release' + BuildDir = './build' +} + +Task Default -depends Test + +Task Clean { + if (Test-Path $BuildDir) { Remove-Item $BuildDir -Recurse -Force } +} + +Task 'Build' @{ + DependsOn = 'Clean' + Inputs = 'src/**/*.cs', 'src/**/*.csproj' + Outputs = 'bin/**/*.dll' + Action = { exec { dotnet build -c $Configuration -o $BuildDir } } +} + +Task Test -depends Build { + exec { dotnet test } +} +``` + +The second version skips the Build task entirely when source files haven't changed — in a typical edit-test loop this cuts build time significantly. diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..06c5858 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# +# Keep review attention on this repository's own code. +# +# Two directories here hold content that is copied in from elsewhere, and a +# finding against either of them cannot be fixed here -- the fix has to land +# upstream and arrive on the next sync. Reviewing them downstream produces +# comments nobody can action, multiplied by every repository in the fleet: one +# AIM sync generated 115 inline comments, of which 109 were against copied +# content and 6 against anything this repository actually owns. +# +# Only exclusions are listed. Adding an inclusion pattern would turn this into +# an allowlist and silently stop everything else from being reviewed. +reviews: + path_filters: + # Agent Skills vendored verbatim from psake/psake-llm-tools, pinned in + # aim.config.json. .agents/skills/NOTICE.md and step 7.2 of + # instructions/update.instructions.md both say never to edit these in place + # -- re-sync from upstream instead -- so a review comment here is an + # instruction to do the one thing that is not allowed. Their own repository + # maintains them. + # + # The skill folders only. .agents/skills/NOTICE.md is ours: we write the + # attribution, and a review of it caught the license link still pointing at + # a moving branch while the skills were pinned to a tag. + - "!.agents/skills/psake/**" + - "!.agents/skills/powershellbuild/**" + + # Instruction modules synced from tablackburn/ai-agent-instruction-modules. + # That repository has CodeRabbit installed and reviews every human PR, so + # this content is already reviewed where it lives and where it can be + # changed. Listed file by file on purpose: repository-specific.instructions.md + # is NEVER copied from upstream, is genuinely local, and stays reviewed. + - "!instructions/agent-workflow.instructions.md" + - "!instructions/contributing.instructions.md" + - "!instructions/git-workflow.instructions.md" + - "!instructions/github-cli.instructions.md" + - "!instructions/markdown.instructions.md" + - "!instructions/powershell.instructions.md" + - "!instructions/readme.instructions.md" + - "!instructions/releases.instructions.md" + - "!instructions/shorthand.instructions.md" + - "!instructions/testing.instructions.md" + - "!instructions/update.instructions.md" diff --git a/AGENTS.md b/AGENTS.md index 4209af4..ff38cf9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,3 +56,21 @@ This is a PowerShell module project following standard conventions for: ## Instructions Directory See the `instructions/` folder for detailed guidance on specific topics. + +## Skill Dependencies + +This repository vendors Agent Skills (the open [Agent Skills](https://agentskills.io) `SKILL.md` +standard) under `.agents/skills/` - the cross-client convention - so they travel with the +repository and any agent can use them. Provenance and pinned versions are recorded in +`aim.config.json` under `skills`. + +| Skill | Location | Use for | +| ----------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `psake` | `.agents/skills/psake/SKILL.md` | Authoring and troubleshooting psake build scripts (`build.psake.ps1`, tasks, dependencies) | +| `powershellbuild` | `.agents/skills/powershellbuild/SKILL.md` | PowerShellBuild module build/test/publish (`build.ps1`, PSBPreference, Pester, PSScriptAnalyzer) | + +These skills are routed from the Instruction Applicability Matrix above. Because Claude Code reads +`CLAUDE.md` rather than `AGENTS.md`, the repository's `CLAUDE.md` imports this file (`@AGENTS.md`) +to carry the routing into Claude Code. The skills are vendored from `psake/psake-llm-tools` (MIT) +at the version pinned in `aim.config.json`; re-sync from upstream rather than editing the vendored +copies. See `.agents/skills/NOTICE.md` for attribution. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..36899a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +@AGENTS.md + +# Claude Code Instructions + + diff --git a/aim.config.json b/aim.config.json index 74587c5..d6312e5 100644 --- a/aim.config.json +++ b/aim.config.json @@ -26,5 +26,27 @@ "description": "Community-contributed instructions from GitHub's awesome-copilot repository" } ] + }, + "skills": { + "enabled": true, + "vendorPath": ".agents/skills", + "dependencies": [ + { + "name": "psake", + "source": "psake/psake-llm-tools", + "path": "plugins/psake/skills/psake", + "version": "v2.2.0", + "format": "skill-md", + "description": "psake build authoring and patterns (Agent Skill, agentskills.io)" + }, + { + "name": "powershellbuild", + "source": "psake/psake-llm-tools", + "path": "plugins/powershellbuild/skills/powershellbuild", + "version": "v2.2.0", + "format": "skill-md", + "description": "PowerShell module build/test/publish via PowerShellBuild (Agent Skill)" + } + ] } } diff --git a/instructions/agent-workflow.instructions.md b/instructions/agent-workflow.instructions.md index 3ce2032..efc3f66 100644 --- a/instructions/agent-workflow.instructions.md +++ b/instructions/agent-workflow.instructions.md @@ -72,8 +72,8 @@ This builds trust and catches misunderstandings early. ## When in Doubt 1. **Ask for clarification** - Better to ask than implement incorrectly -1. **Check existing code** - Follow established patterns in the codebase -1. **Keep it simple** - The simplest solution that works is usually best +2. **Check existing code** - Follow established patterns in the codebase +3. **Keep it simple** - The simplest solution that works is usually best ## Post-Task Protocol diff --git a/instructions/contributing.instructions.md b/instructions/contributing.instructions.md index c1e407d..3c03a57 100644 --- a/instructions/contributing.instructions.md +++ b/instructions/contributing.instructions.md @@ -56,7 +56,7 @@ Follow existing patterns in the repository: **For new instruction files:** -- Place in `instructions/` folder +- Place in `instruction-templates/` folder - Use `.instructions.md` extension - Include required YAML frontmatter diff --git a/instructions/git-workflow.instructions.md b/instructions/git-workflow.instructions.md index 7e1fdb1..9c3d96c 100644 --- a/instructions/git-workflow.instructions.md +++ b/instructions/git-workflow.instructions.md @@ -7,39 +7,81 @@ description: 'Git workflow conventions including branching, commits, and pull re Guidelines for consistent Git usage across repositories. +## Working on Branches + +**Agents must always work on branches, never directly on main.** + +Before starting any work: + +1. Create a branch from `main` using the naming conventions below +2. Make changes in small, logical commits +3. Push the branch and create a pull request +4. Wait for CI checks and address any review feedback +5. Report status and wait for instructions before merging + +This ensures all changes go through review and CI validation before reaching the main branch. + ## Branch Naming -Use descriptive, lowercase branch names with hyphens: +Use descriptive, lowercase branch names with hyphens. + +### Basic Format ```text / ``` -**Types:** +### Format with Ticket Numbers + +When using project management tools, include the ticket identifier: + +```text +/- +``` + +### Branch Types -- `feature/` - New functionality -- `fix/` - Bug fixes -- `docs/` - Documentation only -- `refactor/` - Code restructuring -- `test/` - Adding or updating tests -- `chore/` - Maintenance tasks +| Prefix | Purpose | Example | +| ----------- | ------------------------------------ | ------------------------------------ | +| `feature/` | New functionality | `feature/user-authentication` | +| `bugfix/` | Bug fixes | `bugfix/login-validation-error` | +| `hotfix/` | Urgent production patches | `hotfix/security-vulnerability` | +| `release/` | Release preparation | `release/v1.2.0` | +| `docs/` | Documentation only | `docs/api-documentation` | +| `refactor/` | Code restructuring | `refactor/database-queries` | +| `test/` | Adding or updating tests | `test/payment-integration` | +| `chore/` | Maintenance tasks | `chore/update-dependencies` | -**Examples:** +### Examples with Ticket Numbers ```text -feature/user-authentication -fix/login-validation-error -docs/api-documentation -refactor/database-queries -test/payment-integration -chore/update-dependencies +feature/PROJ-123-add-user-authentication +bugfix/PROJ-456-fix-login-validation +hotfix/PROJ-789-patch-security-issue ``` -**Avoid:** +### Best Practices + +- **Be descriptive**: Names should reflect the branch's purpose or task +- **Be concise**: Keep names brief but meaningful +- **Be consistent**: Follow the same conventions across the team +- **Use lowercase**: Avoid mixed case for cross-platform compatibility +- **Use hyphens**: Separate words with hyphens, not underscores or spaces + +### Technical Constraints + +Avoid the following in branch names: + +- Dots at the start of the name +- Trailing slashes +- Reserved Git names (`HEAD`, `FETCH_HEAD`) +- Spaces or special characters (except hyphens and forward slashes) + +### Avoid -- Spaces or special characters - Overly long names - Generic names like `fix`, `update`, `changes` +- Names without context or purpose ## Commit Messages @@ -94,9 +136,9 @@ asdfasdf ### Before Creating a PR 1. Ensure your branch is up to date with the base branch -1. Run tests locally and verify they pass -1. Review your own changes first -1. Remove debugging code and console logs +2. Run tests locally and verify they pass +3. Review your own changes first +4. Remove debugging code and console logs ### PR Title @@ -140,6 +182,13 @@ None - Large changes should be split into logical commits - If a PR is too large, consider breaking it into smaller PRs +### After Creating a PR + +1. **Monitor CI**: Wait for CI checks to complete and verify they pass +2. **Check for comments**: Review the PR for any feedback or requested changes +3. **Address feedback**: Make additional commits to address review comments +4. **Report status**: Report the PR status to the user and wait for instructions before merging + ## Branching Strategy ### Default Branch @@ -158,10 +207,10 @@ None ### Feature Branches 1. Create feature branch from `main` -1. Make changes in small, logical commits -1. Push branch and create PR -1. After review and approval, merge to `main` -1. Delete feature branch after merge +2. Make changes in small, logical commits +3. Push branch and create PR +4. After review and approval, merge to `main` +5. Delete feature branch after merge ### Keeping Branches Updated diff --git a/instructions/github-cli.instructions.md b/instructions/github-cli.instructions.md index 950373e..347a024 100644 --- a/instructions/github-cli.instructions.md +++ b/instructions/github-cli.instructions.md @@ -164,9 +164,15 @@ gh run view --log-failed ### Creating Releases +Use `--notes-file` (write notes to a temporary file first) rather than `--notes` to avoid +escaping issues with backticks, backslashes, and quotes. For project releases, the rules in +`releases.instructions.md` take precedence over these examples. + ```bash -# Create release from tag -gh release create v1.0.0 --title "Version 1.0.0" --notes "Release notes" +# Create release from tag (write notes to a file first to avoid escaping issues) +printf '## Highlights\n\n- Your release notes here\n' > release-notes.md +gh release create v1.0.0 --title "Version 1.0.0" --notes-file release-notes.md +rm release-notes.md # Create release with auto-generated notes gh release create v1.0.0 --generate-notes diff --git a/instructions/markdown.instructions.md b/instructions/markdown.instructions.md index 90bc1bc..430da58 100644 --- a/instructions/markdown.instructions.md +++ b/instructions/markdown.instructions.md @@ -26,7 +26,7 @@ Consistent Markdown formatting for documentation files. ## Lists - Use `-` for unordered lists -- Use `1.` for ordered lists (let markdown handle numbering) +- Use sequential numbering for ordered lists (`1.`, `2.`, `3.`, etc.) - Use 2 spaces for nested list indentation ```markdown @@ -47,6 +47,8 @@ Text after list. - Always specify language for fenced code blocks - Ensure closing triple backticks are on their own line - No trailing whitespace after closing backticks +- Code inside fenced blocks should follow the conventions of the relevant language's instruction + file (e.g., PowerShell snippets follow `powershell.instructions.md`) ```javascript // JavaScript code here diff --git a/instructions/powershell.instructions.md b/instructions/powershell.instructions.md index 1331430..cd49306 100644 --- a/instructions/powershell.instructions.md +++ b/instructions/powershell.instructions.md @@ -7,16 +7,53 @@ description: 'PowerShell coding standards and best practices' Style rules for PowerShell code based on Microsoft guidelines and community standards. +## Common Mistakes to Avoid + +**IMPORTANT**: These are frequent violations that MUST be avoided: + +1. **Plural nouns in function names** - ALWAYS use singular nouns regardless of how many items the + function returns. Use `Get-User` not `Get-Users`, `Get-Item` not `Get-Items`. + ## Function Structure 1. Always start functions with `[CmdletBinding()]` attribute -1. Always include explicit `param()` block -1. Use `process {}` block when accepting pipeline input -1. For system-modifying cmdlets, use `[CmdletBinding(SupportsShouldProcess)]` -1. Document output types with `[OutputType([TypeName])]` attribute -1. Include comment-based help for all functions +2. Always include explicit `param()` block +3. Use `process {}` block when accepting pipeline input +4. For system-modifying cmdlets, use `[CmdletBinding(SupportsShouldProcess)]` +5. Document output types with `[OutputType([TypeName])]` attribute +6. Include comment-based help for all functions +7. Do not define nested functions inside other functions; define helper functions at module or + script scope ```powershell +# Bad - nested function +function Get-Data { + [CmdletBinding()] + param() + + function Format-Result { + param($Value) + # Helper logic + } + + $result = Get-RawData + Format-Result -Value $result +} + +# Good - separate functions at module/script scope +function Format-Result { + [CmdletBinding()] + [OutputType([psobject])] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [psobject] + $Value + ) + # Helper logic +} + + function Get-Data { [CmdletBinding()] [OutputType([hashtable])] @@ -61,6 +98,7 @@ function Get-Setting { [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] + [ValidateNotNull()] [hashtable] $Configuration ) @@ -81,14 +119,15 @@ function Get-Setting { ## Naming Conventions 1. Use approved PowerShell verbs only (verify with `Get-Verb`) -1. Use singular nouns for function names (`Get-Item` not `Get-Items`) -1. Use PascalCase for function names and parameters -1. Use camelCase for local variables (`$userName`, `$itemCount`) -1. Use descriptive variable names that indicate purpose -1. Use full cmdlet names, never aliases (`Get-Process` not `gps`) +2. Use singular nouns for function names (`Get-Item` not `Get-Items`) +3. Use PascalCase for function names and parameters +4. Use camelCase for local variables (`$userName`, `$itemCount`) +5. Use descriptive variable names that indicate purpose +6. Use full cmdlet names, never aliases (`Get-Process` not `gps`) ```powershell # Good - descriptive variable names +$backupPath = 'C:\Backups' $backupFiles = Get-ChildItem -Path $backupPath -Filter '*.bak' $activeUsers = Get-ADUser -Filter { Enabled -eq $true } @@ -97,12 +136,57 @@ $files = Get-ChildItem -Path $backupPath -Filter '*.bak' $users = Get-ADUser -Filter { Enabled -eq $true } ``` +### Path vs Directory Naming + +Use the appropriate suffix to indicate what the variable holds: + +- Use `Path` for any path string (file or folder) +- Reserve `Directory` for directory objects (e.g., `[System.IO.DirectoryInfo]`) or bare folder names + +```powershell +# Good - Path suffix for path strings +$configurationPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' +$outputPath = Join-Path -Path $PSScriptRoot -ChildPath 'results' +$backupPath = 'C:\Backups' + +# Good - Directory suffix for a directory object +$logDirectory = [System.IO.DirectoryInfo]::new('C:\Logs') + +# Bad - Directory suffix on a path string +$outputDirectory = 'C:\App\results' +``` + ## Parameters -1. Use full parameter names in scripts and functions -1. Always use quotes around string parameter values -1. Include validation on every parameter -1. Place each component on its own line +1. Name parameters on calls that pass two or more arguments; a single-argument call may stay + positional. Naming disambiguates which value maps to which parameter when there are several; + with one argument there is nothing to disambiguate, so naming it only adds noise. +2. Always use quotes around string parameter values +3. Include validation on every parameter +4. Place each component on its own line + +```powershell +# Good - 2+ arguments: name them (no positional guessing) +Get-ChildItem -Path 'C:\Logs' -Filter '*.log' -Recurse +Copy-Item -Path $sourcePath -Destination $destinationPath + +# Good - single argument: positional is fine +Test-Path $configurationPath +Import-Module $modulePath + +# Avoid - naming the only argument adds noise without removing ambiguity +Test-Path -Path $configurationPath +``` + +```powershell +# Good - string parameter values are quoted +Get-Process 'powershell' +Get-ChildItem -Path 'C:\Program Files' -Filter '*.txt' + +# Bad - bare string parameter values +Get-Process powershell +Get-ChildItem -Path C:\Program Files -Filter *.txt +``` ```powershell function Get-UserData { @@ -129,11 +213,11 @@ function Get-UserData { ## Formatting 1. Opening brace `{` at end of line, closing brace `}` on new line -1. Use 4 spaces per indentation level -1. Maximum line length: 115 characters -1. Use splatting for long parameter lists -1. Two blank lines before function definitions -1. One blank line at end of file +2. Use 4 spaces per indentation level +3. Maximum line length: 115 characters +4. Use splatting for long parameter lists +5. Two blank lines before function definitions +6. One blank line at end of file ```powershell function Test-Code { @@ -157,39 +241,95 @@ function Test-Code { } # Good - splatting for readability -$parameters = @{ +$invokeRestMethodParameters = @{ Uri = 'https://api.example.com/endpoint' Method = 'Post' Headers = $headers Body = $body } -Invoke-RestMethod @parameters +Invoke-RestMethod @invokeRestMethodParameters +``` + +## Line Continuation + +1. Do not use backtick (`` ` ``) line continuation +2. Do not use semicolons (`;`) to chain multiple statements on one line +3. Prefer splatting (`@copyItemParameters`) for long parameter lists +4. Use natural continuation inside `()`, `@{}`, or `@()` when grouping expressions or collections +5. Place each hashtable element on its own line in multi-line hashtables +6. Pipelines continue without backticks when the line ends with `|` + +```powershell +# Good - splatting for long parameter lists +$copyItemParameters = @{ + Path = $sourcePath + Destination = $destinationPath + Recurse = $true + Force = $true +} +Copy-Item @copyItemParameters + +# Good - pipeline continues across lines +Get-ChildItem -Path $sourceDirectory -Recurse | + Where-Object { $_.Length -gt 1MB } | + Sort-Object -Property 'Length' -Descending + +# Good - natural continuation inside parentheses +$summaryMessage = ( + "Processed $successCount of $totalCount records. " + + "Skipped $skipCount records. " + + "Encountered $errorCount errors." +) + +# Good - for-loop semicolons are syntactic, not statement chaining +for ($i = 0; $i -lt 10; $i++) { + Write-Output -InputObject $i +} + +# Good - hashtable with each element on its own line +$webRequestOptions = @{ + Name = 'Value' + Size = 100 +} + +# Bad - backtick line continuation +Copy-Item -Path $sourcePath ` + -Destination $destinationPath ` + -Recurse ` + -Force + +# Bad - semicolons chaining statements +Import-Module -Name 'PSReadLine'; Set-PSReadLineOption -EditMode 'Emacs' + +# Bad - hashtable elements chained with semicolons on one line +$webRequestOptions = @{ Name = 'Value'; Size = 100 } ``` ## Paths and File System 1. Use `$PSScriptRoot` for script-relative paths -1. Use `$Env:UserProfile` or `$HOME` instead of `~` -1. Use `Join-Path` to construct paths +2. Use `$Env:UserProfile` or `$HOME` instead of `~` +3. Use `Join-Path` to construct paths ```powershell # Good -$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' -$userPath = Join-Path -Path $Env:UserProfile -ChildPath 'Documents' +$configurationPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json' +$documentsPath = Join-Path -Path $Env:UserProfile -ChildPath 'Documents' # Bad -$configPath = '.\config.json' -$userPath = '~\Documents' +$configurationPath = '.\config.json' +$documentsPath = '~\Documents' ``` ## Error Handling 1. Use `-ErrorAction 'Stop'` for cmdlets within try/catch -1. Immediately copy `$_` in catch blocks before other commands +2. Immediately copy `$_` in catch blocks before other commands ```powershell +$filePath = 'C:\Data\settings.json' try { - Get-Item -Path $path -ErrorAction Stop + Get-Item -Path $filePath -ErrorAction 'Stop' } catch { $errorRecord = $_ # Capture immediately @@ -200,8 +340,8 @@ catch { ## Credential Handling 1. Use `[PSCredential]` for credential parameters, never `[string]` for passwords -1. Make credentials optional when the function can run without them -1. Use `[System.Management.Automation.Credential()]` attribute for flexibility +2. Make credentials optional when the function can run without them +3. Use `[System.Management.Automation.Credential()]` attribute for flexibility ```powershell function Connect-Service { @@ -231,20 +371,20 @@ function Connect-Service { ## Output 1. Write objects to pipeline immediately, don't batch into arrays -1. Use `Write-Verbose` for detailed operation information -1. Use `Write-Warning` for potential issues +2. Use `Write-Verbose` for detailed operation information +3. Use `Write-Warning` for potential issues ```powershell # Good - immediate output foreach ($item in $collection) { - $result = Process-Item $item + $result = Format-Item -InputObject $item $result # Output immediately } # Bad - batching $results = @() foreach ($item in $collection) { - $results += Process-Item $item + $results += Format-Item -InputObject $item } $results ``` @@ -289,8 +429,8 @@ function Get-UserData { ## Quotes 1. Use single quotes for string literals -1. Use double quotes only when variable expansion is needed -1. Quote hashtable keys only when necessary (hyphens, spaces) +2. Use double quotes only when variable expansion is needed +3. Quote hashtable keys only when necessary (hyphens, spaces) ```powershell # Good @@ -306,22 +446,124 @@ $title = 'Static string' ## Spacing 1. Spaces around all operators: `$x = 1 + 2` -1. Spaces around comparison operators: `$value -eq 10` -1. Space after commas and semicolons -1. No trailing spaces +2. Spaces around comparison operators: `$value -eq 10` +3. Space after commas and semicolons +4. No trailing spaces + +## Build Systems -## Semicolons +When a repository uses a build system (psake, Invoke-Build, etc.), use the build system's tasks for +operations like testing, building, publishing, and deployment rather than running commands directly +or creating separate scripts. Check for common build files: -1. Do not use semicolons as line terminators -1. Place each hashtable element on its own line +- `psakefile.ps1` or `psake.ps1` (psake) +- `*.build.ps1` (Invoke-Build) +- `build.ps1` (general build script) ```powershell -# Good -$options = @{ - Name = 'Value' - Size = 100 +# Good - use the build system +Invoke-psake -taskList Test +Invoke-Build -Task Test + +# Avoid - bypassing the build system +Invoke-Pester -Path .\tests\ +``` + +## Static Analysis + +PSScriptAnalyzer warnings indicate real issues. Fix the underlying problem rather than suppressing warnings. + +### Warnings to Always Fix + +These warnings represent naming and style violations that should be corrected: + +- **PSUseSingularNouns** - Rename function to use singular noun (`Get-Item` not `Get-Items`) +- **PSUseApprovedVerbs** - Use an approved verb from `Get-Verb` +- **PSAvoidUsingCmdletAliases** - Replace alias with full cmdlet name +- **PSAvoidUsingWriteHost** - Use `Write-Output`, `Write-Verbose`, or `Write-Information` + +```powershell +# Bad - suppressing instead of fixing +function Get-Items { # PSUseSingularNouns warning + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + param() + # Returns multiple items } -# Bad -$options = @{ Name = 'Value'; Size = 100 } +# Good - fix the naming +function Get-Item { + [CmdletBinding()] + param() + # Returns zero, one, or more items (singular noun is correct regardless) +} +``` + +### Suppression Requirements + +When suppression is genuinely necessary (rare), include a justification: + +1. Use `SuppressMessageAttribute` with the `Justification` parameter +2. Explain why the warning cannot be resolved +3. Reference external constraints if applicable + +```powershell +# Acceptable - justified suppression for API compatibility +function Get-AWSItems { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'Matches AWS SDK naming convention for consistency with existing tooling' + )] + [CmdletBinding()] + param() +} +``` + +### Never Suppress Without Justification + +Suppressions without justification are not acceptable: + +```powershell +# Never do this +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] +``` + +## Pester + +### Skipping Tests + +`Set-ItResult -Skipped` (and `-Inconclusive`) ends the `It` block immediately - it throws an +internal error record that Pester catches and records as the test result. Code after the call +does not run, so a trailing `return` is unreachable dead code; do not add one. Reviewers, +including automated ones, recurrently suggest the redundant `return`. + +```powershell +# Good - Set-ItResult ends the test; nothing after it runs +It 'Validates the required version' { + if (-not $dependency.ContainsKey('RequiredVersion')) { + Set-ItResult -Skipped -Because 'No RequiredVersion to validate' + } + Test-VersionConstraint -Version $dependency.RequiredVersion | Should -BeTrue +} + +# Bad - the return can never execute; Set-ItResult already threw +It 'Validates the required version' { + if (-not $dependency.ContainsKey('RequiredVersion')) { + Set-ItResult -Skipped -Because 'No RequiredVersion to validate' + return + } + Test-VersionConstraint -Version $dependency.RequiredVersion | Should -BeTrue +} +``` + +Prefer `-Skip:$condition` on `It`, `Context`, or `Describe` when the condition is known at +discovery time; reserve `Set-ItResult -Skipped` for conditions only knowable at runtime inside +the test body. + +```powershell +# Good - a discovery-time condition uses the -Skip parameter +It 'Runs only on Windows' -Skip:(-not $IsWindows) { + Get-Service | Should -Not -BeNullOrEmpty +} ``` diff --git a/instructions/releases.instructions.md b/instructions/releases.instructions.md index 17db68a..17aa7c7 100644 --- a/instructions/releases.instructions.md +++ b/instructions/releases.instructions.md @@ -57,28 +57,29 @@ Follow [Semantic Versioning](https://semver.org/): ## Pre-Release Checklist -Before creating a release: +Follow `git-workflow.instructions.md` for branching and PR workflow. The steps below are +release-specific: 1. **Verify current release state**: Run `gh release list --limit 5` to check the most recent releases. Compare the latest released version against the version in CHANGELOG.md. If they match, the changelog version needs to be incremented. If the changelog is already ahead, use that version. NEVER release a version that already exists. -1. **Check repository-specific instructions**: Review `repository-specific.instructions.md` for +2. **Check repository-specific instructions**: Review `repository-specific.instructions.md` for any additional release requirements specific to this repository -1. **Update CHANGELOG.md**: Add new version section with all changes -1. **Update version numbers**: Bump version in relevant files as needed -1. **Update changelog links**: Add comparison link for the new version at the bottom of +3. **Update CHANGELOG.md**: Add new version section with all changes +4. **Update version numbers**: Bump version in relevant files as needed +5. **Update changelog links**: Add comparison link for the new version at the bottom of CHANGELOG.md (e.g., `[0.2.0]: https://github.com/owner/repo/compare/v0.1.0...v0.2.0`) -1. **Run tests**: Ensure all tests pass -1. **Commit changes**: Commit all version updates before creating the release -1. **Push to remote**: Push commits to the repository -1. **Create release**: Use the `gh release create` command with `--notes-file` +6. **Run tests**: Ensure all tests pass +7. **Commit changes**: Commit all version updates +8. **Create PR and wait for merge**: Follow the PR workflow in `git-workflow.instructions.md` +9. **Create release**: After PR is merged, use `gh release create` with `--notes-file` ## Post-Release After creating a release: 1. Verify the release appears correctly on GitHub -1. Check that release notes display properly (no formatting issues) -1. Confirm download links work if applicable -1. Notify team members if this is a significant release +2. Check that release notes display properly (no formatting issues) +3. Confirm download links work if applicable +4. Notify team members if this is a significant release diff --git a/instructions/shorthand.instructions.md b/instructions/shorthand.instructions.md index 77b09cf..de68961 100644 --- a/instructions/shorthand.instructions.md +++ b/instructions/shorthand.instructions.md @@ -37,6 +37,7 @@ use full, descriptive words instead of shorthand or abbreviations. | Err | Error | | Msg | Message | | Conn | Connection / Connections | +| Dir | Directory | | Cmd | Command | | Svc | Service | | Cfg | Configuration | diff --git a/instructions/testing.instructions.md b/instructions/testing.instructions.md index ce68dd4..4753984 100644 --- a/instructions/testing.instructions.md +++ b/instructions/testing.instructions.md @@ -7,6 +7,17 @@ description: 'Test writing best practices and conventions' Language-agnostic guidelines for writing effective tests. +## Discovering Existing Test Tooling + +Before creating scripts for test-related tasks (running tests, gathering coverage, generating reports): + +1. **Check for build systems** - Look for `Makefile`, `build.ps1`, `package.json` scripts, `tox.ini`, + `pyproject.toml`, or similar build configuration files +2. **Search README and CI configs** - Existing commands are often documented or visible in CI workflows +3. **Ask the user** - If unsure whether tooling exists, ask before creating anything new + +**Never create new scripts when existing build tooling already handles the task.** + ## Test Structure ### Arrange-Act-Assert (AAA) @@ -245,9 +256,9 @@ adminUser = createTestUser({ role: "admin" }) Prioritize testing: 1. Business-critical functionality -1. Error handling and edge cases -1. Security-sensitive code -1. Complex algorithms +2. Error handling and edge cases +3. Security-sensitive code +4. Complex algorithms ### Coverage Goals diff --git a/instructions/update.instructions.md b/instructions/update.instructions.md index 9760d58..4fc7231 100644 --- a/instructions/update.instructions.md +++ b/instructions/update.instructions.md @@ -29,6 +29,20 @@ Repositories control AIM behavior through `aim.config.json` in the repository ro "description": "Community-contributed instructions from GitHub" } ] + }, + "skills": { + "enabled": true, + "vendorPath": ".agents/skills", + "dependencies": [ + { + "name": "psake", + "source": "psake/psake-llm-tools", + "path": "plugins/psake/skills/psake", + "version": "v2.2.0", + "format": "skill-md", + "description": "psake build authoring (Agent Skill, agentskills.io)" + } + ] } } ``` @@ -40,6 +54,13 @@ Repositories control AIM behavior through `aim.config.json` in the repository ro - `modules.exclude` - List of modules to exclude (takes precedence over include) - `externalSources.enabled` - Enable fetching from external repositories - `externalSources.repositories` - List of external instruction sources +- `skills.enabled` - Enable vendoring declared Agent Skill (SKILL.md) dependencies into the repo +- `skills.vendorPath` - Directory skills are vendored into (default `.agents/skills`, the + cross-client Agent Skills convention) +- `skills.dependencies` - List of skills to vendor, each with `name`, `source` (repo), `path` + (skill folder within the source), `version` (tag to pin, or `latest`), `format` (`skill-md`), + and `description`. Unlike instruction modules, skills are copied to `vendorPath` (not + `instructions/`) and routed via `AGENTS.md` - see step 7 ## Update Procedure @@ -82,12 +103,12 @@ Based on `aim.config.json`: For each instruction file in the upstream `instruction-templates/` folder: 1. Check if the module should be synced based on configuration -1. Check if the file already exists in the downstream `instructions/` folder -1. **If the file exists, ask the user:** +2. Check if the file already exists in the downstream `instructions/` folder +3. **If the file exists, ask the user:** - "File X already exists. Overwrite with upstream version? (yes/no/diff)" - If "diff", show the differences between local and upstream versions - Only overwrite if the user confirms -1. **If the file is new**, copy it without prompting +4. **If the file is new**, copy it without prompting ### 6. Handle External Sources @@ -95,9 +116,9 @@ If `externalSources.enabled` is true and a needed language/framework instruction AIM: 1. Check each configured external repository in order -1. For awesome-copilot, look in the `instructions/` path for matching `.instructions.md` files -1. Download the instruction file and copy to the downstream `instructions/` folder -1. Inform the user which files were fetched from external sources +2. For awesome-copilot, look in the `instructions/` path for matching `.instructions.md` files +3. Download the instruction file and copy to the downstream `instructions/` folder +4. Inform the user which files were fetched from external sources **Example external fetch:** @@ -106,21 +127,50 @@ Fetching python.instructions.md from github/awesome-copilot... Fetching react.instructions.md from github/awesome-copilot... ``` -### 7. Update AGENTS.md +### 7. Handle Skill Dependencies + +If `skills.enabled` is true, vendor each declared Agent Skill (SKILL.md format) into the +repository so it travels with the code and any agent can use it - materialized like an instruction +module, not installed per-developer. Skills are NOT copied into `instructions/`; they are vendored +under `skills.vendorPath` (default `.agents/skills`), the cross-client +[Agent Skills](https://agentskills.io) convention that conforming agents discover directly. + +For each entry in `skills.dependencies`: + +1. Resolve `source` at the pinned `version` and locate the skill folder at `path` (the directory + containing `SKILL.md`). `version` is an exact tag or `latest`; `latest` means the most recent + release tag of `source` (its newest version tag when the source publishes no GitHub releases), + never the default branch's moving HEAD, so every agent vendors identical contents. +2. Copy that folder verbatim to `//` (the `SKILL.md` plus any `references/`, + `scripts/`, or `assets/`). Do not edit the vendored copy - re-sync from upstream instead. +3. **If `//` already exists, ask the user** before overwriting (same posture as + instruction files): overwrite / skip / diff. +4. Record or refresh upstream attribution and license in `/NOTICE.md`. +5. Route the skill in `AGENTS.md`: add a row to the Instruction Applicability Matrix mapping the + relevant task type to `//SKILL.md`, and list it in the "Skill Dependencies" + section. This is what makes any AGENTS-aware agent consult the skill. +6. Ensure a `CLAUDE.md` exists whose first line imports `AGENTS.md` (`@AGENTS.md`). Claude Code + reads `CLAUDE.md` - not `AGENTS.md` and not `/` - so this import is the bridge that + carries the routing into Claude Code. Preserve any Claude-specific content below the import. + +Agents that natively scan `.agents/skills/` (for example Cursor and opencode) pick the skill up +directly; the `AGENTS.md` routing plus the `CLAUDE.md` bridge covers agents that do not. + +### 8. Update AGENTS.md - Replace the HTML comment block at the top (the comment starting with `