claude-code-session-manager 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-BBlKMDCL.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-CHTpW79G.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
13
  <link rel="stylesheet" crossorigin href="./assets/index-DMIi9YZH.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -11,6 +11,7 @@
11
11
  "bin/",
12
12
  ".claude-plugin/",
13
13
  "plugins/",
14
+ "scripts/postinstall.cjs",
14
15
  "src/main/",
15
16
  "src/preload/",
16
17
  "dist/index.html",
@@ -26,7 +27,7 @@
26
27
  "build": "vite build",
27
28
  "typecheck": "tsc --noEmit",
28
29
  "start": "electron .",
29
- "postinstall": "electron-rebuild -f -w node-pty",
30
+ "postinstall": "node scripts/postinstall.cjs",
30
31
  "prepublishOnly": "vite build",
31
32
  "test:unit": "vitest run",
32
33
  "test:verify": "node src/main/__tests__/runVerify.test.cjs",
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * postinstall — rebuild node-pty against the bundled Electron's ABI.
6
+ *
7
+ * The terminal needs node-pty's native binary to match the Electron we ship.
8
+ * We run electron-rebuild, but a failure here MUST NOT fail `npm i -g` — that
9
+ * would block the entire app (config tabs, scheduler, etc.) over one native
10
+ * module. So we run it best-effort and, on failure, print the exact manual fix
11
+ * and exit 0. pty.cjs surfaces the same guidance in-app if a tab can't start.
12
+ */
13
+
14
+ const { spawnSync } = require('node:child_process');
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ // Resolve the electron-rebuild bin from THIS package's own node_modules first,
19
+ // so a global install never has `npx` reach out to the registry (and risk
20
+ // pulling the wrong/deprecated `electron-rebuild` package). Fall back to npx.
21
+ function rebuildArgv() {
22
+ const pkgRoot = path.join(__dirname, '..');
23
+ const local = path.join(pkgRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'electron-rebuild.cmd' : 'electron-rebuild');
24
+ if (fs.existsSync(local)) return [local, ['-f', '-w', 'node-pty']];
25
+ return [process.platform === 'win32' ? 'npx.cmd' : 'npx', ['electron-rebuild', '-f', '-w', 'node-pty']];
26
+ }
27
+
28
+ function run() {
29
+ const [cmd, args] = rebuildArgv();
30
+ const r = spawnSync(cmd, args, { stdio: 'inherit', cwd: path.join(__dirname, '..') });
31
+ return r.status === 0 && !r.error;
32
+ }
33
+
34
+ if (run()) process.exit(0);
35
+
36
+ const mac = process.platform === 'darwin';
37
+ const bar = '─'.repeat(64);
38
+ console.warn(`\n${bar}`);
39
+ console.warn('claude-code-session-manager: node-pty rebuild did not complete.');
40
+ console.warn('The app will still launch, but terminal tabs may not start until');
41
+ console.warn('the native module is rebuilt for this Electron. To fix it:');
42
+ console.warn('');
43
+ console.warn(' cd "' + __dirname.replace(/\/scripts$/, '') + '"');
44
+ console.warn(' npx electron-rebuild -f -w node-pty');
45
+ if (mac) {
46
+ console.warn('');
47
+ console.warn(' # macOS: if the rebuild fails, install the compiler first:');
48
+ console.warn(' xcode-select --install');
49
+ }
50
+ console.warn(`${bar}\n`);
51
+ // Exit 0 — never fail the install over the terminal native module.
52
+ process.exit(0);
@@ -25,10 +25,14 @@ function userBinDirs() {
25
25
  ];
26
26
  }
27
27
 
