# Windows Technician Toolkit — Profile Migration & Drivers

A single-file Windows GUI application (`UserAccountMigrator.exe`) that does the two jobs a
technician hits on the same machine visit:

1. **Profiles & programs** — copies or merges the contents of one Windows user profile into another
   and catalogues the programs that were installed so they can be downloaded or reinstalled, with a
   preview-first workflow, full logging and a working undo.
2. **Drivers & unknown devices** — identifies the yellow triangles in Device Manager from their raw
   hardware IDs, and asks Windows Update which newer drivers are on offer for the PC.

The two halves are top-level tabs of one window, so there is one executable to copy to the machine.

**Built artifact:** `dist/win-x64/UserAccountMigrator.exe` (self-contained, ~62 MB, no .NET install
required on the target PC, Windows 10/11 x64).

---

## Why it is safe

| Guarantee | How |
|---|---|
| The source profile is never changed | The engine only ever *reads* the source. There is no move or delete code path for it. |
| Nothing runs without a preview | "Start migration" stays disabled until a dry run has completed **for the exact settings you are about to use**. Change any option and it locks again. |
| Nothing is overwritten by default | The default placement copies everything into one new sub-folder of the destination profile, so collisions are impossible. |
| Overwrites are reversible | When merging in place, any file about to be replaced is copied to an `OverwriteBackup` folder first. |
| Every run is auditable | Each run gets its own folder under `C:\ProgramData\UserAccountMigrator\Logs` with a full text log and a CSV manifest of every single action. |
| Runs can be undone | "Undo a previous migration" replays the manifest backwards: files the tool created are deleted, files it replaced are restored. Anything it did not touch is left alone. |
| Dangerous configurations are refused | Same account as source and destination, identical profile paths, or one profile nested inside the other are all blocked in the engine *and* in pre-flight. |
| Copies are verified | Every file is size-checked after copying; optional SHA-256 deep verification. |
| The program catalogue only reads | It reads uninstall registry keys and lists installer files. It never installs, uninstalls or changes a program, on either machine. |
| Explicit confirmation | Live runs require typing `MIGRATE`; registry imports require `IMPORT`; undo requires `UNDO`. |
| Scanning for drivers changes nothing | Enumerating devices, decoding hardware IDs, exporting reports and checking for updates are all read-only. Anything that installs a driver asks first and names what it will do. |

---

## Using it

Copy the `.exe` to the PC and run it. It requests administrator rights automatically (UAC prompt);
if you decline, it opens read-only and pre-flight will block the migration.

The window has two top-level tabs — **Profiles & programs** and **Drivers & unknown devices**. Each
has its own numbered steps and its own status bar; nothing in one half affects the other.

