Skip to content

Commit 99c6793

Browse files
astyfxclaude
andcommitted
fix: harden better-sqlite3 Electron ABI compatibility and install workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4e83405 commit 99c6793

12 files changed

Lines changed: 237 additions & 165 deletions

File tree

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

README.md

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ Stave is an Electron-based AI coding workspace built with Bun, React, Vite, and
2929

3030
## Prerequisites
3131

32-
- Bun
33-
- Node.js
32+
- **Bun** — package manager and script runner
33+
- **Node.js ≥ 20** (Node 22 LTS recommended — see `.nvmrc`). The project pins a version in `.nvmrc`; if you use `nvm` just run `nvm use` after cloning.
34+
- **C++ build toolchain** — required for compiling `better-sqlite3` and `node-pty` native modules:
35+
- macOS: Xcode Command Line Tools (`xcode-select --install`)
36+
- Linux: `build-essential` (`sudo apt install build-essential`)
37+
- Windows: Windows Build Tools (`npm install -g windows-build-tools`)
3438
- a working `claude` CLI login if you want Claude support
3539
- a working `codex` CLI login if you want Codex support
3640
- `pyright-langserver` or `basedpyright-langserver` on your PATH if you want Python LSP support in the editor
@@ -48,6 +52,14 @@ codex login
4852
bun install
4953
```
5054

55+
`bun install` automatically runs a `postinstall` hook that patches `better-sqlite3` for Electron 41 compatibility and recompiles both `better-sqlite3` and `node-pty` against the Electron ABI. This is required because these native modules must be compiled for Electron's internal Node runtime, not the host Node version.
56+
57+
If you need to skip the native rebuild (e.g. in a CI environment that only runs web builds), set `SKIP_ELECTRON_REBUILD=1`:
58+
59+
```bash
60+
SKIP_ELECTRON_REBUILD=1 bun install
61+
```
62+
5163
## Development
5264

5365
```bash
@@ -78,24 +90,65 @@ bun run dev:desktop:poll
7890
- `bun run package:linux:appimage`
7991
- `bun run package:linux:deb`
8092

93+
## Running the desktop app
94+
95+
The primary way to build and launch Stave locally is:
96+
97+
```bash
98+
bun run run:desktop:built
99+
```
100+
101+
This single command:
102+
1. Recompiles native modules (`better-sqlite3`, `node-pty`) against the Electron ABI
103+
2. Applies the Electron 41 `better-sqlite3` C++ patch (`info.HolderV2()`)
104+
3. Runs `electron-vite build` to produce the production bundle
105+
4. Launches the app:
106+
- **macOS** — packages with `electron-builder --dir` and opens `Stave.app` so the OS titlebar shows "Stave" instead of "Electron"
107+
- **Linux / Windows** — runs `electron .` with `STAVE_RUNTIME_PROFILE=production`
108+
81109
## Desktop packaging
82110

83-
The desktop packaging scripts and `bun run run:desktop:built` now rebuild native Electron modules automatically before bundling or launching the built app. On macOS, `bun run run:desktop:built` now launches the unpacked `Stave.app` bundle so the OS shows the app as `Stave` instead of `Electron`. If your local install gets out of sync after `bun install`, run the rebuild manually:
111+
### Why native modules need rebuilding
112+
113+
`better-sqlite3` and `node-pty` are C++ native modules. When you run `bun install`, they are compiled for the **host Node.js** ABI. Electron bundles its **own Node.js runtime** with a different ABI, so the modules must be recompiled for Electron specifically. Additionally, Electron 41 changed how V8 `PropertyCallbackInfo` works in getter callbacks, requiring a source-level patch to `better-sqlite3` (`info.This()``info.HolderV2()`) before the C++ is compiled.
114+
115+
All of this is handled automatically by `postinstall` and by each packaging/run script. You should not need to think about it in normal development.
116+
117+
### Manual rebuild
118+
119+
If you reinstall packages with `--ignore-scripts`, or if your native modules become out of sync for any reason, rebuild manually:
84120

85121
```bash
86122
bun run rebuild:electron-deps
87123
```
88124

