You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(desktop): add Windows support with an NSIS installer and release job (#81)
## Related Issue
No prior issue. The problem is described below.
## Problem
`apps/desktop` shipped a macOS-only application. `build.win` used the
`dir`
target, which emits an unpacked directory and no installer, so Windows
had
nothing to distribute and `electron-updater` had no `latest.yml` feed to
read.
Three Windows defects also lived in the shell itself, each dead or wrong
code
rather than a missing feature:
- `host-supervisor.ts` terminated the Host with `child.kill()`. Windows
has no
signal delivery, so that is `TerminateProcess` on one PID and the Host's
own
children — node-pty shells, subagent hosts — survived and kept holding
the
loopback port.
- The packaged-runtime guard tested `nodeExecutable.includes('/')`,
which is
never true for `C:\...\Pythinker.exe`. The check could not fire on
Windows.
- No `app.setAppUserModelId`, so taskbar pinning did not survive an
installer
upgrade and toasts were attributed to a generated identity.
Separately, `apps/desktop`'s 65 vitest cases and its typecheck script
ran in no
required check, so a desktop regression merged green.
## What changed
**Windows packaging** — per-user NSIS installer (`oneClick: false`,
`perMachine: false`, elevation allowed, installation directory
selectable,
desktop and Start Menu shortcuts), `dist:win`, and a `windows-latest`
release
job running in parallel with `mac`. `verify-win-installer.ts` sniffs the
DOS and
PE headers of both artifacts rather than trusting the file extension.
`dist:win` refuses to run anywhere but native Windows x64. This is not
caution:
`stage-runtime.ts` runs `pnpm deploy` on the build machine, and the
closure
resolves platform-gated natives — a macOS-staged tree carries
`@opentui/core-darwin-arm64` and cannot produce a working Windows build.
**Windows runtime** — tree kill via `taskkill /T /F`, `isAbsolute()` for
the
artifact guard, `setAppUserModelId`, and an `afterPack` check for the
win32
`node-pty` prebuilds.
**Release workflow** — an unset GitHub secret interpolates to an empty
string,
not to an absent variable, and electron-builder resolves an empty
`CSC_LINK` as
a certificate path: `path.resolve(appDir, '')` is the app directory, so
the
macOS job died on `not a file`. Credentials are now exported only when
they
carry a value, which also fixed the pre-existing macOS failure. Windows
artifacts are unsigned until `WIN_CSC_LINK` and `WIN_CSC_KEY_PASSWORD`
are
configured; the job passes them through, so enabling signing needs no
code
change.
**Staging** — Node refuses to spawn a `.cmd` shim without a shell, so
`pnpm.cmd`
raised `EINVAL`. And pnpm joins its workspace root with the deploy
target rather
than resolving it, so an absolute path on another volume became
`D:\repo\C:\Users\...`. The target is now workspace-relative, which is
correct
whether pnpm joins or resolves, and the staging directory moved onto the
repository's own volume.
**CI gates** — `apps/desktop` registered in the root vitest projects,
plus a
per-package typecheck step. Deliberately not added to the `tsgo` loop,
which
covers the source tsconfig and would silently skip
`tests/tsconfig.json`.
Out of scope, and left alone: the reference implementation's Windows ACL
runner
and pwsh sandbox trampoline (`packages/kaos` and `agent-core` carry
their own
win32 handling), Windows arm64, Linux packaging, and signing
certificates.
`apps/vscode` has the same CI-gate omission; that is pre-existing and
belongs in
its own change.
## How this was verified
Windows packaging cannot be validated on a macOS workstation, so the
proof is a
green CI run, not a local claim.
Run
[31914773126](https://github.com/PyModel/pythinker-code/actions/runs/31914773126)
— `mac: success`, `windows: success`:
```
• building target=nsis file=dist\Pythinker-0.1.0-x64-Setup.exe archs=x64 oneClick=false perMachine=false
• building block map blockMapFile=dist\Pythinker-0.1.0-x64-Setup.exe.blockmap
• uploading file=Pythinker-0.1.0-x64-Setup.exe provider=github
```
Artifacts: `desktop-windows` 134 MB, `desktop-macos` 329 MB, with
`latest.yml`
and a `.blockmap` so Windows auto-update downloads differentially.
Three earlier runs failed and each fix is a separate commit, so the
sequence is
reviewable: `EINVAL` on `.cmd`, then empty signing credentials, then the
pnpm
deploy target.
Locally, every added test was mutation-proved — the guard was reverted,
the test
was watched go red, then restored and watched go green. That includes
the new CI
gate itself: breaking a desktop test now fails root `pnpm run test` with
`FAIL |@pymodel/pythinker-desktop| tests/stage-runtime.spec.ts`, which
it could
not do before this change.
Full root suite: 682 files, 10184 passed, 71 skipped. Desktop typecheck
and
`pnpm run lint` both exit 0.
## Checklist
- [x] I have read the
[CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md)
document.
- [x] I have linked a related issue, or explained the problem above.
- [x] I have added tests that prove my feature works.
- [x] Ran `gen-changesets` skill, or this PR needs no changeset.
- [x] Ran `gen-docs` skill, or this PR needs no doc update.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added Windows x64 NSIS installer packaging with customizable
installation location, shortcuts, and optional signing.
* Added a Windows desktop release workflow and distribution command.
* **Bug Fixes**
* Improved Windows process-tree shutdown and packaged runtime
validation.
* Fixed Windows application identity and runtime staging behavior.
* Added installer validation to catch invalid or incomplete release
artifacts.
* **Documentation**
* Documented Windows packaging requirements, installer output, signing
options, and current platform limitations.
* **Tests**
* Expanded automated coverage for Windows packaging, staging, runtime
checks, and process termination.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Copy file name to clipboardExpand all lines: apps/desktop/README.md
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -64,11 +64,15 @@ hdiutil detach "$MOUNT_POINT"
64
64
rmdir "$MOUNT_POINT"
65
65
```
66
66
67
+
### Windows
68
+
69
+
Run `pnpm run dist:win` on a native Windows x64 host; cross-building from macOS is not possible because the staged Host closure contains platform-gated native packages. The output is `dist/Pythinker-<version>-x64-Setup.exe`, an assisted NSIS installer that defaults to a per-user install, offers a per-machine option that requires elevation, and lets you select the installation directory. Artifacts are unsigned unless `WIN_CSC_LINK` and `WIN_CSC_KEY_PASSWORD` are set.
70
+
67
71
## Known limitations
68
72
69
73
The first desktop assembly uses a loopback HTTP Host. The renderer and Host protocol remain unchanged so the application can replace the transport with the IPC carrier reserved by the GUI architecture without changing product features.
70
74
71
-
The signed installer path currently targets macOS. Windows and Linux packaging creates unpacked applications; their installer formats and distribution signing remain release work.
75
+
The signed installer path currently targets macOS. Linux packaging creates an unpacked application; its installer format and distribution signing remain release work.
0 commit comments