Where we actually stand
Nothing declares a platform. package.json has no os field, and the whole
repo contains exactly one process.platform check — app/Support/Update/self-update.ts:121.
So the app doesn't refuse to run on Windows; it just fails in ways that look
like bugs rather than like "unsupported".
The good news first: the shell is not the problem. craft-native@0.0.120
already resolves win32, appends .exe, and carries 114 Windows references in
its runtime. The launcher is the only place that hardcodes a POSIX binary name
(app/Desktop/launcher.ts:81, path.join(macosDir, 'craft-runtime')). A
Windows build of the window is close to free.
The work is all in the cleaning logic, and it's substantial:
| Packages, all with macOS-only commands | 6 of 6 |
exec/spawn call sites | ~73 (core 26, monitor 25, clean 14, uninstall 6, disk 2) |
| Hardcoded macOS path references | ~280 |
process.platform guards today | 1 |
| Clean targets to re-map | ~84 |
| App screens | 15 |
macOS-only commands, by package:
- clean —
diskutil,launchctl,mdfind,defaults,periodic,kextcache,purge,lsof - core —
launchctl,osascript,sips,sw_vers,system_profiler,sysctl,purge - monitor —
vm_stat,iostat,pmset,scutil,system_profiler,sysctl,osascript - uninstall —
launchctl,osascript - cli —
defaults,osascript,sysctl,purge - disk —
osascript
The seam that already exists
packages/core/src/paths.ts:187 exports macPaths — 22 keys, every consumer
goes through it. That's the natural place to split, and it means the path half
of this is a mechanical refactor rather than a rewrite.
The command half has no equivalent seam. There is no exec abstraction; each package shells out directly.
Plan
1. Declare the platform, stop pretending. Add os to package.json and a
single platform() helper. Today a Windows user gets command not found
surfaced as an empty result — the Quick Clean screen would show 0 B recoverable
and look merely disappointing rather than unsupported. That's the worst failure
mode and the cheapest to fix.
2. Split macPaths into a platform-dispatched paths module. Same keys,
resolved per-OS. The Windows map is genuinely different in kind, not just in
spelling:
| concept | macOS | Windows |
|---|---|---|
| user caches | ~/Library/Caches | %LOCALAPPDATA%\<app>\Cache, %TEMP% |
| app support | ~/Library/Application Support | %APPDATA%, %LOCALAPPDATA% |
| installed apps | /Applications (bundles) | registry Uninstall keys + %ProgramFiles% |
| trash | ~/.Trash | $Recycle.Bin (per-volume, SID-scoped) |
| startup items | launchctl + LaunchAgents | Run/RunOnce keys, Startup folder, Scheduled Tasks |
| logs | ~/Library/Logs | Event Log + per-app %LOCALAPPDATA% |
3. Introduce a capability layer for the commands. Each of the ~73 call
sites becomes a named capability (diskFree(), memoryPressure(),
listStartupItems(), emptyTrash()) with a per-platform implementation.
PowerShell/WMI covers most of the monitor package; the Recycle Bin and registry
work needs real Windows APIs rather than shelling out.
4. Decide what Windows doesn't get. Some screens have no honest Windows
analogue — app bundle uninstall is a registry operation, not a directory
delete; .DS_Store cleanup is meaningless; Gatekeeper/notarization has no
counterpart. Better to hide a screen than to ship one that silently finds
nothing.
5. CI. Every job is ubuntu-latest except the desktop release job
(macos-26). Windows needs a windows-latest matrix leg for tests, and a
separate packaging path — code signing on Windows is Authenticode, not
codesign/notarization.
Open questions
- Scope of v1. Quick Clean + Disk Usage + Large Files + Duplicates + Developer Junk are all portable in concept. Applications, Startup, Privacy and Maintenance are the macOS-shaped ones. Shipping the first five on Windows is a coherent product; shipping all fifteen is a much longer road.
- Testing. 38 test suites, and the ones that matter here assert behaviour against a real filesystem. Windows CI will surface path-separator and permission assumptions that macOS and Linux both hide.
- Does Craft give us a real Windows window? The resolver knows
win32, but nobody has built this app against it. That's the first thing to prove, and it's a spike, not a plan.
Suggested order
Prove the Craft window on Windows first — if that doesn't work, everything above is moot. Then the platform declaration (step 1), because it converts a confusing failure into an honest one for whatever ships next.