89-
This rebuild now patches `better-sqlite3` in the Electron 41 getter contexts that need `HolderV2()`, then runs `node-gyp rebuild --runtime=electron --build-from-source` for `better-sqlite3` and `node-pty` using the current Electron version and host architecture. Electron headers are cached under `.cache/node-gyp/` in the repo so the rebuild does not depend on a writable home-directory cache.
125+
The rebuild reads the **actual installed Electron version** from `node_modules/electron/package.json`, so the compiled ABI always matches what is on disk — regardless of semver ranges in `package.json`. Electron headers are cached under `.cache/node-gyp/` inside the repo so the rebuild does not depend on a writable home-directory cache.
90126

91-
Useful packaging commands:
127+
### Packaging commands
92128

93129
```bash
130+
# Build and run locally (primary workflow)
94131
bun run run:desktop:built
132+
133+
# Package as unpacked directory
134+
bun run package:desktop
135+
136+
# Linux targets
95137
bun run package:linux:dir
138+
bun run package:linux:appimage
96139
bun run package:linux:deb
97140
```
98141

142+
### Troubleshooting
143+
144+
| Symptom | Likely cause | Fix |
145+
|---|---|---|
146+
| `NODE_MODULE_VERSION` mismatch on launch | Native modules compiled for host Node, not Electron | `bun run rebuild:electron-deps` |
147+
| App crashes or freezes on first persist | Electron 41 `HolderV2` patch not applied | `bun run rebuild:electron-deps` |
148+
| `[persistence] upsert-workspace-sync failed` in Electron logs | Same as above, check the full error message | `bun run rebuild:electron-deps` |
149+
| `Patch signature not found` error during rebuild | `better-sqlite3` version changed or `node_modules` corrupted | `bun install && bun run rebuild:electron-deps` |
150+
| Build fails with `node-gyp` errors | Missing C++ toolchain | Install Xcode CLT / build-essential (see Prerequisites) |
151+
99152
## Docs
100153

101154
Stable project documentation now lives under `docs/`.

