Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2b95931
feat(cli): lower windows floor to build 14393 with support tiers
claude Sep 17, 2026
67aa01b
fix(cli): handle a missing winget instead of throwing
claude Sep 17, 2026
5852a99
feat(gui): support windows 10 22h2 as the gui floor
claude Sep 17, 2026
187c3c0
docs: document the windows support tiers
claude Sep 17, 2026
ab92073
feat: set the floors at windows 19045 and kernel 6.0
claude Sep 17, 2026
e166865
refactor: move the windows stack under src/Windows
claude Sep 17, 2026
7623855
feat(linux): add the python linux app
claude Sep 17, 2026
a79b018
ci: lint and type-check the linux app
claude Sep 17, 2026
b9b3cfc
refactor(cli): make the powershell cli windows-only
claude Sep 17, 2026
1095e6b
docs: document the windows/linux split
claude Sep 17, 2026
e45ff25
build: publish self-contained and ship an MSI installer
claude Sep 17, 2026
bdaf556
fix(ci): pin wix to v5 and build the installer on windows
claude Sep 17, 2026
5a48a74
fix(linux): make `python app.py` work from the gui directory
claude Sep 17, 2026
c545db8
build: move to wix v7 with the osmf eula accepted
claude Sep 17, 2026
d8ebde7
fix(build): pass the eula name to wix -acceptEula
claude Sep 17, 2026
05e2f15
refactor(linux): rebuild the gui around declarative tool choices
claude Sep 17, 2026
7091fd0
feat(linux): add the health report, and stop progress output flooding
claude Sep 17, 2026
1464ad9
fix(linux): ask for the password once per tool, not once per command
claude Sep 18, 2026
5d3abf8
refactor: make the tool contract structural instead of line-based
claude Sep 18, 2026
675567d
refactor(gui): fold command output away on the windows pages
claude Sep 18, 2026
547bdb7
fix(gui): restore the Diagnostics using and drop the accidental BOMs
claude Sep 18, 2026
0f19fc9
refactor: read the system through native APIs instead of parsing CLI …
claude Sep 18, 2026
4b83b3d
feat(gui): drive winget through its COM API instead of winget.exe
claude Sep 18, 2026
521aefa
fix(linux): stop asking for a password to talk to the user's own session
claude Sep 18, 2026
df69c69
feat(gui): start the Windows Update repair from inside pcHealth
claude Sep 18, 2026
1d4dcee
fix(gui): treat a null from ShellExecute as success, not failure
claude Sep 18, 2026
0dc62a0
fix(tools): make the repair hunt report what it found and what it missed
claude Sep 18, 2026
8060563
feat(tools): capture command lines and chase SystemSettingsAdminFlows
claude Sep 18, 2026
2ed1990
fix(tools): match case-sensitively and look where the update stack lives
claude Sep 19, 2026
aa61593
feat(tools): scan the per-area handlers and stop guessing at paths
claude Sep 19, 2026
70d4811
feat(gui): run the repair command directly instead of pressing a button
claude Sep 19, 2026
6722b3e
fix(gui): open Settings before asking for the repair, and stop claimi…
claude Sep 19, 2026
c00e929
fix(gui): match Intel CPUs and OEM chipsets on the health page
claude Sep 19, 2026
47d04a7
feat(gui): add 26H2 and date unknown Windows releases instead of givi…
claude Sep 19, 2026
3b8f1c7
feat(gui): show the machine model next to the board code
claude Sep 19, 2026
ae3f2e8
feat(gui): report installed legacy components, not just enabled features
claude Sep 19, 2026
899a2c3
fix(gui): stop the health page crashing on the capability read
claude Sep 19, 2026
af7d09d
feat(gui): add a development runner and rename the release launcher
claude Sep 19, 2026
c543fbc
feat(gui): log what the app does, not only what fails
claude Sep 19, 2026
cdaeb72
fix(gui): stop the battery date query dumping a stack trace every scan
claude Sep 19, 2026
597b671
fix(gui): stop the license key export killing the app
claude Sep 19, 2026
055e1e7
feat(gui): log package operations, service restarts and shutdowns
claude Sep 19, 2026
b6145e6
docs: shorten the supported platforms section
claude Sep 19, 2026
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
5 changes: 2 additions & 3 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,12 @@
"windows-11":
- changed-files:
- any-glob-to-any-file:
- "src/GUI/**"
- "src/Windows/GUI/**"

"linux":
- changed-files:
- any-glob-to-any-file:
- "src/CLI/tools/linux/**"
- "src/CLI/start.sh"
- "src/Linux/**"

# ── Language labels ───────────────────────────────────────────────────────────

