Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Release

on:
pull_request:
types: [closed]
branches:
- main

jobs:
release:
if: >-
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.title, 'chore: release')
runs-on: macos-latest

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Get version from package.json
id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install

- name: Build desktop app
run: bun run build:desktop

- name: Package macOS DMG
run: bunx electron-builder --config electron-builder.yml --mac
env:
CSC_IDENTITY_AUTO_DISCOVERY: false

- name: Create GitHub release and upload DMG
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.version }}
name: v${{ steps.version.outputs.version }}
generate_release_notes: true
files: release/*.dmg
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
64 changes: 59 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ Stave is an Electron-based AI coding workspace built with Bun, React, Vite, and

## Prerequisites

- Bun
- Node.js
- **Bun** — package manager and script runner
- **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.
- **C++ build toolchain** — required for compiling `better-sqlite3` and `node-pty` native modules:
- macOS: Xcode Command Line Tools (`xcode-select --install`)
- Linux: `build-essential` (`sudo apt install build-essential`)
- Windows: Visual Studio Build Tools 2022 with the **Desktop development with C++** workload (VC++ tools) and Python installed. See the [node-gyp Windows docs](https://github.com/nodejs/node-gyp#on-windows) for details. Example with Chocolatey:
- `choco install visualstudio2022buildtools python --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"`
- a working `claude` CLI login if you want Claude support
- a working `codex` CLI login if you want Codex support
- `pyright-langserver` or `basedpyright-langserver` on your PATH if you want Python LSP support in the editor
Expand All @@ -48,6 +53,14 @@ codex login
bun install
```

`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.

If you need to skip the native rebuild (e.g. in a CI environment that only runs web builds), set `SKIP_ELECTRON_REBUILD=1`:

```bash
SKIP_ELECTRON_REBUILD=1 bun install
```

## Development

```bash
Expand Down Expand Up @@ -78,24 +91,65 @@ bun run dev:desktop:poll
- `bun run package:linux:appimage`
- `bun run package:linux:deb`

## Running the desktop app

The primary way to build and launch Stave locally is:

```bash
bun run run:desktop:built
```

This single command:
1. Recompiles native modules (`better-sqlite3`, `node-pty`) against the Electron ABI
2. Applies the Electron 41 `better-sqlite3` C++ patch (`info.HolderV2()`)
3. Runs `electron-vite build` to produce the production bundle
4. Launches the app:
- **macOS** — packages with `electron-builder --dir` and opens `Stave.app` so the OS titlebar shows "Stave" instead of "Electron"
- **Linux / Windows** — runs `electron .` with `STAVE_RUNTIME_PROFILE=production`

## Desktop packaging

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:
### Why native modules need rebuilding

`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.

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.

### Manual rebuild

If you reinstall packages with `--ignore-scripts`, or if your native modules become out of sync for any reason, rebuild manually:

```bash
bun run rebuild:electron-deps
```

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.
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.

Useful packaging commands:
### Packaging commands

```bash
# Build and run locally (primary workflow)
bun run run:desktop:built

# Package as unpacked directory
bun run package:desktop

# Linux targets
bun run package:linux:dir
bun run package:linux:appimage
bun run package:linux:deb
```

### Troubleshooting

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

## Docs

Stable project documentation now lives under `docs/`.
Expand Down
1 change: 1 addition & 0 deletions electron/main/ipc/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function registerPersistenceHandlers() {
});
event.returnValue = { ok: true };
} catch (error) {
console.error("[persistence] upsert-workspace-sync failed:", error);
event.returnValue = { ok: false, message: String(error) };
}
});
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
},
"type": "module",
"main": "out/main/index.js",
"engines": {
"node": ">=20"
},
"scripts": {
"postinstall": "node scripts/rebuild-electron-deps.mjs",
"server-dev": "bun run server/dev-server.ts",
"dev": "vite",
"dev:all": "concurrently -n server,client -c blue,green \"bun run server-dev\" \"bun run dev\"",
Expand Down
12 changes: 6 additions & 6 deletions scripts/patch-better-sqlite3-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ const PATCH_TARGETS = [
{
filePath: "node_modules/better-sqlite3/src/objects/statement.cpp",
signature: "NODE_GETTER(Statement::JS_busy) {",
from: "Statement* stmt = Unwrap<Statement>(info.This());",
to: "Statement* stmt = Unwrap<Statement>(info.HolderV2());",
from: "Unwrap<Statement>(PROPERTY_HOLDER(info))",
to: "Unwrap<Statement>(info.HolderV2())",
},
{
filePath: "node_modules/better-sqlite3/src/objects/database.cpp",
signature: "NODE_GETTER(Database::JS_open) {",
from: "info.GetReturnValue().Set(Unwrap<Database>(info.This())->open);",
to: "info.GetReturnValue().Set(Unwrap<Database>(info.HolderV2())->open);",
from: "Unwrap<Database>(PROPERTY_HOLDER(info))",
to: "Unwrap<Database>(info.HolderV2())",
},
{
filePath: "node_modules/better-sqlite3/src/objects/database.cpp",
signature: "NODE_GETTER(Database::JS_inTransaction) {",
from: "Database* db = Unwrap<Database>(info.This());",
to: "Database* db = Unwrap<Database>(info.HolderV2());",
from: "Unwrap<Database>(PROPERTY_HOLDER(info))",
to: "Unwrap<Database>(info.HolderV2())",
},
];

Expand Down
19 changes: 19 additions & 0 deletions scripts/rebuild-electron-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ const nativeModules = ["better-sqlite3", "node-pty"];

export function resolveElectronVersion(args = {}) {
const repoRoot = args.repoRoot ?? defaultRepoRoot;

// Prefer the actual installed version from node_modules/electron/package.json
// so that the compiled ABI matches exactly what is on disk, even when
// package.json contains a semver range like "^41.0.0".
const installedElectronPkgPath = path.join(repoRoot, "node_modules", "electron", "package.json");
try {
const installedPkg = JSON.parse(readFileSync(installedElectronPkgPath, "utf8"));
if (typeof installedPkg.version === "string" && installedPkg.version.length > 0) {
return installedPkg.version;
}
} catch {
// Fall through to package.json derivation below.
}

// Fallback: strip the semver prefix from the devDependencies range.
const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
const rawElectronVersion = packageJson.devDependencies?.electron;

Expand Down Expand Up @@ -83,6 +98,10 @@ function isDirectExecution() {
}

if (isDirectExecution()) {
if (process.env.SKIP_ELECTRON_REBUILD) {
console.log("SKIP_ELECTRON_REBUILD is set — skipping Electron native module rebuild.");
process.exit(0);
}
try {
rebuildElectronDeps();
} catch (error) {
Expand Down
80 changes: 0 additions & 80 deletions skills/stave-patch-release/SKILL.md

This file was deleted.

75 changes: 0 additions & 75 deletions skills/stave-patch-release/references/stave-release-checklist.md

This file was deleted.

Loading