handmux 0.5.2 → 0.6.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.
@@ -0,0 +1,186 @@
1
+ // English catalog (the fallback locale). Keys are grouped by command/area. `{var}` placeholders are filled
2
+ // by translate(). Keep this in lockstep with zh.js — a missing zh key silently falls back to the line here.
3
+ export default {
4
+ // generic
5
+ 'err.generic': '✗ {msg}',
6
+ 'err.configNotFound': '✗ --config {path}: not found',
7
+ 'err.badConfig': '✗ bad config {path}: {msg}',
8
+ 'err.namedNotProvisioned': '✗ named tunnel not provisioned — run `handmux setup` first',
9
+
10
+ // config line printed at the top of start / service install
11
+ 'config.loaded': 'config: {path}',
12
+ 'config.none': '(none — flags + defaults)',
13
+
14
+ // ssh preflight
15
+ 'ssh.confirmSetup': 'passwordless SSH to {host} is not set up. Configure it now?',
16
+ 'ssh.notSetup': 'passwordless SSH not set up — run: {bin} setup-key {host}',
17
+
18
+ // tmux presence / version
19
+ 'tmux.notFound': '✗ tmux not found.',
20
+ 'tmux.explain1': ' handmux runs on top of tmux (a terminal multiplexer) — it drives your real tmux',
21
+ 'tmux.explain2': ' panes from your phone, so you need tmux on this machine first.',
22
+ 'tmux.install': ' Install it: {hint}',
23
+ 'tmux.thenStart': ' Then run `handmux start` again.',
24
+ 'tmux.tooOld': '⚠ tmux {raw} is below the tested minimum {min}; terminal rendering may be off',
25
+
26
+ // start — already running
27
+ 'start.running.same': 'handmux is already running — open the address below.',
28
+ 'start.running.changedHead': 'handmux is already running (tunnel: {tunnel}). A running instance does NOT pick up config changes on its own:',
29
+ 'start.running.changedRow': ' • {key}: {from} → {to} (what you asked for now)',
30
+ 'start.running.switchQ': 'Switch to the new settings now? (restarts handmux)',
31
+ 'start.running.hint': "Leaving it as-is. Run 'handmux restart' whenever you want to apply the new settings.",
32
+
33
+ // start — launching
34
+ 'start.overrides': ' ↳ --tunnel {flag} overrides config ({file}) for this run only',
35
+ 'start.foreground': 'starting handmux (tunnel: {tunnel}, port: {port}) — Ctrl-C to stop',
36
+ 'start.starting': 'starting handmux (tunnel: {tunnel}, port: {port}) …',
37
+
38
+ // stop / status
39
+ 'stop.notRunning': 'handmux not running',
40
+ 'stop.stopped': 'stopped handmux (pid {pid})',
41
+ 'status.stopped': '● handmux stopped',
42
+ 'status.running': '● handmux running',
43
+
44
+ // logs
45
+ 'logs.none': '(no log yet — start handmux first)',
46
+
47
+ // update
48
+ 'update.available': ' ⬆ handmux {latest} is available (you have {current})',
49
+ 'update.how': ' upgrade: handmux update (or npm i -g handmux@latest)',
50
+ 'update.running': 'Upgrading handmux (npm i -g handmux@latest)…',
51
+ 'update.done': '✓ handmux updated.',
52
+ 'update.restartHint': ' run `handmux restart` to run the new version.',
53
+ 'update.failed': '✗ Upgrade failed. Try manually: npm i -g {pkg}@latest (may need sudo).',
54
+
55
+ // cloudflared auto-download
56
+ 'cf.downloading': ' ↓ downloading cloudflared ({file}) …',
57
+
58
+ // access block (printAccess)
59
+ 'access.noState': ' (no state)',
60
+ 'access.error': ' ✗ {msg}',
61
+ 'access.tunnel': ' tunnel {tunnel} · pid {pid}',
62
+ 'access.open': ' 🌐 open {url}',
63
+ 'access.pending': '(pending…)',
64
+ 'access.lan': ' 📶 lan {url}',
65
+ 'access.local': ' 💻 local {url}',
66
+ 'access.token': ' 🔑 token {token}',
67
+ 'access.reachable': ' ✓ reachable',
68
+ 'access.unreachable': ' ⚠ tunnel up but {url} did not answer — check the server-side reverse proxy / DNS',
69
+ 'access.hint': ' handmux status | stop',
70
+
71
+ // hooks
72
+ 'hooks.confirmEnable': 'Enable coding-agent notifications (inbox)?',
73
+ 'hooks.installedShort': '✓ Agent hooks installed.',
74
+ 'hooks.noClaude': 'Claude Code not detected (~/.claude missing) — nothing to install.',
75
+ 'hooks.noAgents': 'No coding agent detected (no ~/.claude, and codex not on PATH) — nothing to install.',
76
+ 'hooks.installed': '✓ Claude hooks installed → ~/.claude/settings.json',
77
+ 'hooks.installedClaude': '✓ Claude Code hooks installed → ~/.claude/settings.json',
78
+ 'hooks.installedCodex': '✓ Codex hooks installed → ~/.codex/config.toml',
79
+ 'hooks.installedHint': ' Restart or open a new agent session to load them; the inbox lights up as panes report.',
80
+ 'hooks.removed': '✓ Agent hooks removed.',
81
+ 'hooks.usage': 'usage: handmux hooks install|uninstall',
82
+
83
+ // tmux status-dot offer
84
+ 'tmuxdot.tip': ' Tip: to show a Claude status dot on each tmux window, run `handmux hooks install` from an interactive terminal — it wires {conf} for you (see tmux/README.md in the handmux package).',
85
+ 'tmuxdot.confirm': 'Also show a per-window Claude status dot in tmux? (adds a block to ~/.tmux.conf)',
86
+ 'tmuxdot.added': '✓ tmux dot added → {path}',
87
+ 'tmuxdot.apply': ' Apply with: tmux source-file ~/.tmux.conf (it changes the shared tmux server — all clients, including your PC).',
88
+
89
+ // service
90
+ 'service.usage': 'usage: handmux service install [start-flags] | handmux service uninstall',
91
+ 'service.installed': "handmux will now start at login. 'handmux service uninstall' to remove.",
92
+
93
+ // config command
94
+ 'configcmd.file': 'config file: {path}',
95
+ 'configcmd.fileNone': '(none — using defaults; run `handmux setup` to create one)',
96
+ 'configcmd.legend': ' origin: flag (this run only) · file · env · default',
97
+
98
+ // setup wizard
99
+ 'setup.confirmStart': 'Start handmux now?',
100
+ 'setup.later': "run 'handmux start' when you're ready.",
101
+ 'setup.needTty': 'handmux setup needs an interactive terminal',
102
+ 'setup.langQ': 'Language / 语言',
103
+ 'setup.lang1': ' 1) English',
104
+ 'setup.lang2': ' 2) 中文',
105
+ 'setup.askName': 'app name (shown in the browser tab / home-screen icon; blank = default)',
106
+ 'setup.tunnelQ': 'How should your phone reach this machine?',
107
+ 'setup.tunnel1': ' 1) none — same Wi-Fi / LAN only',
108
+ 'setup.tunnel2': ' 2) cloudflare — instant, random temporary https URL',
109
+ 'setup.tunnel3': ' 3) cloudflare-named — your domain, stable HTTPS (most hands-off)',
110
+ 'setup.tunnel4': ' 4) ssh (tunlite) — your own server / edge',
111
+ 'setup.choose': 'choose 1-4',
112
+ 'setup.invalid': 'invalid choice',
113
+ 'setup.askPort': 'server port',
114
+ 'setup.askHostname': 'public hostname (e.g. handmux.example.com)',
115
+ 'setup.askTunnelName': 'tunnel name',
116
+ 'setup.askSshHost': 'ssh host (user@host[:port])',
117
+ 'setup.askRemotePort': 'remote port on the ssh host',
118
+ 'setup.askPublicUrl': 'public url (blank = http://host:remotePort)',
119
+ 'setup.wrote': '✓ wrote {path}',
120
+ 'setup.pushKeep': 'keep push notifications configured?',
121
+ 'setup.pushSetup': 'set up push notifications now? (generates VAPID keys)',
122
+ 'setup.pushContact': 'contact (mailto: or https URL, for the push service)',
123
+ 'setup.pushGenerated': '✓ generated VAPID keypair',
124
+ 'setup.voiceKeep': 'keep voice input configured?',
125
+ 'setup.voiceSetup': 'set up voice input now? (needs iFlytek/xfyun keys)',
126
+ 'setup.voiceAppId': 'xfyun appId',
127
+ 'setup.voiceApiKey': 'xfyun apiKey',
128
+ 'setup.voiceApiSecret': 'xfyun apiSecret',
129
+ 'setup.voiceSkipped': ' (skipped — missing fields)',
130
+ 'setup.cfLogin': '→ logging in to Cloudflare (a browser will open) …',
131
+ 'setup.cfReuse': '✓ reusing existing tunnel {name} ({id})',
132
+ 'setup.cfCredMissing1': '⚠ credentials file {file} not found on this machine — the tunnel was likely',
133
+ 'setup.cfCredMissing2': ' created elsewhere. Run `{bin} tunnel delete {name}` and re-run setup to recreate it here.',
134
+ 'setup.cfCreate': '→ creating tunnel {name} …',
135
+ 'setup.cfRoute': '→ routing {host} → tunnel …',
136
+ 'setup.cfRouteFail': "⚠ route dns failed — is {domain}'s DNS hosted on Cloudflare?",
137
+ 'setup.cfRouteFail2': ' Add the domain on Cloudflare (free) and point its nameservers there, then re-run setup.',
138
+ 'setup.sshReady': '✓ passwordless SSH already set up',
139
+ 'setup.sshSetup': "→ setting up passwordless SSH to {host} (you'll enter the password once) …",
140
+ 'setup.sshHelp1': 'Server side (one-time): point a reverse proxy at the forwarded loopback port.',
141
+ 'setup.sshHelpNginx': ' nginx: proxy_pass http://127.0.0.1:{port}; (add client_max_body_size 60m; proxy_read_timeout 90s;)',
142
+ 'setup.sshHelpCaddy': ' caddy: {url} { reverse_proxy 127.0.0.1:{port} }',
143
+ 'setup.previewHelp1': 'Optional — dynamic port preview (open a dev server by port on your phone):',
144
+ 'setup.previewHelp2': ' set "previewDomain": "..." in the config, and route the wildcard preview domain to the gateway.',
145
+ 'setup.previewTlsCf': " TLS: Cloudflare's free cert covers ONE level (*.example.com); deeper (*.preview.example.com) needs Advanced Certificate Manager.",
146
+ 'setup.previewTlsEdge': " TLS: your edge serves the wildcard cert (e.g. a Let's Encrypt *.preview.your.domain).",
147
+
148
+ // help
149
+ 'help.body': `handmux — drive your tmux from your phone
150
+
151
+ handmux start run it (defaults to LAN-only; no config needed)
152
+ handmux setup configure tunnel / name / notifications (writes config; re-run to change)
153
+ handmux stop | restart | status
154
+ handmux logs [--follow] [--lines N]
155
+ handmux update upgrade to the latest published version (npm i -g handmux@latest)
156
+
157
+ The model: 'start' runs · 'setup' configures (writes ~/.handmux/config.json) · re-run setup to change.
158
+ A flag overrides one value for one run and never persists (flag > file > default).
159
+
160
+ advanced (scripting / multiple configs):
161
+ handmux config show the effective config + where each value came from
162
+ handmux hooks install|uninstall enable/disable Claude Code notifications (inbox)
163
+ handmux service install [start-flags] start at login (launchd/systemd)
164
+ handmux service uninstall remove the autostart entry
165
+ --config PATH use this config file instead of ~/.handmux/config.json (dev / multi-config)
166
+ --lang en|zh CLI language (default: auto-detect from your shell locale)
167
+ --version, -v print the handmux version
168
+
169
+ start flags (one-run overrides — for persistence use 'handmux setup'):
170
+ --tunnel none|cloudflare|cloudflare-named|ssh expose method (default: none)
171
+ --ssh-host user@host[:port] ssh tunnel target (tunlite)
172
+ --remote-port N port bound on the ssh host (default: --port)
173
+ --public-url URL public url to advertise (any tunnel, incl. none if you run your own;
174
+ ssh defaults to http://host:remotePort)
175
+ --ssh-jump u@h[,…] optional bastion for ssh
176
+ --cf-hostname H public hostname for cloudflare-named
177
+ --cf-tunnel-name N tunnel name for cloudflare-named (default: handmux)
178
+ --port N server port (default: 19999)
179
+ --host H bind host (default: 0.0.0.0)
180
+ --token S auth token (default: generated)
181
+ --name "My Box" app name in the browser tab + home-screen icon label
182
+ --preview-domain D enable dynamic previews (needs wildcard subdomain)
183
+ --foreground, -f run in the foreground (don't daemonize)
184
+ --no-qr don't render the QR code
185
+ `,
186
+ };
@@ -0,0 +1,53 @@
1
+ // CLI-side i18n. Mirrors the web i18n shape (a pure `translate` with English fallback) but resolves the
2
+ // locale from the shell environment / flags / config instead of the browser + localStorage. Default is
3
+ // English; a Chinese shell (LANG/LC_* = zh…) or an explicit `--lang zh` / config `"lang": "zh"` switches it.
4
+ // Adding a language = create ./<code>.js with the same keys, import it, and add it to LOCALES — any missing
5
+ // key falls back to English, so a partial translation degrades gracefully rather than printing blanks.
6
+ import en from './en.js';
7
+ import zh from './zh.js';
8
+
9
+ const LOCALES = { en, zh };
10
+ export const AVAILABLE = [
11
+ { code: 'en', label: 'English' },
12
+ { code: 'zh', label: '中文' },
13
+ ];
14
+
15
+ // pure + testable: current-dict → English-fallback → key, then {var} interpolation (same as web).
16
+ export function translate(dict, fallback, key, vars) {
17
+ let s = dict && dict[key] != null ? dict[key]
18
+ : fallback && fallback[key] != null ? fallback[key]
19
+ : key;
20
+ if (vars) s = s.replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? vars[k] : `{${k}}`));
21
+ return s;
22
+ }
23
+
24
+ // A POSIX locale string → the base language we might have a dict for: `zh_CN.UTF-8` → `zh`, `en` → `en`.
25
+ const baseOf = (v) => String(v || '').toLowerCase().split(/[._@-]/)[0];
26
+
27
+ // pure + testable: explicit choice (flag/config) wins, else the first shell locale var that names a known
28
+ // language, else English. `C`/`POSIX`/`C.UTF-8` carry no language → fall through to English.
29
+ export function detectLang(flags = {}, fileCfg = {}, env = {}, available = LOCALES) {
30
+ const explicit = flags.lang ?? fileCfg.lang;
31
+ if (explicit != null) { const b = baseOf(explicit); if (available[b]) return b; }
32
+ for (const k of ['HANDMUX_LANG', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
33
+ const b = baseOf(env[k]);
34
+ if (b && available[b]) return b;
35
+ }
36
+ return 'en';
37
+ }
38
+
39
+ let current = 'en';
40
+
41
+ // Resolve + install the active locale for the whole CLI process (called once at startup). Returns the code.
42
+ export function initLocale(flags, fileCfg, env = process.env) {
43
+ current = detectLang(flags, fileCfg, env);
44
+ return current;
45
+ }
46
+
47
+ // Force a specific locale (e.g. right after the setup wizard's language question, so the rest of the wizard
48
+ // speaks the chosen language). No-op for an unknown code.
49
+ export function setLocale(code) { if (LOCALES[code]) current = code; }
50
+ export function getLocale() { return current; }
51
+
52
+ // The one call sites use: translate `key` in the active locale, falling back to English then the key itself.
53
+ export function t(key, vars) { return translate(LOCALES[current], en, key, vars); }
@@ -0,0 +1,185 @@
1
+ // 中文字典。键与 en.js 一一对应;缺键会自动回退到英文。命令名、flag、隧道名等字面量保持英文(它们是要照抄输入的)。
2
+ export default {
3
+ // 通用
4
+ 'err.generic': '✗ {msg}',
5
+ 'err.configNotFound': '✗ --config {path}:找不到该文件',
6
+ 'err.badConfig': '✗ 配置有误 {path}:{msg}',
7
+ 'err.namedNotProvisioned': '✗ 命名隧道尚未配置 —— 请先运行 `handmux setup`',
8
+
9
+ // start / service install 顶部打印的配置来源
10
+ 'config.loaded': '配置:{path}',
11
+ 'config.none': '(无 —— 使用 flag + 默认值)',
12
+
13
+ // ssh 预检
14
+ 'ssh.confirmSetup': '到 {host} 的免密 SSH 还没配置。现在配置吗?',
15
+ 'ssh.notSetup': '免密 SSH 未配置 —— 请运行:{bin} setup-key {host}',
16
+
17
+ // tmux 是否存在 / 版本
18
+ 'tmux.notFound': '✗ 未找到 tmux。',
19
+ 'tmux.explain1': ' handmux 跑在 tmux(终端复用器)之上 —— 它从手机驱动你真实的 tmux 窗格,',
20
+ 'tmux.explain2': ' 所以这台机器上必须先装好 tmux。',
21
+ 'tmux.install': ' 安装: {hint}',
22
+ 'tmux.thenStart': ' 然后再运行 `handmux start`。',
23
+ 'tmux.tooOld': '⚠ tmux {raw} 低于测试过的最低版本 {min};终端渲染可能有偏差',
24
+
25
+ // start —— 已在运行
26
+ 'start.running.same': 'handmux 已在运行 —— 直接打开下面的地址即可。',
27
+ 'start.running.changedHead': 'handmux 已在运行(隧道:{tunnel})。运行中的实例不会自动应用配置改动:',
28
+ 'start.running.changedRow': ' • {key}:{from} → {to}(你这次想要的)',
29
+ 'start.running.switchQ': '现在切换到新设置吗?(会重启 handmux)',
30
+ 'start.running.hint': "保持不变。想应用新设置时,运行 'handmux restart' 即可。",
31
+
32
+ // start —— 启动中
33
+ 'start.overrides': ' ↳ --tunnel {flag} 仅本次覆盖配置文件里的 {file}',
34
+ 'start.foreground': '正在启动 handmux(隧道:{tunnel},端口:{port})—— 按 Ctrl-C 停止',
35
+ 'start.starting': '正在启动 handmux(隧道:{tunnel},端口:{port})…',
36
+
37
+ // stop / status
38
+ 'stop.notRunning': 'handmux 未在运行',
39
+ 'stop.stopped': '已停止 handmux(pid {pid})',
40
+ 'status.stopped': '● handmux 已停止',
41
+ 'status.running': '● handmux 运行中',
42
+
43
+ // logs
44
+ 'logs.none': '(还没有日志 —— 先运行 handmux start)',
45
+
46
+ // update
47
+ 'update.available': ' ⬆ handmux {latest} 可升级(当前 {current})',
48
+ 'update.how': ' 升级: handmux update (或 npm i -g handmux@latest)',
49
+ 'update.running': '正在升级 handmux(npm i -g handmux@latest)…',
50
+ 'update.done': '✓ handmux 已更新。',
51
+ 'update.restartHint': ' 运行 `handmux restart` 以启用新版本。',
52
+ 'update.failed': '✗ 升级失败。请手动运行:npm i -g {pkg}@latest(可能需要 sudo)。',
53
+
54
+ // cloudflared 自动下载
55
+ 'cf.downloading': ' ↓ 正在下载 cloudflared({file})…',
56
+
57
+ // 访问信息块(printAccess)
58
+ 'access.noState': ' (无状态)',
59
+ 'access.error': ' ✗ {msg}',
60
+ 'access.tunnel': ' 隧道 {tunnel} · pid {pid}',
61
+ 'access.open': ' 🌐 打开 {url}',
62
+ 'access.pending': '(等待中…)',
63
+ 'access.lan': ' 📶 局域网 {url}',
64
+ 'access.local': ' 💻 本机 {url}',
65
+ 'access.token': ' 🔑 令牌 {token}',
66
+ 'access.reachable': ' ✓ 可访问',
67
+ 'access.unreachable': ' ⚠ 隧道已起,但 {url} 没有响应 —— 检查服务端的反向代理 / DNS',
68
+ 'access.hint': ' handmux status | stop',
69
+
70
+ // hooks
71
+ 'hooks.confirmEnable': '启用编程 agent 通知(收件箱)?',
72
+ 'hooks.installedShort': '✓ agent hooks 已安装。',
73
+ 'hooks.noClaude': '未检测到 Claude Code(缺少 ~/.claude)—— 无需安装。',
74
+ 'hooks.noAgents': '未检测到编程 agent(无 ~/.claude,且 PATH 上没有 codex)—— 无需安装。',
75
+ 'hooks.installed': '✓ Claude hooks 已安装 → ~/.claude/settings.json',
76
+ 'hooks.installedClaude': '✓ Claude Code hooks 已安装 → ~/.claude/settings.json',
77
+ 'hooks.installedCodex': '✓ Codex hooks 已安装 → ~/.codex/config.toml',
78
+ 'hooks.installedHint': ' 重启或新开一个 agent 会话以加载;窗格上报后收件箱就会亮起。',
79
+ 'hooks.removed': '✓ agent hooks 已移除。',
80
+ 'hooks.usage': '用法:handmux hooks install|uninstall',
81
+
82
+ // tmux 状态点
83
+ 'tmuxdot.tip': ' 提示:想在每个 tmux 窗口上显示 Claude 状态点,请在交互终端里运行 `handmux hooks install` —— 它会替你写好 {conf}(见 handmux 包内的 tmux/README.md)。',
84
+ 'tmuxdot.confirm': '同时在 tmux 里显示每个窗口的 Claude 状态点吗?(会往 ~/.tmux.conf 加一段)',
85
+ 'tmuxdot.added': '✓ tmux 状态点已添加 → {path}',
86
+ 'tmuxdot.apply': ' 生效方式:tmux source-file ~/.tmux.conf (它改的是共享 tmux 服务器 —— 对所有客户端生效,包括你的电脑)。',
87
+
88
+ // service
89
+ 'service.usage': '用法:handmux service install [start-flags] | handmux service uninstall',
90
+ 'service.installed': "handmux 现在会开机自启。移除请运行 'handmux service uninstall'。",
91
+
92
+ // config 命令
93
+ 'configcmd.file': '配置文件:{path}',
94
+ 'configcmd.fileNone': '(无 —— 使用默认值;运行 `handmux setup` 可创建一个)',
95
+ 'configcmd.legend': ' 来源:flag(仅本次)· file · env · default',
96
+
97
+ // setup 向导
98
+ 'setup.confirmStart': '现在启动 handmux 吗?',
99
+ 'setup.later': "准备好后运行 'handmux start' 即可。",
100
+ 'setup.needTty': 'handmux setup 需要一个交互式终端',
101
+ 'setup.langQ': 'Language / 语言',
102
+ 'setup.lang1': ' 1) English',
103
+ 'setup.lang2': ' 2) 中文',
104
+ 'setup.askName': '应用名称(显示在浏览器标签 / 主屏图标;留空 = 默认)',
105
+ 'setup.tunnelQ': '你的手机怎么连到这台机器?',
106
+ 'setup.tunnel1': ' 1) none —— 仅同一 Wi-Fi / 局域网',
107
+ 'setup.tunnel2': ' 2) cloudflare —— 即时、随机的临时 https 地址',
108
+ 'setup.tunnel3': ' 3) cloudflare-named —— 用你自己的域名,稳定 HTTPS(最省心)',
109
+ 'setup.tunnel4': ' 4) ssh (tunlite) —— 你自己的服务器 / 边缘',
110
+ 'setup.choose': '选择 1-4',
111
+ 'setup.invalid': '无效选择',
112
+ 'setup.askPort': '服务端口',
113
+ 'setup.askHostname': '公网域名(例如 handmux.example.com)',
114
+ 'setup.askTunnelName': '隧道名称',
115
+ 'setup.askSshHost': 'ssh 主机(user@host[:port])',
116
+ 'setup.askRemotePort': 'ssh 主机上的远程端口',
117
+ 'setup.askPublicUrl': '公网地址(留空 = http://host:remotePort)',
118
+ 'setup.wrote': '✓ 已写入 {path}',
119
+ 'setup.pushKeep': '保留已配置的推送通知吗?',
120
+ 'setup.pushSetup': '现在配置推送通知吗?(会生成 VAPID 密钥)',
121
+ 'setup.pushContact': '联系方式(mailto: 或 https 地址,供推送服务使用)',
122
+ 'setup.pushGenerated': '✓ 已生成 VAPID 密钥对',
123
+ 'setup.voiceKeep': '保留已配置的语音输入吗?',
124
+ 'setup.voiceSetup': '现在配置语音输入吗?(需要讯飞/xfyun 密钥)',
125
+ 'setup.voiceAppId': 'xfyun appId',
126
+ 'setup.voiceApiKey': 'xfyun apiKey',
127
+ 'setup.voiceApiSecret': 'xfyun apiSecret',
128
+ 'setup.voiceSkipped': ' (已跳过 —— 字段缺失)',
129
+ 'setup.cfLogin': '→ 正在登录 Cloudflare(会打开一个浏览器)…',
130
+ 'setup.cfReuse': '✓ 复用已有隧道 {name}({id})',
131
+ 'setup.cfCredMissing1': '⚠ 本机找不到凭据文件 {file} —— 该隧道很可能是',
132
+ 'setup.cfCredMissing2': ' 在别处创建的。运行 `{bin} tunnel delete {name}` 再重新 setup,即可在本机重建。',
133
+ 'setup.cfCreate': '→ 正在创建隧道 {name} …',
134
+ 'setup.cfRoute': '→ 正在把 {host} 路由到隧道 …',
135
+ 'setup.cfRouteFail': '⚠ route dns 失败 —— {domain} 的 DNS 托管在 Cloudflare 上吗?',
136
+ 'setup.cfRouteFail2': ' 在 Cloudflare 上添加该域名(免费)并把 nameserver 指过去,然后重新 setup。',
137
+ 'setup.sshReady': '✓ 免密 SSH 已配置好',
138
+ 'setup.sshSetup': '→ 正在配置到 {host} 的免密 SSH(你需要输入一次密码)…',
139
+ 'setup.sshHelp1': '服务端(一次性):把反向代理指向被转发的 loopback 端口。',
140
+ 'setup.sshHelpNginx': ' nginx: proxy_pass http://127.0.0.1:{port}; (加上 client_max_body_size 60m; proxy_read_timeout 90s;)',
141
+ 'setup.sshHelpCaddy': ' caddy: {url} { reverse_proxy 127.0.0.1:{port} }',
142
+ 'setup.previewHelp1': '可选 —— 动态端口预览(在手机上按端口打开一个 dev server):',
143
+ 'setup.previewHelp2': ' 在配置里设 "previewDomain": "..." ,并把通配预览域名路由到网关。',
144
+ 'setup.previewTlsCf': ' TLS:Cloudflare 的免费证书只覆盖一级(*.example.com);更深一层(*.preview.example.com)需要 Advanced Certificate Manager。',
145
+ 'setup.previewTlsEdge': ' TLS:由你自己的边缘提供通配证书(例如 Let\'s Encrypt 的 *.preview.your.domain)。',
146
+
147
+ // help
148
+ 'help.body': `handmux —— 从手机驱动你的 tmux
149
+
150
+ handmux start 直接运行(默认仅局域网;无需配置)
151
+ handmux setup 配置隧道 / 名称 / 通知(写入配置;重跑即可修改)
152
+ handmux stop | restart | status
153
+ handmux logs [--follow] [--lines N]
154
+ handmux update 升级到最新发布版本(npm i -g handmux@latest)
155
+
156
+ 模型:'start' 运行 · 'setup' 配置(写入 ~/.handmux/config.json)· 重跑 setup 即可修改。
157
+ flag 仅本次覆盖某一项、绝不持久化(优先级 flag > file > default)。
158
+
159
+ 进阶(脚本 / 多套配置):
160
+ handmux config 显示生效的配置 + 每一项来自哪里
161
+ handmux hooks install|uninstall 启用/停用 Claude Code 通知(收件箱)
162
+ handmux service install [start-flags] 开机自启(launchd/systemd)
163
+ handmux service uninstall 移除自启项
164
+ --config PATH 使用指定配置文件,替代 ~/.handmux/config.json(开发 / 多套配置)
165
+ --lang en|zh CLI 语言(默认:根据你的 shell locale 自动检测)
166
+ --version, -v 打印 handmux 版本
167
+
168
+ start flag(仅本次覆盖 —— 要持久化请用 'handmux setup'):
169
+ --tunnel none|cloudflare|cloudflare-named|ssh 暴露方式(默认:none)
170
+ --ssh-host user@host[:port] ssh 隧道目标(tunlite)
171
+ --remote-port N 绑定在 ssh 主机上的端口(默认:--port)
172
+ --public-url URL 对外公布的公网地址(任意隧道均可,包括自建的 none;
173
+ ssh 默认 http://host:remotePort)
174
+ --ssh-jump u@h[,…] ssh 的可选跳板机
175
+ --cf-hostname H cloudflare-named 的公网域名
176
+ --cf-tunnel-name N cloudflare-named 的隧道名(默认:handmux)
177
+ --port N 服务端口(默认:19999)
178
+ --host H 绑定地址(默认:0.0.0.0)
179
+ --token S 鉴权令牌(默认:自动生成)
180
+ --name "My Box" 浏览器标签 + 主屏图标里的应用名
181
+ --preview-domain D 启用动态预览(需要通配子域名)
182
+ --foreground, -f 前台运行(不后台化)
183
+ --no-qr 不渲染二维码
184
+ `,
185
+ };
@@ -137,6 +137,9 @@ export function explainConfig(flags = {}, fileCfg = {}, cfgPath = null, env = pr
137
137
  const name = trace(flags, fileCfg, env, cfgPath, 'name', 'HANDMUX_APP_NAME', null);
138
138
  add('name', name, name.value == null ? '(default)' : String(name.value));
139
139
 
140
+ const lang = trace(flags, fileCfg, env, cfgPath, 'lang', 'HANDMUX_LANG', null);
141
+ add('lang', lang, lang.value == null ? '(auto — shell locale)' : String(lang.value));
142
+
140
143
  const token = trace(flags, fileCfg, env, cfgPath, 'token', 'HANDMUX_TOKEN', null);
141
144
  add('token', token, token.value == null ? '(generated each start)' : mask(token.value));
142
145