Expand Down
105 changes: 101 additions & 4 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ concurrency:
#
# dotnet-build → dotnet build (GUI/ WinUI 3 app)
# dotnet-format → dotnet format --verify-no-changes (code style)
# python-lint → ruff + mypy (Linux app, src/Linux)
# installer-build → wix build (MSI authoring compiles)
# pr-title → conventional commits (PR title format)
# commit-lint → conventional commits (commit message format)
# markdown-lint → markdownlint (README, SECURITY, .github docs)
Expand Down Expand Up @@ -53,10 +55,10 @@ jobs:
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore "src/GUI/pcHealth/pcHealth.csproj"
run: dotnet restore "src/Windows/GUI/pcHealth/pcHealth.csproj"

- name: Build
run: dotnet build "src/GUI/pcHealth/pcHealth.csproj" -c Release --no-restore
run: dotnet build "src/Windows/GUI/pcHealth/pcHealth.csproj" -c Release --no-restore

# ----------------------------------------------------------
# DOTNET FORMAT
Expand All @@ -78,10 +80,105 @@ jobs:
dotnet-version: '10.0.x'

- name: Restore
run: dotnet restore "src/GUI/pcHealth/pcHealth.csproj"
run: dotnet restore "src/Windows/GUI/pcHealth/pcHealth.csproj"

- name: Check formatting
run: dotnet format "src/GUI/pcHealth/pcHealth.csproj" --verify-no-changes --verbosity diagnostic
run: dotnet format "src/Windows/GUI/pcHealth/pcHealth.csproj" --verify-no-changes --verbosity diagnostic

# ----------------------------------------------------------
# PYTHON LINT
# ruff (lint + format) and mypy for the Linux app.
# Runs on Linux because that is the only platform it targets.
# The runner image already ships Python, so no setup step is needed.
# ----------------------------------------------------------
python-lint:
name: Python lint (Linux app)
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/Linux
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install tooling
run: python3 -m pip install --disable-pip-version-check ruff mypy

- name: Lint
run: python3 -m ruff check .

- name: Check formatting
run: python3 -m ruff format --check .

- name: Type check
run: python3 -m mypy pchealth

# Guards the split: the shared catalogue and the Python registry must
# list the same tools, or the menu shows an entry that cannot run.
- name: Catalogue matches registry
run: python3 -c "from pchealth import catalog; from pchealth.tools import REGISTRY; missing = [t.id for t in catalog.load() if t.id not in REGISTRY]; assert not missing, f'no implementation for {missing}'; print(f'{len(REGISTRY)} tools wired')"

# ----------------------------------------------------------
# INSTALLER AUTHORING
# Compiles installer/pcHealth.wxs against a stub payload so a
# broken installer is caught on push instead of at release
# time, when a failed build means a release without its MSI.
# Windows runner: WiX warns that it supports Windows only and
# that everything after that point is undefined.
# ----------------------------------------------------------
installer-build:
name: Installer authoring (WiX)
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup .NET 10
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'

# WiX v7 refuses to build until the Open Source Maintenance Fee EULA is
# accepted (error WIX7015), which -acceptEula below does.
#
# Accepted by the pcHealth maintainers (@Stensel8, REALSDEALS). The fee
# itself does not apply here: it is owed by organisations above $10,000
# annual revenue that use WiX to generate revenue, and pcHealth is a
# FOSS project well under that line. Only the acceptance is required.
# https://docs.firegiant.com/wix/osmf/
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 7.0.0

# A stub stands in for the publish output: this job checks the
# authoring, not the app. The real payload is built by
# development/tools/Build-Release.ps1.
- name: Create stub payload
shell: pwsh
run: |
$null = New-Item stub-publish -ItemType Directory -Force
Set-Content stub-publish/pcHealth.exe 'stub'
Set-Content stub-publish/Microsoft.WindowsAppRuntime.Bootstrap.dll 'stub'

