handmux 0.10.0 → 0.11.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/README.md CHANGED
@@ -59,10 +59,10 @@ handmux start --tunnel cloudflare # instant public URL (cloudflared auto-insta
59
59
 
60
60
  ## Networking: one decision
61
61
 
62
- LAN-only by default, nothing exposed. To reach it from outside, just ask: **does your machine already have a public address?**
62
+ No tunnel by default — the phone connects **straight to your own computer**, nothing exposed and no middleman. To reach it from outside, just ask: **does your machine already have a public address?**
63
63
 
64
64
  - **Yes** (cloud box / public IP / forwarded port) — no tunnel needed, connect directly; fastest and most private.
65
- - **No** — open a tunnel: `cloudflare` (zero-config, up in seconds, but its public nodes can be slow or unreliable in some regions) · `cloudflare-named` (your own domain, steadier) · `ssh` self-hosted (through your own server the pick where Cloudflare is unreliable, e.g. mainland China).
65
+ - **No** — open a tunnel. Each runs on **your own free third-party account** — handmux just wires it up and operates no relay of its own: `cloudflare` (zero-config, up in seconds, but its public edge can be slow or unreliable in some regions) · `cloudflare-named` (your own domain, steadier) · `natapp` / `cpolar` (domestic providers that stay reachable inside mainland China) · `ssh` self-hosted (through your own server).
66
66
 
