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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ jobs:
shell: powershell
run: ./build/Invoke-Build.ps1 -Task Analyze

# The compiled module is what ships, so a merge that fails to produce an
# importable module must fail here rather than at release time.
- name: Compile the module
shell: powershell
run: ./build/Invoke-Build.ps1 -Task Compile

# Published as an artifact rather than gated on: runner timing is noisy
# enough that a hard threshold would fail for reasons unrelated to a
# change. The numbers are still worth having on every run.
- name: Benchmark
shell: powershell
continue-on-error: true
run: ./build/Measure-Performance.ps1 -Iterations 3 -OutputPath ./benchmark.json

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
Expand All @@ -52,6 +66,37 @@ jobs:
path: testResults.xml
if-no-files-found: ignore

- name: Upload benchmark
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark
path: benchmark.json
if-no-files-found: ignore

docs:
name: Command reference is current
runs-on: windows-latest

steps:
- uses: actions/checkout@v4

# docs/COMMANDS.md is generated from the module's own help. Regenerating
# it here and failing on a diff is what stops it describing a command that
# no longer exists, or missing one that was just added.
- name: Regenerate and compare
shell: powershell
run: |
./build/Invoke-Build.ps1 -Task Docs
# --ignore-cr-at-eol so a checkout that materialised CRLF does not
# read as a stale reference; the task itself writes LF.
$diff = git diff --ignore-cr-at-eol --name-only -- docs/COMMANDS.md
if ($diff) {
git --no-pager diff --ignore-cr-at-eol -- docs/COMMANDS.md
throw 'docs/COMMANDS.md is out of date. Run ./build/Invoke-Build.ps1 -Task Docs and commit the result.'
}
Write-Host 'Command reference is up to date.'

test-pwsh7:
name: Test on PowerShell 7
runs-on: windows-latest
Expand Down
51 changes: 37 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,18 @@ jobs:
shell: powershell
run: ./build/Invoke-Build.ps1 -Task All

- name: Stage artifacts and checksums
# Compiled first so the signing step below can sign the module that
# actually ships, before it is packed into an archive. Signing after
# packaging would leave the file inside every zip unsigned - which is the
# one file users execute.
- name: Compile the module
shell: powershell
run: ./build/Invoke-Build.ps1 -Task Release -Version ${{ steps.version.outputs.version }}
run: ./build/Invoke-Build.ps1 -Task Compile

# Signing runs only when a certificate is configured. Without it the
# release still ships, unsigned, with checksums - which is what the
# project has today.
- name: Sign scripts
- name: Sign the module and scripts
if: ${{ secrets.SIGNING_CERTIFICATE != '' }}
env:
SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }}
Expand All @@ -72,15 +76,23 @@ jobs:
$password = ConvertTo-SecureString $env:SIGNING_PASSWORD -AsPlainText -Force
$cert = Get-PfxCertificate -FilePath $pfxPath -Password $password

Get-ChildItem ./out -Include *.ps1, *.psm1, *.psd1 -Recurse | ForEach-Object {
Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert `
# The compiled module, the GUI shell inside it, the bootstrapper and
# the legacy single-file tools - everything a user can execute.
$targets = @()
$targets += Get-ChildItem ./obj/PCTools -Include *.ps1, *.psm1, *.psd1 -Recurse -File
$targets += Get-ChildItem . -Filter *.ps1 -File

foreach ($target in $targets) {
Set-AuthenticodeSignature -FilePath $target.FullName -Certificate $cert `
-TimestampServer 'http://timestamp.digicert.com' -HashAlgorithm SHA256
}

Remove-Item $pfxPath -Force

# Signing rewrites the files, so the checksums must be regenerated.
./build/Invoke-Build.ps1 -Task Checksum
# -SkipCompile so the signatures written above survive into the archives.
- name: Stage artifacts and checksums
shell: powershell
run: ./build/Invoke-Build.ps1 -Task Release -SkipCompile -Version ${{ steps.version.outputs.version }}

# Publishing to the Gallery is opt-in: without an API key the release
# still ships as an archive. Gallery versions are immutable, so this runs
Expand All @@ -91,7 +103,10 @@ jobs:
env:
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
run: |
$module = './src/PCTools'
# The compiled, signed module - not the source tree. Publishing the
# sources would ship the sixty-file dot-sourcing loader and, when a
# certificate is configured, discard the signatures.
$module = './obj/PCTools'