- name: Build MSI from authoring
shell: pwsh
run: |
$version = (Get-Content VERSION -Raw).Trim()
wix build installer/pcHealth.wxs `
-acceptEula wix7 `
-arch x64 `
-d "Version=$version" `
-d "PublishDir=$((Resolve-Path stub-publish).Path)" `
-out (Join-Path $PWD 'pcHealth-authoring-check.msi')

- name: Confirm the MSI was produced
shell: pwsh
run: |
$msi = Get-Item pcHealth-authoring-check.msi -ErrorAction SilentlyContinue
if (-not $msi -or $msi.Length -eq 0) { throw 'wix produced no MSI' }
"MSI authoring builds ($([Math]::Round($msi.Length / 1KB)) KB)"

# ----------------------------------------------------------
# PR TITLE CHECK
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
build-mode: manual

- name: Build for CodeQL
run: dotnet build "src/GUI/pcHealth/pcHealth.csproj" -c Release
run: dotnet build "src/Windows/GUI/pcHealth/pcHealth.csproj" -c Release

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ jobs:

# ------------------------------------------------------------
# BUILD
# Compiles the WinUI 3 app and packages GUI + CLI into ZIPs,
# then uploads the artifacts to the GitHub Release.
# Publishes the WinUI 3 app self-contained, packages GUI + CLI
# into ZIPs, builds the MSI installers, and uploads everything
# to the GitHub Release.
# ------------------------------------------------------------
build:
name: Build & upload release artifacts
Expand All @@ -37,13 +38,20 @@ jobs:
with:
dotnet-version: '10.0.x'

# The MSI is built by WiX; -RequireMsi below turns a missing toolset
# into a failed release rather than a release without its installer.
# The OSMF EULA acceptance is explained in ci-cd.yml's installer-build job.
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 7.0.0

- name: Build release (x64)
shell: pwsh
run: pwsh -File development/tools/Build-Release.ps1 -Architecture x64
run: pwsh -File development/tools/Build-Release.ps1 -Architecture x64 -RequireMsi

- name: Build release (arm64)
shell: pwsh
run: pwsh -File development/tools/Build-Release.ps1 -Architecture arm64 -Output dist-arm64
run: pwsh -File development/tools/Build-Release.ps1 -Architecture arm64 -Output dist-arm64 -RequireMsi

- name: Upload release assets
shell: pwsh
Expand All @@ -53,6 +61,8 @@ jobs:
$v = "${{ steps.version.outputs.version }}"
$tag = "${{ github.ref_name }}"
gh release upload $tag `
"dist/pcHealth-${v}-win-x64.msi" `
"dist-arm64/pcHealth-${v}-win-arm64.msi" `
"dist/pcHealth-${v}-win-x64.zip" `
"dist-arm64/pcHealth-${v}-win-arm64.zip" `
"dist/pcHealth-CLI-${v}.zip" `
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
# ============================================================
# Overview of checks:
#
# powershell-lint → PSScriptAnalyzer (src/CLI/ scripts)
# powershell-lint → PSScriptAnalyzer (src/Windows/CLI/ scripts)
# trivy-scan → Trivy (filesystem vulnerability scan)
# devskim → DevSkim (insecure code patterns, C# / PS)
# semgrep → Semgrep (OWASP Top 10, C# rules, secrets)
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Run PSScriptAnalyzer (SARIF output)
uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f # v1.1
with:
path: src/CLI
path: src/Windows/CLI
recurse: true
settings: .github/PSScriptAnalyzerSettings.psd1
output: psscriptanalyzer-results.sarif
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,12 @@ coverage.xml
.env.*.local
/.claude
.claude*

# Python (src/Linux)
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
.mypy_cache/
.ruff_cache/
59 changes: 34 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@

## Project Structure

This project has **two separate codebases**. Know which one you're in:
This project has **three separate codebases**. Know which one you're in:

| Part | Location | Language | Purpose |
|---|---|---|---|
| CLI | `src/CLI/` | PowerShell 7 + Bash | Cross-platform terminal health tool |
| GUI | `src/GUI/pcHealth/` | C# / WinUI 3 (.NET) | Windows-only graphical frontend |
| Windows CLI | `src/Windows/CLI/` | PowerShell 7 | Windows terminal health tool |
| Windows GUI | `src/Windows/GUI/pcHealth/` | C# / WinUI 3 (.NET) | Windows-only graphical frontend |
| Linux app | `src/Linux/pchealth/` | Python 3.11+ / GTK4 | Linux terminal menu and desktop app |

Do not mix patterns between them. C# APIs do not belong in PowerShell scripts and vice versa.
Do not mix patterns between them. C# APIs do not belong in PowerShell scripts, and neither belongs in the Python package.

The tool list is shared: `assets/tools.json` is read by both stacks. A new tool
is added there first, then implemented on each side that should have it.

**Each side owns its platform completely.** There are no `$IsLinux` branches in
the PowerShell any more, and no Windows paths in the Python. A Linux tool
belongs in `src/Linux/pchealth/tools/`, never in `src/Windows/`.

---

Expand All @@ -42,7 +50,7 @@ https://github.com/JuliusBrussee/caveman

## Deprecated APIs — Avoid These

### C# / .NET (GUI — `src/GUI/`)
### C# / .NET (GUI — `src/Windows/GUI/`)

The GUI uses WinUI 3 on .NET. Replace legacy APIs with their modern equivalents:

Expand Down Expand Up @@ -74,7 +82,7 @@ foreach (var instance in session.QueryInstances(
Console.WriteLine(instance.CimInstanceProperties["Caption"].Value);
```

