Skip to content

Commit 23d6eaa

Browse files
authored
feat: upgrade to Electron 41 (v0.1.0)
* feat: upgrade to Electron 41 and fix terminal creation in packaged builds Upgrade from Electron 33 to 41.0.2 with all native module compatibility fixes. This is the first fully working packaged build — version reset to v0.1.0. Key changes: - Electron 33 → 41.0.2, @electron/rebuild 3 → 4.0.3 - node-pty 1.0.0 → 1.2.0-beta.12 (fixes /dev/ptmx leak, better error msgs) - better-sqlite3 → 12.8.0 (Electron 41 ABI 145 compatible) - tsup target node20 → node22 Root cause fix: node-pty's unixTerminal.js naively replaces 'app.asar' with 'app.asar.unpacked' in the spawn-helper path. When Module._load already redirects requires to the unpacked path, this double-replaces to 'app.asar.unpacked.unpacked' (ENOENT → posix_spawnp failed). Fix: afterPack.cjs hook patches node-pty during electron-builder packaging to use a regex with negative lookahead that avoids the double replacement. * chore: fix eslint config and lint warnings - Exclude **/dist/ and scripts/ from eslint (build output and CJS scripts) - Fix react-hooks/refs warnings in useLaunchSettings (useRef → direct call) - Fix no-explicit-any in terminal-registry (add _loadRenderer to interface) - Move cleanup setState to effect teardown in useTerminalScrollButton * style: format afterPack.cjs with prettier * fix: address PR review comments - Fix err.stack fallback to err.message in server error handler - Pin node-pty to exact version in server package (match root) * chore: sync yarn.lock with pinned node-pty version
1 parent aa9a5a5 commit 23d6eaa

12 files changed

Lines changed: 130 additions & 458 deletions

File tree

.yarn/install-state.gz

-53.8 KB
Binary file not shown.

electron-builder.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ linux:
3737
icon: resources/icon.png
3838

3939
npmRebuild: true
40+
afterPack: scripts/afterPack.cjs
4041
files:
4142
- 'out/**/*'
4243
- 'node_modules/**/*'

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
55
import prettier from 'eslint-config-prettier'
66

77
export default tseslint.config(
8-
{ ignores: ['out/', 'dist/', 'node_modules/', '*.config.js'] },
8+
{ ignores: ['out/', 'dist/', '**/dist/', 'node_modules/', '*.config.js', 'scripts/'] },
99

1010
js.configs.recommended,
1111
...tseslint.configs.recommended,

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vibegrid",
3-
"version": "0.7.1",
3+
"version": "0.1.0",
44
"packageManager": "yarn@4.13.0",
55
"author": "Javier Canizalez <javier-canizalez@outlook.com>",
66
"description": "AI Agent Terminal Manager - Stage Manager for coding agents",
@@ -10,7 +10,7 @@
1010
],
1111
"scripts": {
1212
"dev": "electron-vite dev",
13-
"build:server": "cd packages/server && npx tsup src/index.ts --format cjs --target node20 --external node-pty --external better-sqlite3 --clean",
13+
"build:server": "cd packages/server && npx tsup src/index.ts --format cjs --target node22 --external node-pty --external better-sqlite3 --clean",
1414
"build": "yarn build:server && electron-vite build",
1515
"preview": "electron-vite preview",
1616
"postinstall": "electron-builder install-app-deps",
@@ -54,22 +54,22 @@
5454
"@xterm/addon-web-links": "^0.11.0",
5555
"@xterm/addon-webgl": "^0.19.0",
5656
"@xterm/xterm": "^5.5.0",
57-
"better-sqlite3": "^12.6.2",
57+
"better-sqlite3": "^12.8.0",
5858
"electron-log": "^5.4.3",
5959
"electron-updater": "^6.8.3",
6060
"framer-motion": "^12.36.0",
6161
"lowlight": "^3.3.0",
6262
"lucide-react": "^0.576.0",
6363
"node-cron": "^3.0.3",
64-
"node-pty": "^1.0.0",
64+
"node-pty": "1.2.0-beta.12",
6565
"react": "^19.2.4",
6666
"react-dom": "^19.2.4",
6767
"ws": "^8.18.2",
6868
"zod": "^4.3.6",
6969
"zustand": "^5.0.0"
7070
},
7171
"devDependencies": {
72-
"@electron/rebuild": "^3.7.0",
72+
"@electron/rebuild": "^4.0.3",
7373
"@eslint/js": "^10.0.1",
7474
"@types/better-sqlite3": "^7.6.13",
7575
"@types/d3-hierarchy": "^3.1.7",
@@ -78,7 +78,7 @@
7878
"@types/react-dom": "^19",
7979
"@vitejs/plugin-react": "^4.3.0",
8080
"autoprefixer": "^10.4.0",
81-
"electron": "^33.0.0",
81+
"electron": "^41.0.2",
8282
"electron-builder": "^26.8.1",
8383
"electron-vite": "^5.0.0",
8484
"eslint": "^10.0.3",

packages/server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
"@fastify/websocket": "^11.0.2",
2121
"@modelcontextprotocol/sdk": "^1.27.1",
2222
"@vibegrid/shared": "workspace:*",
23-
"better-sqlite3": "^12.6.2",
23+
"better-sqlite3": "^12.8.0",
2424
"fastify": "^5.3.3",
2525
"node-cron": "^3.0.3",
26-
"node-pty": "^1.0.0",
26+
"node-pty": "1.2.0-beta.12",
2727
"pino": "^9.6.0",
2828
"zod": "^4.3.6"
2929
},