A footer runs along the bottom of the window on both tabs, with a link to the author
([Patrick Hamid](https://linkedin.com/in/phamid)) and a **Buy me a coffee** link
(<https://coffee.insta.host>) for anyone who finds the tool useful. Both open in your default
browser; the addresses are fixed in the code and the tool never sends anything to them.

## Profiles & programs

### 1. Accounts
Lists every real user profile from `HKLM\...\ProfileList` with its account name, type, folder,
size (on demand), last modified date, sign-in state and SID.

* Select a row → **Set as SOURCE** (the profile to take data from).
* Select a row → **Set as DESTINATION** (the profile to put data into).
* Accounts that exist but have never signed in are listed separately — sign in once as that user so
  Windows creates the profile, then refresh.

Both users should be **signed out**. The tool warns loudly if they are not.

#### Profiles on other disks (offline profiles)
**Add from another drive…** opens a picker showing every ready volume with its label, bus type,
file system, free/total space and whether it looks like it holds Windows. Pick a disk — or
**Browse to a folder…** — and the tool scans it read-only for profiles.

It understands the layouts you actually meet in recovery work:

| You point it at | It finds |
|---|---|
| `D:\` (old system disk in a caddy) | `D:\Users\*` |
| `D:\` after a reinstall | `D:\Windows.old\Users\*` |
| `D:\Users` or `D:\Documents and Settings` | the profiles directly inside |
| `E:\Backups\OldPC\Users\john` | that one profile |

A folder counts as a profile if it holds `NTUSER.DAT`, or (for copied/partial backups) a plausible
combination of `Desktop`, `Documents`/`My Documents` and `AppData`. `Default`, `All Users`,
`Default User` and junction points are skipped; `Public` is included because shared data is often
worth recovering. The search is depth-limited and skips `Windows`, `Program Files`, `ProgramData`,
`$Recycle.Bin` and similar so it stays fast.

Found profiles join the main list as type **External** / state **Offline**, survive **Refresh
list**, and can be dropped again with **Remove**. Everything else works normally with them:
sizing, preview, the copy engine, and even the registry tab (their `NTUSER.DAT` is mounted
temporarily). Pre-flight re-checks that the disk is still attached before a run.

An offline profile can also be used as the **destination** — handy when restoring data to a disk
that is going back into another PC — after an explicit confirmation. In that case ownership and
permissions are deliberately skipped and logged, because the account does not exist on this
computer; re-apply them once the disk is back in its own machine.

### 2. What to copy
* Tick the folders to copy. **Recommended** ticks Desktop, Documents, Downloads, Pictures, Music,
  Videos, Favorites, Links, Contacts, Searches, Saved Games, 3D Objects.
* **Placement**
  * *Into one new sub-folder* (default) — everything lands in e.g.
    `C:\Users\jane\Migrated from john 2026-08-04\Desktop\...`. Nothing existing can be touched.
  * *Merge into matching folders* — Desktop into Desktop, Documents into Documents, etc.
* **Conflict policy** (only relevant when merging): skip existing (default), overwrite only when the
  source file is newer, keep both copies (`report (migrated).docx`), or always overwrite.
* **Exclusions** — semicolon separated, `*` and `?` wildcards. Patterns containing `\` match the
  path relative to the profile root; otherwise they match the file or folder name. Sensible
  defaults are pre-filled (registry hives, temp, caches, `AppData\Local\Packages`, LocalLow…).
* **Safety options** — skip junctions/symlinks/OneDrive placeholders, include hidden and system
  files, preserve timestamps, back up before overwriting, verify, SHA-256 deep verify, apply
  ownership + full control to the destination account, create a System Restore point.

### 3. Settings (advanced) — optional
Copies per-user registry settings (mapped drives, printers, Office, file associations, startup
items, application settings, custom `HKCU` keys).

1. **Export selected from SOURCE** — mounts the source `NTUSER.DAT` under `HKEY_USERS`, exports each
   key to a plain-text `.reg` file, then unmounts. Review the files.
2. **Import into DESTINATION** — mounts the destination hive, exports the destination's current
   version of each key to a `rollback\*.rollback.reg` file, rewrites the exported keys to point at
   the destination hive, then imports. If a key did not exist, the rollback file simply deletes it
   again.

The destination user must be signed out or Windows will overwrite the hive on sign-out.

### 4. Programs - catalogue what was installed
Copying files never brings the applications across. This tab reads the uninstall registry keys and
writes a **reinstall kit**: what was installed, which of it can be reinstalled automatically, and
what needs a person. It only reads - nothing is installed, changed or uninstalled from this tab.

It reads, for the profile selected as SOURCE:

| Source | Read from |
|---|---|
| Programs for all users, 64-bit and 32-bit | `HKLM\...\Uninstall` and `WOW6432Node\...\Uninstall` |
| Programs installed for that one user | that profile's `NTUSER.DAT`, mounted temporarily |
| Microsoft Store / MSIX apps | the Appx package store |
| An **offline** disk's Windows | its `Windows\System32\config\SOFTWARE` hive, mounted temporarily |

Windows updates, hotfixes, patches of a parent product and hidden system components are filtered out,
and the same product found in two hives is collapsed into one row.

Each program is then given a route back:

* **winget** - a package id is known, so it can be scripted. Ids come from the built-in map of
  common business applications, and - on a live machine - from `winget list`, which always wins.
  **Look up unknown apps in winget** searches winget by name for whatever is left (needs internet).
* **Microsoft Store** - reinstall from the Store, or with `winget --source msstore`.
* **Vendor download** - no package id, but the program's own registry entry gave a vendor URL.
  Double-click the row to open it.
* **Installer on disk** - an installer for it was found in Downloads, on the Desktop or in an
  `Installers`/`Software` folder on the old disk. **Copy found installers to…** copies them off.
* **Manual / licensed** - accounting suites, Adobe, Autodesk, managed antivirus, printer software.
  These carry a plain-English warning about serials, data files and activation limits, because they
  are the ones that ruin a rebuild if the old machine is wiped first.
* **Redistributable** - VC++ runtimes, .NET, chipset and GPU drivers. Listed, hidden by default and
  never installed by the script unless asked for; the new machine supplies its own.

**Write the reinstall kit…** produces, for the ticked rows:

```
20260805-181500_programs_john\
  installed-programs.html          readable report, grouped by route, with clickable links
  installed-programs.csv           every field, for the job notes or a handover sheet
  reinstall-programs.ps1           run on the NEW PC: -WhatIf, -IncludeRedistributables, -FromFolder
  download-installers.ps1          "winget download" everything into a folder for an offline rebuild
  winget-packages.json             winget import <file>
  manual-reinstall-checklist.md    tick-box list of what still needs a person, plus a pre-wipe check
  installers-found-on-disk.csv     installers already sitting on the old disk
  catalogue.log                    exactly what was read, and anything that could not be
```

`reinstall-programs.ps1` is safe to rehearse: `-WhatIf` prints the plan and installs nothing, and
`-FromFolder D:\Installers` prefers an installer you already downloaded or copied over the internet
copy, falling back to winget only when there is no local match.

### 5. Preview & run
* **Pre-flight checks** — elevation, both profiles selected/different/present, nesting, sign-in
  state, folder selection, free disk space vs. the previewed size, and a plain-English summary of
  the conflict behaviour. Any `FAIL` blocks the run.
* **1. Run preview** — walks everything and writes `preview.log` + `planned-actions.csv`. Nothing is
  written to the destination.
* **2. Start migration** — unlocked only by a matching preview. Live progress bar, per-file log,
  and **Cancel** at any time (already-copied files stay; use Undo to remove them).
* **Undo a previous migration…** — pick the `manifest.csv` of any past run.

### Output locations
```
C:\ProgramData\UserAccountMigrator\Logs\
  20260804-131500_john_to_jane_preview\   preview.log, planned-actions.csv
  20260804-132200_john_to_jane\           migration.log, manifest.csv,
                                          OverwriteBackup\..., undo-*.log
  20260804-133000_registry_export\        *.reg, import\rollback\*.rollback.reg
  20260804-134000_programs_john\          the reinstall kit (see tab 4)
```

---

## Drivers & unknown devices

The second top-level tab. Nothing here changes the PC until you ask it to.

### 1. Unknown devices
Enumerates every PnP device through SetupAPI/CfgMgr32 — the same source Device Manager uses — and
lists everything Windows filed under **"Other devices"** plus anything reporting a problem code
(Code 28 "drivers are not installed", Code 10, Code 43, Code 45…), each translated into plain
English with a recommended next step. Optionally includes ghost devices that are no longer plugged
in.

Each device's raw hardware ID is decoded — bus, vendor, device, subsystem, revision, PCI class — so
`PCI\VEN_8086&DEV_A0F0&SUBSYS_00748086&REV_20` reads as *Intel Corporation — Wi-Fi 6 AX201*.
`PCI`, `USB`, `HDAUDIO`, `ACPI`, `HID` and `SCSI` identifiers are all understood.

* **Identify online** opens the right lookup for that exact device: Device Hunt, PCI Lookup, and a
  **Microsoft Update Catalog** query trimmed to `VEN_xxxx&DEV_xxxx` (leaving the subsystem and
  revision on usually returns zero catalog results).
* **Retry driver match** re-runs driver matching against the driver store and then tells you whether
  the device actually picked something up.
* **Details…** shows every property, all hardware and compatible IDs, and the lookup buttons.

### 2. All devices
The full inventory with driver version, date and provider, filterable by text and by class.

### 3. Driver updates
Asks the Windows Update agent which driver packages are on offer — including the **optional driver
updates Windows never installs by itself**.

* tick **Include Microsoft Update** to get third-party driver packages as well;
* every offer is matched back to a device on this PC, so you can see which update fixes which
  yellow triangle;
* tick the ones you want and **Install ticked updates** downloads and installs them (EULAs accepted
  automatically) and reports whether a restart is needed;
* common failures are explained rather than shown as a hex code — a disabled `wuauserv`, a WSUS
  policy blocking driver offers, and so on.

### 4. Driver store & tools
* the third-party packages published in the driver store (`pnputil /enum-drivers`), with removal;
* **Add drivers** — point it at a folder of extracted vendor drivers and every matching INF is
  installed (`/add-driver *.inf /subdirs /install`); this is the fix for a freshly imaged machine
  with no network driver;
* **Back up all drivers** — exports the driver store to a folder for a rebuild;
* **Download ID database** — fetches the public `pci.ids` / `usb.ids` files to
  `C:\ProgramData\DriverFinder\ids` so exact device names resolve **offline** from then on (without
  it, a built-in table of ~200 common vendors is used);
* **Vendor support page** — a direct link to the manufacturer's drivers, using the BIOS serial where
  the vendor supports it (Dell service tag, Lenovo/HP serial);
* scan for hardware changes, open Device Manager.

### Reports
**Export report…** writes a self-contained HTML report (machine identity, devices needing attention
with recommended actions, available driver updates, full inventory) or CSV, defaulting to
`Documents\DriverFinder`. Driver-side logs go to `C:\ProgramData\DriverFinder\Logs`.

---

## Known limits (by design)

* Copies **profile data**, it does not re-point a profile or rename an account. To hand a machine to
  a new account, create/sign in as the new account, then migrate into it.
* Offline profiles are identified by folder name — an old disk's SIDs cannot be resolved to account
  names on this computer, so the account name shown is the profile folder name.
* Mounting an offline `NTUSER.DAT` (registry tab) needs the disk to be writable; `reg load` fails on
  a read-only or BitLocker-locked volume, and the error is logged.
* The same applies to an offline `SOFTWARE` hive on the programs tab. When it cannot be mounted the
  tool falls back to listing the `Program Files` folder names, so the technician still gets a list -
  without versions or publishers.
* The programs tab catalogues and *identifies* software; it does not license it. Serial numbers,
  activation states and cloud sign-ins cannot be read out of a profile, which is why licensed
  products are always listed as manual work with a warning to collect the keys before the old
  machine is wiped.
* winget package ids resolved by name search are a best guess - the report says so, and the row
  should be checked before the reinstall script is trusted on a customer machine.
* `AppData\Local` and `AppData\LocalLow` are unticked/excluded by default — machine-specific caches
  and UWP package state rarely survive a move and can break applications. Tick `AppData` and remove
  the exclusions only if you know the application tolerates it.
* Files locked by a signed-in user may fail to copy; they are logged as errors and the run
  continues. Sign both users out for a clean result.
* Credential Manager entries, DPAPI-protected secrets and EFS-encrypted files cannot be transferred
  between accounts — they are cryptographically bound to the original user.
* OneDrive/cloud placeholder files are skipped while "skip junctions, symlinks and OneDrive
  placeholders" is on, so the tool will not trigger multi-gigabyte downloads.
* The driver half only ever *offers* what Windows Update offers. If a WSUS/Intune policy suppresses
  driver updates, the list will be empty — the Update Catalog links on the unknown-devices tab still
  work, and the driver `.cab` can be downloaded on another PC and installed from the tools tab.
* "Retry driver match" only re-matches against INFs already in the driver store; it does not go
  online. Use the driver-updates tab or a vendor download for anything the store does not cover.
* No System Restore point is created before a driver install. Create one first on a production PC.

---

## Building from source

Requires the .NET 7 SDK. The build works from Windows, macOS or Linux.

```bash
./build.sh              # test + publish self-contained single-file exe -> dist/win-x64
./build.sh --framework  # small exe, needs the .NET 7 Desktop Runtime on the PC
./build.sh --test       # run the engine tests only
```

Because the .NET SDK cannot customise the Windows apphost when publishing from macOS/Linux
(`NETSDK1074`), the build does two things automatically:

* `tools/patch_pe.py` flips the PE subsystem from console to Windows GUI so no console window
  appears;
* the application elevates itself at runtime via the UAC `runas` verb instead of relying on an
  embedded manifest (`app.manifest` is still in the project, so a Windows build embeds it and the
  runtime path simply becomes a no-op).

### Layout
```
src/UserAccountMigrator/
  Program.cs           entry point + self-elevation
  ShellForm.cs         the window: hosts both halves as top-level tabs
  MigrationPanel.cs    the five-tab profile/programs GUI
  ConfirmDialog.cs     type-the-word confirmation dialog
  Models.cs            options, results, manifest entries
  ProfileService.cs    profile discovery, SID resolution, sizing
  ExternalProfileScanner.cs  offline profile discovery on other disks / Windows.old
  DriveScanDialog.cs   drive picker for offline profiles
  MigrationEngine.cs   dry-run/copy engine, verification, manifest
  UndoService.cs       manifest-driven rollback
  RegistryMigrator.cs  NTUSER.DAT mount / export / guarded import
  ProgramInventory.cs  program catalogue model, noise filter, app map, winget parsing
  ProgramScanner.cs    uninstall-key reading (live + offline hives), installer harvesting
  ProgramCatalogExporter.cs  CSV / HTML / winget import / reinstall + download scripts / checklist
  SystemUtil.cs        long paths, exclusions, elevation, icacls, restore points
  Drivers/             the driver half, in namespace DriverFinder
    DriverPanel.cs         the four-tab driver GUI
    DeviceDetailsDialog.cs per-device detail sheet with lookup buttons
    DeviceScanner.cs       SetupAPI/CfgMgr32 enumeration (+ Get-PnpDevice fallback)
    HardwareId.cs          hardware ID decoding, built-in vendor table, lookup links
    IdsDatabase.cs         pci.ids / usb.ids parser, download + cache
    ProblemCodes.cs        CM_PROB_* codes in plain English, with advice
    WindowsUpdateService.cs late-bound Windows Update Agent COM: search, download, install
    UpdateQuery.cs         WUA criteria + HRESULT translation
    DriverTools.cs         pnputil: driver store, offline installs, backup, export
    MachineInfo.cs         PC identity from the registry, for vendor support links
    ReportService.cs       driver CSV + HTML reports
    TextUtil.cs, Models.cs, SystemUtil.cs
tests/EngineTests/     headless end-to-end tests of both engines (runs on any OS)
tools/patch_pe.py      PE subsystem fix-up
```

The two halves keep their own namespaces (`UserAccountMigrator` and `DriverFinder`), so each has its
own `SystemUtil`/`Models` without colliding. Both GUIs are `UserControl`s; `ShellForm` owns the
window and the "something is still running" guard on close.

### Tests
`./build.sh --test` exercises the real engine against synthetic profile folders: exclusion matching,
preview-writes-nothing, skip-existing merge, keep-both renaming, overwrite + backup + undo
round-trip, isolated placement, refusal of overlapping profiles, external-drive scanning (including
`Windows.old`, single-profile folders and skip rules) and an end-to-end migration from an offline
profile.

It also covers the program catalogue end to end: update/system-component filtering, install-date
parsing, the reinstall-route classifier (winget, Store, vendor link, installer on disk, licensed,
redistributable), cross-hive de-duplication, `winget list` table parsing including truncated names
and non-installable `ARP\...` ids, installer-to-program matching, and the generated kit itself -
CSV quoting, PowerShell quoting, HTML escaping, and the rule that unticked programs never reach an
artefact.

The same run then executes the driver-side assertions (170 of them): hardware-ID decoding for
`PCI`/`USB`/`HDAUDIO`/`ACPI`/`SCSI`, Update Catalog query trimming, the `pci.ids`/`usb.ids` parser
(including the class blocks that must not be mistaken for vendors), `pnputil /enum-drivers` parsing,
the Windows Update criteria builder, problem-code mapping, update-to-device matching, date
normalisation and both driver report writers. Any failure on either side fails the build.