### PowerShell 7 (CLI — `src/CLI/`)
### PowerShell 7 (CLI — `src/Windows/CLI/`)

| Deprecated / Avoid | Preferred | Why |
|---|---|---|
Expand All @@ -86,36 +94,37 @@ foreach (var instance in session.QueryInstances(
| String concatenation for paths (`"$dir\$file"`) | `Join-Path $dir $file` | Handles both `\` and `/` correctly on Windows and Linux |
| Bare `ls`, `cat`, `cp` aliases | `Get-ChildItem`, `Get-Content`, `Copy-Item` | Aliases are unreliable in strict or non-interactive environments |
| `(& somecmd args).Trim()` | `Get-PcCommandOutput 'somecmd' @('args')` | A missing or silent command returns `$null`, and `.Trim()` on it throws — which aborts the whole tool, not just that field. On Linux this is routine: no systemd in containers and WSL, no `mokutil`/`lspci` on minimal installs |
| `sudo <cmd>` inside a tool | Call the command directly | pcHealth already exits unless it is running as root on Linux. Re-elevating is a no-op where sudo exists and a hard failure where it does not. `sudo -u <user>` to *drop* privileges is still correct |
| `$env:USER` / `$env:HOME` on Linux | `Get-PcDesktopUser` | Under `sudo pwsh` both describe root, not the person at the keyboard |
| `$IsLinux` branches | Nothing -- the Windows CLI is Windows-only | Linux is `src/Linux/`, in Python. A platform branch here means the tool is in the wrong stack |

### Bash (CLI Linux — `src/CLI/start.sh`)
### Python (Linux app — `src/Linux/pchealth/`)

| Avoid | Prefer | Why |
| Deprecated / Avoid | Preferred | Why |
|---|---|---|
| Unquoted variables (`$VAR`) | Quoted (`"$VAR"`) | Breaks on paths with spaces |
| `ls` in scripts | `find` or explicit glob | `ls` output is not reliably parseable |
| `[ ]` (single bracket) | `[[ ]]` (double bracket) | Double bracket is safer and supports regex |
| `subprocess.run(..., shell=True)` | An argv list, no shell | A shell turns any interpolated value into possible code. Every call in `system.py` passes a list |
| `os.system`, backticks, `shell=True` pipelines | `system.run` / `system.stream` | They centralise the missing-command, timeout and encoding handling |
| Bare `subprocess` calls in a tool | `system.run`, `system.output`, `system.stream` | A missing binary is the normal case on Linux, not an edge case; these return instead of raising |
| `os.geteuid() == 0` checks scattered in tools | `system.run_root` / `system.elevated` | Privilege is raised per action via pkexec so the GUI never runs as root |
| `print()` inside a tool, or any formatted text | `ui.section` / `ui.fields` / `ui.note` / `ui.run` | A tool describes results; the front-end decides whether they become text or widgets. A tool that emits `"[>>] ..."` has decided it lives in a terminal |
| Running a command by hand and printing its output | `ui.run(argv, label=...)` / `ui.run_all(...)` | Handles the step, its raw output, the exit code, and a single elevation prompt for a batch |
| `$HOME` / `os.environ["USER"]` | `system.desktop_user()` | Under sudo or pkexec both describe root, not the person at the keyboard |
| Touching GTK from a worker thread | `GLib.idle_add` | GTK may only be called from the main loop |

Run `python3 -m ruff check .`, `python3 -m ruff format --check .` and
`python3 -m mypy pchealth` from `src/Linux/` before committing. CI runs all three.

---

## Platform Guards — Mandatory

Both CLI and C# code must guard platform-specific calls:

**PowerShell:**
```powershell
if ($IsWindows) { Get-CimInstance Win32_Processor }
if ($IsLinux) { & lscpu }
```

**C#:**
```csharp
if (OperatingSystem.IsWindows()) { /* registry, CIM, WinUI */ }
```
The Windows CLI and the WinUI GUI are Windows-only, so CIM, the registry and
`Get-PnpDevice` need no platform guard there -- but they still need error
handling, because a key or a class can be missing on any given machine.

Never call `Get-CimInstance`, registry reads, `Get-PnpDevice`, or WinUI APIs
without a platform guard. The CLI runs on Linux too.
The Python side guards differently: a missing command is the normal case, so
everything goes through `system.run`, `system.output` or `system.stream`, which
return instead of raising.

---

Expand Down
Loading
Loading