# Refuse to republish a version that already exists - the Gallery
# rejects it anyway, and failing here says why.
Expand All @@ -110,15 +125,23 @@ jobs:
out/*
generate_release_notes: true
body: |
## Verifying this release
## Install

```powershell
irm https://raw.githubusercontent.com/likeBloodMoon/pc-powershelltools/${{ github.ref_name }}/install.ps1 | iex
```

The installer prefers the signed PowerShell Gallery package and falls
back to the archive below, verifying it against `SHA256SUMS` first.

## Verifying this release yourself

Every file below is listed in `SHA256SUMS`. Verify before running:
Every file below is listed in `SHA256SUMS`:

```powershell
$file = 'pc-tools.ps1'
(Get-FileHash $file -Algorithm SHA256).Hash.ToLower()
(Get-FileHash pc-tools-${{ steps.version.outputs.version }}.zip -Algorithm SHA256).Hash.ToLower()
# compare against the matching line in SHA256SUMS
Comment thread
likeBloodMoon marked this conversation as resolved.
```

Install commands are pinned to this tag in the
[README](https://github.com/likeBloodMoon/pc-powershelltools#install).
Full instructions are in the
[README](https://github.com/likeBloodMoon/pc-powershelltools#install-and-run-in-one-command).
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ desktop.ini
*.psproj
testResults*.xml
obj/

# Benchmark output. Machine- and load-specific, so a committed baseline would
# mislead rather than help; CI publishes each run as an artifact instead.
build/benchmark.json
179 changes: 178 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,181 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Nothing yet.

## [0.5.0] - 2026-09-02

One command to install and run, hot paths measured rather than assumed, and
roughly double the commands.

### Added

#### One command

- `install.ps1`: a bootstrapper pinned to a release tag. Prefers the signed
PowerShell Gallery package; falls back to the release archive, which it
verifies against the published `SHA256SUMS` and refuses to unpack on a
mismatch. There is no switch to skip that check. Installs a `pctools` command
into `%LOCALAPPDATA%\Microsoft\WindowsApps`, already on PATH on Windows 10
and 11, so no PATH edit and no elevation.
- `Start-PCTools` (alias `pctools`) opens the window. The shell moved from
`src/Shell` into the module, so `Install-Module PCTools` now delivers the GUI
as well as the commands - previously the Gallery shipped the module only.
- `Invoke-PCTools`: a scriptable front door with exit codes (`0` success,
`1` warnings, `2` failures, `3010` restart required), `-Json`, and
`-Report Html|Json|Text|Csv`. What the `pctools` shim invokes.

#### Storage and startup

- `Get-PCDiskSpace`, with a free-space pressure grade rather than a bare number.
- `Get-PCDiskUsage`: largest folders and files, over `System.IO` enumeration.
- `Clear-PCDeliveryOptimization`, `Clear-PCWindowsOld`, `Clear-PCThumbnailCache`,
`Clear-PCCrashDump`, `Clear-PCEventLog`.
- `Optimize-PCVolume`: reads the media type and picks TRIM for an SSD or
defragmentation for an HDD. There is no switch to force the wrong one.
- `Get-PCStartupItem`: Run keys, Startup folders **and** logon scheduled tasks in
one list - Task Manager omits the third, which is where most modern updater
software lives. `Disable-PCStartupItem` / `Enable-PCStartupItem` write the same
`StartupApproved` flag Task Manager writes, so nothing is deleted and either
tool can undo the other.
- `Get-PCService` and `Set-PCServiceStartup`, the latter refusing a fixed list of
services whose loss breaks a machine. No `-Force`.

#### Health and security

- `Get-PCSystemInfo`, `Get-PCDiskHealth` (SMART wear, reallocated sectors,
temperature), `Get-PCBatteryReport` (capacity loss against design).
- `Get-PCBootPerformance`: boot time and, from the diagnostics log, the specific
applications, services and drivers that lengthen it and by how many
milliseconds.
- `Get-PCEventSummary`: errors and criticals grouped by source and event ID
rather than listed, with bugchecks pulled out separately.
- `Get-PCSecurityStatus`: Defender, firewall, BitLocker, UAC, SmartScreen and
pending updates. Read-only by design; there is no counterpart that changes
them.
- `Get-PCHealthReport`: a graded score with the findings behind it, each naming
the command that addresses it. The GUI dashboard renders this.

#### Software

- `Get-PCWindowsUpdate` and `Install-PCWindowsUpdate` over the
`Microsoft.Update.Session` COM API - no third-party module dependency in a tool
that runs elevated. Driver updates are excluded unless named explicitly.
- `Update-PCApplication`: `winget upgrade` with per-package results.
- `Get-PCDriverIssue`: problem devices with their Configuration Manager code
decoded into what it actually means.
- `Get-PCAppxPackage` and `Remove-PCAppxPackage`, allowlist-only. The Store,
App Installer, Photos, Calculator, Terminal and every framework package are
deliberately absent from that list.

#### Automation and reporting

- `Register-PCScheduledMaintenance`, `Get-`, `Unregister-`. Unattended runs are
limited to the profiles that cannot demand a restart.
- `Save-PCHistory` and `Get-PCHistory`, including `-Summary` for the trend across
runs. The GUI records every non-preview run.
- `Register-PCExtension` / `Get-PCExtension`: drop-in `*.PCAction.ps1` actions
usable in a profile like any built-in. An extension cannot replace a built-in
command.
- `Get-PCResultSummary`: the one-line "what just happened", shared by the GUI,
the CLI and the HTML report so they cannot disagree.

#### Other

- `Test-PCThroughput` and `Test-PCDnsServer` - the speed test and resolver
benchmark the roadmap has promised since v0.1.
- New profiles: `Storage`, `Health` (read-only) and `Weekly`.
- `Export-PCReport -Format Html`: a single self-contained file with no external
stylesheet, font or script, so it renders identically offline.
- `build/Measure-Performance.ps1`, and `-Task Compile`, `-Task Docs` and
`-Task Bench` in the build script.
- `docs/COMMANDS.md`, generated from the module's own help, and
`docs/PERFORMANCE.md`.
- New test suites: `Parallel`, `Safety`, `Compile`, `Install`.

### Changed

- **The hot paths run concurrently.** `Test-PCConnectivity` ran nine probes in
sequence, each waiting out its own timeout; `Test-PCRoute` walked up to twenty
TTLs one at a time; `Test-PCMtu` made about eleven sequential probes;
`Get-PCNetworkReport -Full` ran six external captures one after another. All
now run at once behind a new `Invoke-PCParallel` helper. Output ordering is
unchanged - it is the diagnostic value.
- **`Get-PCNetworkAdapter` makes three CIM queries instead of two per adapter.**
A laptop with Wi-Fi, Ethernet, Bluetooth, Hyper-V and VPN adapters was paying
twenty-odd round-trips to build one table.
- **`Clear-PCFolderContent` walks each tree once instead of twice.** It sized
with `Get-ChildItem -Recurse` and then deleted with `Remove-Item -Recurse`;
it now sizes and deletes in a single `System.IO` pass, with top-level entries
cleared concurrently.
- **A locked file no longer costs its whole directory.** Deleting per file rather
than per top-level entry means one held-open browser cache file leaves that
file behind, not the directory containing it. `BytesFreed` is now counted after
each successful delete, so it reports what was reclaimed rather than what was
hoped for.
- **`Invoke-PCProcess` reads the pipes instead of two temp files.** It created
two files with `GetTempFileName`, wrote output to disk and read it back, on
every external command. It also now escapes arguments by the rules
`CommandLineToArgvW` actually applies on Windows PowerShell 5.1; the previous
quoting mangled any argument containing a quote.
- **The shipped module is compiled to one file.** `-Task Compile` merges the
sources so an import parses one file rather than sixty, stamps the version
instead of re-reading the manifest, and prepares the log file on first write
instead of at import. Measured: about 520 ms to 400 ms. `tests/Compile.Tests.ps1`
asserts the compiled and source modules export exactly the same surface.
- The GUI gained Dashboard, Storage, Health and Software pages, and opens on the
dashboard. `Start-PCTools -Page` chooses another.
- The PSScriptAnalyzer warning budget rose from 146 to 210 for the new code. It
is a ceiling to lower, not a target.

### Fixed

- `Get-PCResultSummary` and `Export-PCReport` no longer throw on an empty batch.
`Measure-Object` emits nothing for an empty collection, so reading `.Sum` off
it is a terminating error under `Set-StrictMode -Version Latest` - which a
network-only report reaching the HTML writer would have hit.
- `Save-PCHistory` no longer overwrites an entry when two runs finish in the same
second.
- `Test-PCMtu` no longer uses `GetNewClosure()`. It rebinds a scriptblock to a
scope detached from the module's session state, so the probe could not see the
private helper it needed and every call failed.
- `Start-PCTools` could not find the GUI in the module that actually ships. It
resolved the shell as `$PSScriptRoot\..\Shell`, which is correct in the source
tree - the function lives in `Public/` - and wrong in the compiled module,
where `$PSScriptRoot` is already the module root. Every Gallery and archive
install would have thrown "the shell is missing" on the command this release
is named after.
- `Register-PCScheduledMaintenance` registered a task that imported PCTools by
name rather than by path, for the same reason: two parents up from
`$PSScriptRoot` landed outside the versioned module folder. A scheduled task
runs as SYSTEM, whose module path does not include a CurrentUser install, so
every task registered from a normal install would have failed before running
its profile. Both now resolve from
`$ExecutionContext.SessionState.Module.ModuleBase`, correct in either layout.
- `Invoke-PCParallel` discarded results that had already completed once the
batch deadline passed. The tasks run concurrently, so one slow item early in
the list could exhaust the budget while every later item had finished - and
those finished results were then reported as timeouts. Completion is now
checked before the budget.
- `docs/COMMANDS.md` was generated with a BOM on Windows PowerShell 5.1 and
without one on PowerShell 7, so the CI freshness check called it stale
depending only on which host generated it. It is now written as UTF-8 without
a BOM, with LF endings, on every host.
- The release notes named `pc-tools-v0.5.0.zip`; the build stages the archive
without the tag's leading `v`, so the documented verification step named a
file that is not attached.
- `Import-PCConfiguration`'s error for an unknown action printed literal `{0}`
and `{1}` instead of the profile and action names. The format operator binds
tighter than string concatenation, so `"a {0} " + "b {1}" -f $x, $y` formats
only the second string; the concatenation needs its own parentheses.
- The build's Analyze task and the syntax test both scanned `obj/`, which holds
the compiled module generated from the same sources. Every finding in module
code was therefore reported twice, and the number of syntax tests discovered
depended on whether a build had run. Generated output is now excluded from
both.

## [0.4.0] - 2026-01-15

### Added
- `PCTools` module (`src/PCTools`): 29 public functions covering cleanup,
repair, network, preferences and software installation. Every action returns a
Expand Down Expand Up @@ -116,6 +291,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Prefetch cleanup, DISM + SFC + CHKDSK, network stack reset, adapter IP/DNS
configuration, Windows preference toggles, winget app installation.

[Unreleased]: https://github.com/likeBloodMoon/pc-powershelltools/compare/v0.2.0...HEAD
[Unreleased]: https://github.com/likeBloodMoon/pc-powershelltools/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/likeBloodMoon/pc-powershelltools/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/likeBloodMoon/pc-powershelltools/compare/v0.2.0...v0.4.0
[0.2.0]: https://github.com/likeBloodMoon/pc-powershelltools/releases/tag/v0.2.0
[0.1.0]: https://github.com/likeBloodMoon/pc-powershelltools/releases/tag/v0.1.0
29 changes: 29 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ Names: `DarkMode`, `DisableBingSearch`, `ShowHiddenFiles`, `ShowFileExtensions`,
- **Subnet masks are converted, not passed through.** `255.0.255.0` is rejected
as invalid rather than handed to `netsh`.


## New in v0.5

The commands above all still work exactly as documented. v0.5 added areas the
single-file tools never covered at all:

| Question | Command |
|---|---|
| How is this machine doing? | `Get-PCHealthReport` |
| Where did my disk space go? | `Get-PCDiskSpace`, `Get-PCDiskUsage` |
| What starts with Windows? | `Get-PCStartupItem`, `Disable-PCStartupItem` |
| Is this disk failing? | `Get-PCDiskHealth` |
| Why is boot so slow? | `Get-PCBootPerformance` |
| What keeps crashing? | `Get-PCEventSummary` |
| Is this machine protected? | `Get-PCSecurityStatus` |
| What needs updating? | `Get-PCWindowsUpdate`, `Update-PCApplication` |
| Can this run on a schedule? | `Register-PCScheduledMaintenance` |
| How much have I reclaimed over time? | `Get-PCHistory -Summary` |

And the entry points changed shape:

| Was | Now |
|---|---|
| `.\pc-tools.ps1` | `Start-PCTools`, or `pctools` |
| `.\pc-tools.ps1 -NoGui` | `Import-Module PCTools` |
| (nothing) | `Invoke-PCTools -ProfileName Quick -Exit`, for scripts and scheduled tasks |

`.\pc-tools.ps1` still works from a clone or an extracted release archive.

## Running the old tools

Nothing was removed:
Expand Down
Loading
Loading