packages/server/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ if (isDirectRun) {
117117

118118
startServer({ port, dataDir }).catch((err) => {
119119
log.error({ err }, '[server] failed to start')
120+
const msg =
121+
'[server] failed to start: ' + (err instanceof Error ? err.stack || err.message : String(err))
122+
process.stderr.write(msg + '\n')
120123
process.exit(1)
121124
})
122125
}

packages/server/tsup.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const NATIVE_MODULE_PATCH = `
4040
if (nativeModules[request]) {
4141
return origLoad.call(this, path.join(nativePath, request), parent, isMain);
4242
}
43-
// Shim the 'bindings' package return a function that resolves
43+
// Shim the 'bindings' package -- return a function that resolves
4444
// addon names from our known unpacked paths
4545
if (request === 'bindings') {
4646
return function(opts) {
@@ -62,7 +62,7 @@ const NATIVE_MODULE_PATCH = `
6262
export default defineConfig({
6363
entry: ['src/index.ts'],
6464
format: ['cjs'],
65-
target: 'node20',
65+
target: 'node22',
6666
clean: true,
6767
banner: {
6868
js: NATIVE_MODULE_PATCH

scripts/afterPack.cjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* electron-builder afterPack hook
3+
*
4+
* Patches node-pty's unixTerminal.js in the unpacked node_modules to fix a
5+
* double-replacement bug with the spawn-helper path.
6+
*
7+
* node-pty does: helperPath.replace('app.asar', 'app.asar.unpacked')
8+
* But our Module._load patch already loads node-pty from app.asar.unpacked,
9+
* so __dirname already contains 'app.asar.unpacked'. The naive replace turns
10+
* 'app.asar.unpacked' into 'app.asar.unpacked.unpacked' (ENOENT).
11+
*
12+
* Fix: replace the naive string replace with a regex that only matches
13+
* 'app.asar' when NOT already followed by '.unpacked'.
14+
*/
15+
const path = require('path')
16+
const fs = require('fs')
17+
18+
module.exports = async function afterPack(context) {
19+
const unpackedNodeModules = path.join(
20+
context.appOutDir,
21+
`${context.packager.appInfo.productFilename}.app`,
22+
'Contents/Resources/app.asar.unpacked/node_modules'
23+
)
24+
25+
const unixTerminalPath = path.join(unpackedNodeModules, 'node-pty/lib/unixTerminal.js')
26+
27+
if (!fs.existsSync(unixTerminalPath)) {
28+
console.log('[afterPack] node-pty/lib/unixTerminal.js not found, skipping patch')
29+
return
30+
}
31+
32+
let content = fs.readFileSync(unixTerminalPath, 'utf8')
33+
34+
// Replace the naive string replace with a regex-based one that uses a
35+
// negative lookahead to avoid matching 'app.asar.unpacked'
36+
const oldLine = "helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');"
37+
const newLine =
38+
"helperPath = helperPath.replace(/app\\.asar(?!\\.unpacked)/g, 'app.asar.unpacked');"
39+
40+
if (content.includes(oldLine)) {
41+
content = content.replace(oldLine, newLine)
42+
fs.writeFileSync(unixTerminalPath, content)
43+
console.log('[afterPack] Patched node-pty unixTerminal.js spawn-helper path fix')
44+
} else if (content.includes('app.asar.unpacked.unpacked')) {
45+
console.warn('[afterPack] WARNING: unixTerminal.js already has double-unpacked issue')
46+
} else {
47+
console.log('[afterPack] node-pty unixTerminal.js already patched or uses different pattern')
48+
}
49+
}

src/renderer/hooks/useLaunchSettings.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export function useLaunchSettings() {
2828
const activeProject = useAppStore((s) => s.activeProject)
2929
const defaultAgent = config?.defaults.defaultAgent || 'claude'
3030

31-
const saved = useRef(loadSaved())
32-
const [selectedAgent, setSelectedAgent] = useState<AgentType>(saved.current.agent || defaultAgent)
33-
const [selectedProject, setSelectedProject] = useState(saved.current.project || '')
34-
const [selectedHost, setSelectedHost] = useState(saved.current.host || 'local')
31+
const saved = loadSaved()
32+
const [selectedAgent, setSelectedAgent] = useState<AgentType>(saved.agent || defaultAgent)
33+
const [selectedProject, setSelectedProject] = useState(saved.project || '')
34+
const [selectedHost, setSelectedHost] = useState(saved.host || 'local')
3535
const [localBranches, setLocalBranches] = useState<string[]>([])
3636
const [remoteBranches, setRemoteBranches] = useState<string[]>([])
3737
const [currentBranch, setCurrentBranch] = useState<string | null>(null)
@@ -50,6 +50,7 @@ export function useLaunchSettings() {
5050
useEffect(() => {
5151
if (selectedProject && config?.projects) {
5252
const exists = config.projects.some((p) => p.name === selectedProject)
53+
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: clear invalid selection when config changes
5354
if (!exists) setSelectedProject('')
5455
}
5556
}, [config?.projects, selectedProject])
@@ -58,6 +59,7 @@ export function useLaunchSettings() {
5859
useEffect(() => {
5960
if (activeProject && config?.projects) {
6061
const exists = config.projects.some((p) => p.name === activeProject)
62+
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: sync UI selection to sidebar state
6163
if (exists) setSelectedProject(activeProject)
6264
}
6365
}, [activeProject, config?.projects])
@@ -68,6 +70,7 @@ export function useLaunchSettings() {
6870
// Load branches when project changes
6971
useEffect(() => {
7072
if (!activeProjectPath) {
73+
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: reset state when project deselected
7174
setLocalBranches([])
7275
setRemoteBranches([])
7376
setCurrentBranch(null)

src/renderer/hooks/useTerminalScrollButton.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ export function useTerminalScrollButton(terminalId: string | null | undefined) {
1010
const [showScrollBtn, setShowScrollBtn] = useState(false)
1111

1212
useEffect(() => {
13-
if (!terminalId) {
14-
setShowScrollBtn(false)
15-
return
16-
}
13+
if (!terminalId) return
1714

1815
let scrollDispose: (() => void) | undefined
1916
const check = (): void => setShowScrollBtn(!isAtBottom(terminalId))
@@ -26,6 +23,7 @@ export function useTerminalScrollButton(terminalId: string | null | undefined) {
2623
return () => {
2724
readyDispose()
2825
scrollDispose?.()
26+
setShowScrollBtn(false)
2927
}
3028
}, [terminalId])
3129

0 commit comments

Comments
 (0)