67
67
  > Tunnel config, server-side reverse proxy, autostart, voice/push credentials, and port previews → see the **[docs](https://handmux.com/docs)**.
68
68
 
package/README.zh-CN.md CHANGED
@@ -59,10 +59,10 @@ handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
59
59
 
60
60
  ## 联网:一句话决策
61
61
 
62
- 默认只在局域网、什么都不暴露。想从外面连,只问一句:**电脑有没有公网地址?**
62
+ 默认不开隧道——手机**直连你自己的电脑**,什么都不暴露、也没有中间人。想从外面连,只问一句:**电脑有没有公网地址?**
63
63
 
64
64
  - **有**(云主机 / 公网 IP / 已端口转发)—— 不用隧道,直接连,最快也最私密。
65
- - **没有** —— 开一条隧道:`cloudflare`(零配置秒通,但公共节点在国内常不稳定)· `cloudflare-named`(你的域名,更稳)· `ssh` 自建(接你自己的服务器,**国内网络首选**)。
65
+ - **没有** —— 开一条隧道。每条都跑在**你自己的免费第三方账号**上,handmux 只负责接通、自身不设中转:`cloudflare`(零配置秒通,但公共边缘在国内常不稳)· `cloudflare-named`(你的域名,更稳)· `natapp` / `cpolar`(国内厂商,大陆境内可达)· `ssh` 自建(接你自己的服务器)。
66
66
 
67
67
  > 隧道配置、服务端反向代理、开机自启、语音 / 推送凭证、端口预览等细节 → 见 **[文档](https://handmux.com/docs)**。
68
68
 
package/bin/handmux.js CHANGED
@@ -7,8 +7,8 @@
7
7
  // let you try variations for one run (e.g. --tunnel cloudflare).
8
8
  // handmux setup — the one place to configure persistently. Interactive; writes ~/.handmux/config.json
9
9
  // (name, tunnel, push, voice). Re-run it to change anything.
10
- // `start` reads that file; with no file it uses defaults. Precedence: flag > file > default a flag
11
- // overrides one value for one run and never persists. Advanced: `--config PATH` (a different file, for
10
+ // `start` reads that file; with no file it uses defaults. Precedence: flag > file > env (HANDMUX_*) >
11
+ // default — a flag overrides one value for one run and never persists. Advanced: `--config PATH` (a different file, for
12
12
  // dev / multiple configs), `handmux config` (show what's in effect and where each value came from).
13
13
  //
14
14
  // Tunnels: `none` (LAN only, nothing exposed) · `cloudflare` (instant random https URL) ·
@@ -26,6 +26,7 @@ import { renderCompactQr } from '../src/cli/qr.js';
26
26
  import { supervise, bareUrl, publicUrlWithToken } from '../src/cli/supervisor.js';
27
27
  import { resolveCloudflared } from '../src/cli/cloudflared.js';
28
28
  import { resolveTunlite, checkSshAuth } from '../src/cli/tunlite.js';
29
+ import { resolveNatapp, resolveCpolar } from '../src/cli/tunnelClients.js';
29
30
  import { installService, uninstallService } from '../src/cli/service.js';
30
31
  import { checkTmux, MIN_TMUX, tmuxInstallHint } from '../src/cli/tmuxVersion.js';
31
32
  import { readState, clearState, isAlive, pocketHome, logPath, configPath, claudeStatePath } from '../src/cli/state.js';
@@ -115,6 +116,20 @@ async function preflightSsh(cfg) {
115
116
  throw new Error(t('ssh.notSetup', { bin: cfg.tunliteBin, host: cfg.sshHost }));
116
117
  }
117
118
 
119
+ // natapp/cpolar preflight: resolve the client binary (cpolar auto-downloads; natapp must be pre-installed),
120
+ // and for cpolar seed the authtoken into its own config so the detached `cpolar http` authenticates. Throws
121
+ // a friendly message the caller prints.
122
+ async function preflightNgrok(cfg) {
123
+ if (cfg.tunnel === 'natapp') {
124
+ cfg.natappBin = resolveNatapp(HOME);
125
+ } else {
126
+ cfg.cpolarBin = await resolveCpolar(HOME);
127
+ if (spawnSync(cfg.cpolarBin, ['authtoken', cfg.authtoken], { stdio: 'ignore' }).status !== 0) {
128
+ throw new Error(t('client.cpolarAuthFail'));
129
+ }
130
+ }
131
+ }
132
+
118
133
  async function main() {
119
134
  switch (command) {
120
135
  case 'start': return start();
@@ -228,6 +243,10 @@ async function start() {
228
243
  try { await preflightSsh(cfg); }
229
244
  catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
230
245
  }
246
+ if (cfg.tunnel === 'natapp' || cfg.tunnel === 'cpolar') {
247
+ try { await preflightNgrok(cfg); }
248
+ catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
249
+ }
231
250
 
232
251
  if (cfg.foreground) {
233
252
  supervise(cfg, { home: HOME });
@@ -305,6 +324,10 @@ async function serviceInstall() {
305
324
  console.error(t('err.generic', { msg: t('ssh.notSetup', { bin: cfg.tunliteBin, host: cfg.sshHost }) })); process.exit(1);
306
325
  }
307
326
  }
327
+ if (cfg.tunnel === 'natapp' || cfg.tunnel === 'cpolar') {
328
+ try { await preflightNgrok(cfg); }
329
+ catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
330
+ }
308
331
  const payload = Buffer.from(JSON.stringify(cfg)).toString('base64');
309
332
  const args = [process.execPath, SELF, '__supervise', '--payload', payload];
310
333
  try { installService(args, { home: HOME }); }
@@ -314,8 +337,14 @@ async function serviceInstall() {
314
337
 
315
338
  async function setupCmd() {
316
339
  const target = flags.config ? path.resolve(flags.config) : configPath(HOME);
317
- const cfg = await runSetup({ home: HOME, target });
318
- if (!cfg) { process.exit(2); }
340
+ // Is an instance already up? Then the run-action reads "Save & restart" and applying means a real restart
341
+ // (a running supervisor won't pick up the new config on its own). Captured before the interactive setup so
342
+ // the label and the action agree; stop() below is a safe no-op if it died meanwhile.
343
+ const st = readState(HOME);
344
+ const running = !!(st && isAlive(st.supervisorPid));
345
+ const res = await runSetup({ home: HOME, target, running });
346
+ if (!res) { process.exit(2); }
347
+ const { cfg, start: doStart } = res; // hub's "save & start/restart" carries the intent — no separate confirm
319
348
  // Offer to enable the inbox hooks when an agent is present but not yet wired (Claude 'absent', or Codex
320
349
  // 'absent'). installAgentHooks() then wires every present agent (idempotent for any already installed).
321
350
  const offerHooks = hooksStatus(HOME) === 'absent' || codexHooksStatus(HOME) === 'absent';
@@ -323,8 +352,12 @@ async function setupCmd() {
323
352
  installAgentHooks();
324
353
  }
325
354
  await maybeOfferStatusLine();
326
- if (await confirm(t('setup.confirmStart'))) { Object.assign(flags, cfg); return start(); }
327
- console.log(t('setup.later'));
355
+ if (doStart) {
356
+ Object.assign(flags, cfg);
357
+ if (running) { stop(); await sleep(600); } // restart into the new config
358
+ return start();
359
+ }
360
+ console.log(t(running ? 'setup.laterRestart' : 'setup.later'));
328
361
  }
329
362
 
330
363
  // Offer to enable the Claude statusLine usage capturer — it feeds the phone Usage page's 5h/weekly bars
@@ -488,6 +521,7 @@ function openCmd() {
488
521
  }
489
522
 
490
523
  function help() {
524
+ if (process.argv[3] === 'flags') { console.log(t('help.flags')); return; }
491
525
  console.log(t('help.body'));
492
526
  }
493
527
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -13,7 +13,14 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/handmux/handmux/issues"
15
15
  },
16
- "keywords": ["tmux", "mobile", "terminal", "claude-code", "remote", "pwa"],
16
+ "keywords": [
17
+ "tmux",
18
+ "mobile",
19
+ "terminal",
20
+ "claude-code",
21
+ "remote",
22
+ "pwa"
23
+ ],
17
24
  "engines": {
18
25
  "node": ">=18"
19
26
  },
@@ -36,6 +43,7 @@
36
43
  "test": "vitest run"
37
44
  },
38
45
  "dependencies": {
46
+ "@clack/prompts": "^1.7.0",
39
47
  "busboy": "^1.6.0",
40
48
  "express": "^4.19.2",
41
49
  "tunlite": "^0.10.0",
@@ -49,6 +57,7 @@
49
57
  "vitest": "^2.0.0"
50
58
  },
51
59
  "whatsNew": [
60
+ {"version":"0.11.0","date":"2026-07-08","zh":"国内可用隧道 natapp/cpolar · 配置向导重做","en":"China-usable tunnels · setup redesigned"},
52
61
  {"version":"0.10.0","date":"2026-07-06","zh":"升级前预览新功能 · 电脑 handmux open 接管","en":"Preview an update before installing · handmux open"},
53
62
  {"version":"0.9.1","date":"2026-07-06","zh":"「添加到主屏」引导","en":"\"Add to Home Screen\" coach"},
54
63
  {"version":"0.9.0","date":"2026-07-05","zh":"聊天输入框多行重做","en":"Multi-line chat composer redone"},