electron/main/ipc/persistence.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function registerPersistenceHandlers() {
5555
});
5656
event.returnValue = { ok: true };
5757
} catch (error) {
58+
console.error("[persistence] upsert-workspace-sync failed:", error);
5859
event.returnValue = { ok: false, message: String(error) };
5960
}
6061
});

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
},
1010
"type": "module",
1111
"main": "out/main/index.js",
12+
"engines": {
13+
"node": ">=20"
14+
},
1215
"scripts": {
16+
"postinstall": "node scripts/rebuild-electron-deps.mjs",
1317
"server-dev": "bun run server/dev-server.ts",
1418
"dev": "vite",
1519
"dev:all": "concurrently -n server,client -c blue,green \"bun run server-dev\" \"bun run dev\"",

scripts/rebuild-electron-deps.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ const nativeModules = ["better-sqlite3", "node-pty"];
1010

1111
export function resolveElectronVersion(args = {}) {
1212
const repoRoot = args.repoRoot ?? defaultRepoRoot;
13+
14+
// Prefer the actual installed version from node_modules/electron/package.json
15+
// so that the compiled ABI matches exactly what is on disk, even when
16+
// package.json contains a semver range like "^41.0.0".
17+
const installedElectronPkgPath = path.join(repoRoot, "node_modules", "electron", "package.json");
18+
try {
19+
const installedPkg = JSON.parse(readFileSync(installedElectronPkgPath, "utf8"));
20+
if (typeof installedPkg.version === "string" && installedPkg.version.length > 0) {
21+
return installedPkg.version;
22+
}
23+
} catch {
24+
// Fall through to package.json derivation below.
25+
}
26+
27+
// Fallback: strip the semver prefix from the devDependencies range.
1328
const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
1429
const rawElectronVersion = packageJson.devDependencies?.electron;
1530

@@ -83,6 +98,10 @@ function isDirectExecution() {
8398
}
8499

85100
if (isDirectExecution()) {
101+
if (process.env.SKIP_ELECTRON_REBUILD) {
102+
console.log("SKIP_ELECTRON_REBUILD is set — skipping Electron native module rebuild.");
103+
process.exit(0);
104+
}
86105
try {
87106
rebuildElectronDeps();
88107
} catch (error) {

skills/stave-patch-release/SKILL.md

Lines changed: 0 additions & 80 deletions
This file was deleted.

skills/stave-patch-release/references/stave-release-checklist.md

Lines changed: 0 additions & 75 deletions
This file was deleted.

skills/stave-release/SKILL.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
name: stave-release
3+
description: Release workflow for the Stave repository that bumps the patch version, generates release notes with `conventional-changelog`, and opens a pull request against `main`. Use when the user asks to cut the next patch release, ship the current changes as a versioned release, or prepare a release PR. After the PR merges, the repository's GitHub Actions workflow builds and publishes the release artifacts automatically.
4+
---
5+
6+
# Stave Release
7+
8+
Use this skill to create a versioned release PR for the Stave repository.
9+
10+
Read `references/stave-release-checklist.md` for the exact sequence and repair rules.
11+
12+
## Workflow
13+
14+
1. Detect the repository root.
15+
- Run `git rev-parse --show-toplevel` to find the repo root. Never assume a hardcoded path.
16+
- All subsequent file reads and writes use this path as the base.
17+
18+
2. Inspect the release state before editing.
19+
- Read `package.json` to load the current version.
20+
- Run `git status --short` to confirm the working tree is clean (or note what is uncommitted).
21+
- Run `git remote -v` and confirm `origin` exists.
22+
- Run `git tag --list 'v*' --sort=-version:refname | head -5` to find the most recent semver tag. Incremental `conventional-changelog` generation depends on at least one prior tag.
23+
- If no prior semver tag exists, stop and explain that a baseline `vX.Y.Z` tag is required before incremental changelog generation is safe.
24+
25+
3. Bump only the patch version.
26+
- Increment `package.json` from `x.y.z` to `x.y.(z+1)`.
27+
- Do not bump if the working tree already reflects the intended release version.
28+
29+
4. Generate release notes.
30+
- Run: `bunx --bun conventional-changelog-cli -p conventionalcommits -i CHANGELOG.md -s`
31+
- Inspect the newly generated top section. If it is empty, heading-only, or missing meaningful bullets, automatically append a concise 3–7 bullet summary derived from the actual diff since the previous tag:
32+
- Use `git diff --stat <prev-tag>..HEAD` and `git diff --name-only <prev-tag>..HEAD` as signals.
33+
- Summarize user-visible or architecture-significant outcomes — not file lists.
34+
- Update `README.md` and any other release-facing docs that changed as part of the shipped behavior so docs and changelog stay aligned.
35+
- Review the generated notes before committing.
36+
37+
5. Verify before commit.
38+
- Run `bun run typecheck` at minimum.
39+
- Run focused tests for changed areas (`bun test` or `bun run test:ci` when scope is broad).
40+
- Report any verification that could not run.
41+
42+
6. Stage and commit directly on the current branch (or in a worktree if the working tree is unclean).
43+
- Stage: `git add -A`
44+
- Commit with a Conventional Commit: `chore: release x.y.z`
45+
- Do not amend a previously pushed release commit; always create a new commit.
46+
47+
7. Push the release branch and open a PR.
48+
- Push to `origin`: `git push origin <branch>`
49+
- Open a PR against `main` using `gh pr create --base main`.
50+
- PR title: `chore: release x.y.z`
51+
- PR body must include: a bullet summary of shipped changes, the verification commands run and their results, and the `🤖 Generated with Claude Code` footer.
52+
- **Never push directly to `main`.** All releases land via PR.
53+
54+
8. Report the outcome.
55+
- State the new version.
56+
- State the release commit hash and message.
57+
- State the PR URL.
58+
- State which verification commands ran and their results.
59+
- Note that GitHub Actions will build and publish release artifacts after the PR merges.
60+
61+
## Guardrails
62+
63+
- **Never push directly to `main`.** All releases land via PR.
64+
- Never hardcode a repository path. Always derive it with `git rev-parse --show-toplevel`.
65+
- Do not bump the version twice if the working tree already reflects the intended release.
66+
- Do not create a non-Conventional commit.
67+
- Do not hand-write release notes when `conventional-changelog` is the required path.
68+
- Do not silently skip changelog review or release-facing doc updates when shipped behavior changed.
69+
- Do not create a local semver tag before the PR is merged. Tag the merged `main` commit after merge.
70+
- If verification fails, stop and surface the failure unless the user explicitly accepts releasing anyway.

0 commit comments

Comments
 (0)