28
- /** Electron's PATH prefixed with the user/Homebrew bin dirs the value to put
29
- * in a spawned child's PATH so `claude`, node, git, etc. resolve on macOS. */
28
+ /** Electron's PATH with the user/Homebrew bin dirs APPENDED as a fallback — the
29
+ * value to put in a spawned child's PATH so `claude`, node, git, etc. resolve
30
+ * on macOS even under a stripped Finder/Dock PATH. Appended (not prepended) so
31
+ * a user's version manager (nvm/asdf/volta/mise) earlier in PATH still wins and
32
+ * isn't shadowed by /opt/homebrew. */
30
33
  function pathWithUserBins() {
31
- return `${userBinDirs().join(':')}:${process.env.PATH || ''}`;
34
+ const base = process.env.PATH || '';
35
+ return base ? `${base}:${userBinDirs().join(':')}` : userBinDirs().join(':');
32
36
  }
33
37
 
34
38
  function cleanChildEnv(extra = {}) {
@@ -17,7 +17,16 @@
17
17
  */
18
18
 
19
19
  const { ipcMain } = require('electron');
20
- const pty = require('node-pty');
20
+ // Guard like pty.cjs: a node-pty ABI mismatch must not crash the whole app at
21
+ // module load (index.cjs requires this unconditionally). install() reports a
22
+ // clean error if the native module is unavailable.
23
+ let pty = null;
24
+ try {
25
+ pty = require('node-pty');
26
+ } catch (e) {
27
+ // eslint-disable-next-line no-console
28
+ console.error('[pluginInstall] node-pty failed to load:', e?.message);
29
+ }
21
30
  const path = require('node:path');
22
31
  const os = require('node:os');
23
32
  const fs = require('node:fs');
@@ -72,6 +81,10 @@ function childEnv() {
72
81
  */
73
82
  function runStep({ slug, args, register, unregister }) {
74
83
  return new Promise((resolve) => {
84
+ if (!pty) {
85
+ resolve({ ok: false, exitCode: -1, error: 'node-pty unavailable (native module failed to load)' });
86
+ return;
87
+ }
75
88
  const claudeBin = resolveClaudeBin();
76
89
  let proc;
77
90
  try {
package/src/main/pty.cjs CHANGED
@@ -1,4 +1,17 @@
1
- const pty = require('node-pty');
1
+ // Load the native node-pty lazily-guarded: if its prebuilt/rebuilt binary
2
+ // doesn't match this Electron's ABI, a bare top-level require would throw and
3
+ // crash the WHOLE app at startup (index.cjs requires this module). Instead we
4
+ // capture the error and surface an actionable message when a tab is opened, so
5
+ // the app still boots and the user sees exactly how to fix the terminal.
6
+ let pty = null;
7
+ let ptyLoadError = null;
8
+ try {
9
+ pty = require('node-pty');
10
+ } catch (e) {
11
+ ptyLoadError = e;
12
+ // eslint-disable-next-line no-console
13
+ console.error('[pty] node-pty failed to load:', e?.message);
14
+ }
2
15
  const { ipcMain } = require('electron');
3
16
  const path = require('node:path');
4
17
  const os = require('node:os');
@@ -8,6 +21,37 @@ const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
8
21
  const { checkInsideHome } = require('./lib/insideHome.cjs');
9
22
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
10
23
 
24
+ // Absolute path to the installed package root (src/main/ -> ../../), shown in
25
+ // the remediation message so the user can cd there and rebuild.
26
+ const PKG_DIR = path.join(__dirname, '..', '..');
27
+
28
+ /**
29
+ * ANSI-formatted terminal text explaining a native-module / immediate-exit
30
+ * failure and exactly how to fix it. Written straight into the tab's output so
31
+ * a dead terminal explains itself instead of just going blank.
32
+ */
33
+ function nativeModuleHelp(reason) {
34
+ const mac = process.platform === 'darwin';
35
+ const lines = [
36
+ '',
37
+ `\x1b[1;33m[session-manager] Terminal could not start.\x1b[0m`,
38
+ `\x1b[33m${reason}\x1b[0m`,
39
+ '',
40
+ `This is almost always the node-pty native module not matching this`,
41
+ `Electron build. Rebuild it once:`,
42
+ '',
43
+ ` \x1b[36mcd ${PKG_DIR}\x1b[0m`,
44
+ ` \x1b[36mnpx electron-rebuild -f -w node-pty\x1b[0m`,
45
+ '',
46
+ mac ? `macOS: if the rebuild fails, install the compiler first:` : '',
47
+ mac ? ` \x1b[36mxcode-select --install\x1b[0m` : '',
48
+ mac ? '' : '',
49
+ `Then quit and reopen session-manager.`,
50
+ '',
51
+ ].filter((l) => l !== '');
52
+ return lines.join('\r\n') + '\r\n';
53
+ }
54
+
11
55
  /**
12
56
  * PtyManager — owns every claude PTY process, keyed by tabId (renderer-generated UUID).
13
57
  * Streams output to the renderer via IPC channels scoped by tabId.
@@ -26,6 +70,15 @@ class PtyManager {
26
70
  spawn({ tabId, cwd, cols = 120, rows = 30 }) {
27
71
  console.log('[pty] spawn requested', { tabId });
28
72
 
73
+ // Native module unavailable — explain in the tab and report a clean exit
74
+ // rather than throwing (which would surface as an opaque IPC error).
75
+ if (!pty || ptyLoadError) {
76
+ const reason = `node-pty failed to load: ${ptyLoadError?.message ?? 'module missing'}`;
77
+ sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(reason));
78
+ setImmediate(() => sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode: 1, signal: undefined }));
79
+ return { pid: null, cwd, reattached: false, error: 'node-pty unavailable' };
80
+ }
81
+
29
82
  // Validate that cwd is inside homedir before widening the allowed-root set.
30
83
  if (cwd) {
31
84
  const r = checkInsideHome(cwd);
@@ -88,11 +141,19 @@ class PtyManager {
88
141
  });
89
142
  } catch (err) {
90
143
  console.error('[pty] pty.spawn threw:', err);
91
- throw err;
144
+ // Surface the cause in the tab + report a clean exit rather than throwing
145
+ // an opaque IPC error (covers a missing/invalid cwd or a native fault).
146
+ sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(`pty.spawn failed: ${err?.message ?? String(err)}`));
147
+ setImmediate(() => sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode: 1, signal: undefined }));
148
+ return { pid: null, cwd, reattached: false, error: String(err?.message || 'spawn-failed') };
92
149
  }
93
150
  console.log('[pty] spawned pid=', proc.pid, 'for tabId=', tabId);
94
151
 
152
+ const spawnedAt = Date.now();
153
+ let gotData = false;
154
+
95
155
  proc.onData((data) => {
156
+ gotData = true;
96
157
  sendIfAlive(this.window, `pty:data:${tabId}`, data);
97
158
  });
98
159
 
@@ -105,11 +166,19 @@ class PtyManager {
105
166
  this.sessions.delete(tabId);
106
167
  return;
107
168
  }
169
+ // Fast-exit detector: a shell that dies in <1.2s with a non-zero status
170
+ // and never printed anything almost certainly couldn't exec (broken
171
+ // node-pty spawn-helper / ABI on macOS). Explain it in the tab instead of
172
+ // leaving a blank, instantly-closed terminal. Guarded by !gotData so a
173
+ // real interactive shell (which prints a prompt immediately) is exempt.
174
+ if (!gotData && exitCode !== 0 && Date.now() - spawnedAt < 1200) {
175
+ sendIfAlive(this.window, `pty:data:${tabId}`, nativeModuleHelp(`The shell exited immediately (code ${exitCode}).`));
176
+ }
108
177
  sendIfAlive(this.window, `pty:exit:${tabId}`, { exitCode, signal });
109
178
  this.sessions.delete(tabId);
110
179
  });
111
180
 
112
- this.sessions.set(tabId, { proc, cwd, created: Date.now() });
181
+ this.sessions.set(tabId, { proc, cwd, created: spawnedAt });
113
182
  return { pid: proc.pid, cwd, reattached: false };
114
183
  }
115
184