handmux 0.14.0 → 0.16.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/bin/handmux.js CHANGED
@@ -36,12 +36,15 @@ import { codexHooksStatus, installCodexHooks, uninstallCodexHooks } from '../src
36
36
  import { statusLineStatus, installStatusLine, uninstallStatusLine, composeHint } from '../src/cli/statusLine.js';
37
37
  import { claudeUsagePath } from '../src/usage.js';
38
38
  import { probe } from '../src/cli/probe.js';
39
- import { notifyUpdate, runUpdateCheck, PKG_NAME } from '../src/cli/updateCheck.js';
39
+ import { notifyUpdate, runUpdateCheck, isBrewInstall, PKG_NAME } from '../src/cli/updateCheck.js';
40
40
  import { t, initLocale, setLocale } from '../src/cli/i18n/index.js';
41
41
  import { runPush } from '../src/cli/pushCmd.js';
42
42
 
43
43
  const HOME = homedir();
44
44
  const SELF = fileURLToPath(import.meta.url);
45
+ // Symlink-resolved entry path — a brew (tap) install is symlinked from the prefix's bin into the Cellar,
46
+ // so isBrewInstall() needs the real target, not the symlink, to spot the `/Cellar/` segment.
47
+ const SELF_REAL = (() => { try { return fs.realpathSync(SELF); } catch { return SELF; } })();
45
48
  const HOOKS_SRC = path.resolve(path.dirname(SELF), '../hooks'); // server/hooks (bundled scripts)
46
49
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
47
50
 
@@ -162,6 +165,7 @@ function version() {
162
165
  // restart a running instance; on success we refresh the update cache so the "upgrade available" notice
163
166
  // clears, and remind them to `handmux restart` to actually run the new code.
164
167
  function updateCmd() {
168
+ if (isBrewInstall(SELF_REAL)) { console.log(t('update.brew')); return; }
165
169
  console.log(t('update.running'));
166
170
  const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@latest`], { stdio: 'inherit' });
167
171
  if (r.status === 0) {
@@ -176,7 +180,7 @@ function updateCmd() {
176
180
 
177
181
  // Best-effort upgrade notice from the cached "latest version" (never blocks; refreshes in the background).
178
182
  function maybeNotifyUpdate() {
179
- notifyUpdate(HOME, { version: requireOpt('../package.json').version, selfPath: SELF });
183
+ notifyUpdate(HOME, { version: requireOpt('../package.json').version, selfPath: SELF_REAL });
180
184
  }
181
185
 
182
186
  async function start() {
@@ -267,9 +271,26 @@ async function start() {
267
271
  await waitAndPrint(true);
268
272
  }
269
273
 
274
+ // Best-effort SIGTERM to the supervisor's recorded children. The supervisor reaps its own children on a
275
+ // graceful SIGTERM (supervisor.js shutdown), but if it was SIGKILL'd or crashed, its server/tunnel children
276
+ // are orphaned with no reaper — the classic "stray cloudflared after stop". Only called on the dead-
277
+ // supervisor branch, so these pids have no live parent to clean them up.
278
+ function reapOrphans(st) {
279
+ for (const pid of [st.serverPid, st.tunnelPid]) {
280
+ if (isAlive(pid)) { try { process.kill(pid, 'SIGTERM'); } catch { /* raced away */ } }
281
+ }
282
+ }
283
+
270
284
  function stop() {
271
285
  const st = readState(HOME);
272
- if (!st || !isAlive(st.supervisorPid)) { console.log(t('stop.notRunning')); clearState(HOME); return; }
286
+ if (!st || !isAlive(st.supervisorPid)) {
287
+ // Supervisor already gone — but a non-graceful death may have orphaned its children. Reap them before
288
+ // dropping the state file, or nothing ever will.
289
+ if (st) reapOrphans(st);
290
+ console.log(t('stop.notRunning'));
291
+ clearState(HOME);
292
+ return;
293
+ }
273
294
  try { process.kill(st.supervisorPid, 'SIGTERM'); } catch { /* race: already gone */ }
274
295
  console.log(t('stop.stopped', { pid: st.supervisorPid }));
275
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code / Codex — anything a terminal can run — from your phone.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -57,13 +57,13 @@
57
57
  "vitest": "^2.0.0"
58
58
  },
59
59
  "whatsNew": [
60
+ {"version":"0.16.0","date":"2026-07-15","zh":"分屏布局地图 · 手机也能建/删窗格","en":"Split-layout map · create & close panes from the phone"},
61
+ {"version":"0.15.0","date":"2026-07-14","zh":"键盘弹起终端自适应 · 全屏程序可上下滚动","en":"Keyboard-aware terminal · scroll inside full-screen apps"},
60
62
  {"version":"0.14.0","date":"2026-07-13","zh":"终端文字选中拷贝 · 脚本复用推送通道","en":"Terminal select & copy · scripts reuse the push channel"},
61
63
  {"version":"0.13.0","date":"2026-07-12","zh":"终端文件路径高亮可点","en":"Tappable, highlighted file paths"},
62
64
  {"version":"0.12.3","date":"2026-07-11","zh":"接管会话可自定义名称 + Homebrew 一键安装","en":"Name a takeover session · Homebrew install"},
63
65
  {"version":"0.12.2","date":"2026-07-11","zh":"修复主屏图标偏大 / 换图标后不刷新","en":"Home-screen icon: right size & always fresh"},
64
66
  {"version":"0.12.1","date":"2026-07-11","zh":"全新品牌图标与双色字标","en":"New brand icon & two-tone wordmark"},
65
- {"version":"0.12.0","date":"2026-07-11","zh":"全屏程序也能滑动滚动 · 反馈入口 · 聊天草稿不丢","en":"Swipe-scroll full-screen apps · feedback channels · chat drafts kept"},
66
- {"version":"0.11.1","date":"2026-07-09","zh":"修复 Node 18 下无法启动的问题","en":"Fixes startup crash on Node 18"},
67
- {"version":"0.11.0","date":"2026-07-08","zh":"国内可用隧道 natapp/cpolar · 配置向导重做","en":"China-usable tunnels · setup redesigned"}
67
+ {"version":"0.12.0","date":"2026-07-11","zh":"全屏程序也能滑动滚动 · 反馈入口 · 聊天草稿不丢","en":"Swipe-scroll full-screen apps · feedback channels · chat drafts kept"}
68
68
  ]
69
69
  }