handmux 0.23.0 → 0.25.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.
Files changed (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -1,384 +0,0 @@
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
- 'lifecycle.busy': '✗ 另一个 handmux 启停操作正在进行(pid {pid}),请稍后重试。',
9
- 'lifecycle.scanFailed': '✗ 无法检查 handmux supervisor 进程;为避免启动重复实例,本次操作已停止。',
10
-
11
- // start / service install 顶部打印的配置来源
12
- 'config.loaded': '配置:{path}',
13
- 'config.none': '(无 —— 使用 flag + 默认值)',
14
-
15
- // 快捷项编辑器
16
- 'shortcuts.title': '共享常用快捷项',
17
- 'shortcuts.command': '命令模式',
18
- 'shortcuts.chat': '聊天模式',
19
- 'shortcuts.count': '{n} 项',
20
- 'shortcuts.save': '保存并退出',
21
- 'shortcuts.exit': '放弃修改',
22
- 'shortcuts.addKey': '添加按键',
23
- 'shortcuts.addText': '添加文字',
24
- 'shortcuts.back': '返回',
25
- 'shortcuts.edit': '编辑',
26
- 'shortcuts.move': '移动位置',
27
- 'shortcuts.addPositionPrompt': '放到哪里',
28
- 'shortcuts.movePrompt': '选择新位置(当前:第 {n} 位)',
29
- 'shortcuts.positionFirst': '最前(第 1 位)',
30
- 'shortcuts.positionAfter': '放在 {item} 后(第 {n} 位)',
31
- 'shortcuts.positionLast': '最后(第 {n} 位)',
32
- 'shortcuts.delete': '删除',
33
- 'shortcuts.type': '快捷项类型',
34
- 'shortcuts.key': '按键',
35
- 'shortcuts.text': '文字',
36
- 'shortcuts.textEnter': '文字 · 发送后回车',
37
- 'shortcuts.textOnly': '文字 · 只输入',
38
- 'shortcuts.textPrompt': '输入文字内容',
39
- 'shortcuts.badText': '请输入一行非空文字',
40
- 'shortcuts.enter': '文字后敲 Enter?',
41
- 'shortcuts.modifier': '修饰键',
42
- 'shortcuts.noModifier': '无',
43
- 'shortcuts.base': '基础键',
44
- 'shortcuts.needTty': 'handmux shortcuts 需要交互式终端。',
45
- 'shortcuts.wrote': '已保存 {path}',
46
- 'shortcuts.exited': '未保存修改',
47
- 'shortcuts.applied': '已立即应用到正在运行的 handmux。',
48
- 'shortcuts.applyFailed': '配置已保存,但无法立即应用:{msg}。请运行 `handmux restart`。',
49
-
50
- // ssh 预检
51
- 'ssh.confirmSetup': '到 {host} 的免密 SSH 还没配置。现在配置吗?',
52
- 'ssh.notSetup': '免密 SSH 未配置 —— 请运行:{bin} setup-key {host}',
53
-
54
- // tmux 是否存在 / 版本
55
- 'tmux.notFound': '✗ 未找到 tmux。',
56
- 'tmux.explain1': ' handmux 跑在 tmux(终端复用器)之上 —— 它从手机驱动你真实的 tmux 窗格,',
57
- 'tmux.explain2': ' 所以这台机器上必须先装好 tmux。',
58
- 'tmux.install': ' 安装: {hint}',
59
- 'tmux.thenStart': ' 然后再运行 `handmux start`。',
60
- 'open.usage': '用法: handmux open <会话名> 接入该 tmux 会话(不存在则新建)',
61
- 'open.insideTmux': '你已经在 tmux 里了——切换会话请用 tmux 自己的方式(如 `tmux switch-client -t <会话名>`),不要嵌套。',
62
- 'tmux.tooOld': '⚠ tmux {raw} 低于测试过的最低版本 {min};终端渲染可能有偏差',
63
-
64
- // start —— 已在运行
65
- 'start.running.same': 'handmux 已在运行 —— 直接打开下面的地址即可。',
66
- 'start.running.changedHead': 'handmux 已在运行(隧道:{tunnel})。运行中的实例不会自动应用配置改动:',
67
- 'start.running.changedRow': ' • {key}:{from} → {to}(你这次想要的)',
68
- 'start.running.switchQ': '现在切换到新设置吗?(会重启 handmux)',
69
- 'start.running.hint': "保持不变。想应用新设置时,运行 'handmux restart' 即可。",
70
- 'start.instancesFound': '✗ 检测到未登记或重复的 handmux supervisor(pid: {pids})。',
71
- 'start.instancesHint': ' 为避免再启动一份,本次不会继续。请先运行 `handmux stop` 回收全部副本。',
72
-
73
- // start —— 启动中
74
- 'start.overrides': ' ↳ --tunnel {flag} 仅本次覆盖配置文件里的 {file}',
75
- 'start.foreground': '正在启动 handmux(隧道:{tunnel},端口:{port})—— 按 Ctrl-C 停止',
76
- 'start.starting': '正在启动 handmux(隧道:{tunnel},端口:{port})…',
77
-
78
- // stop / status
79
- 'stop.notRunning': 'handmux 未在运行',
80
- 'stop.stopped': '已停止 handmux(pid {pid})',
81
- 'stop.stoppedMany': '已停止重复的 handmux 实例(pid: {pids})',
82
- 'stop.stoppedManaged': '已停止 handmux 托管服务',
83
- 'stop.timeout': '✗ handmux(pid: {pids})未能完全停止;为避免启动双实例,本次不会继续启动。请运行 `handmux status` / `handmux logs` 检查。',
84
- 'status.stopped': '● handmux {version} 已停止',
85
- 'status.running': '● handmux {version} 运行中',
86
- 'status.installed': ' 已安装版本 {version}(重启后生效)',
87
- 'status.untracked': '⚠ 状态文件已失效,但仍检测到 handmux supervisor(pid: {pids});运行版本未知,已安装版本为 {version}。',
88
- 'status.duplicates': '⚠ 检测到多个 handmux supervisor(pid: {pids});下方访问信息仅来自最后写入状态的实例。',
89
- 'status.cleanupHint': ' 运行 `handmux stop` 可回收全部副本。',
90
- 'status.scanFailed': '⚠ 无法检查 supervisor 进程表;以下状态仅来自 state.json。',
91
-
92
- // logs
93
- 'logs.none': '(还没有日志 —— 先运行 handmux start)',
94
-
95
- // update
96
- 'update.available': ' ⬆ handmux {latest} 可升级(当前 {current})',
97
- 'update.how': ' 升级: handmux update (或 npm i -g handmux@latest)',
98
- 'update.howBrew': ' 升级: brew upgrade handmux',
99
- 'update.brew': '这个 handmux 是用 Homebrew 安装的 — 请用: brew upgrade handmux 升级',
100
- 'update.running': '正在升级 handmux(npm i -g handmux@latest)…',
101
- 'update.done': '✓ handmux 已更新。',
102
- 'update.restartHint': ' 运行 `handmux restart` 以启用新版本。',
103
- 'update.failed': '✗ 升级失败。请手动运行:npm i -g {pkg}@latest(可能需要 sudo)。',
104
-
105
- // cloudflared 自动下载
106
- 'cf.downloading': ' ↓ 正在下载 cloudflared({file})…',
107
-
108
- // natapp / cpolar 客户端二进制
109
- 'client.downloading': ' ↓ 正在下载 {name}({file})…',
110
- 'client.cpolarManual': '✗ cpolar 不可用 —— 到 https://www.cpolar.com/download 下载,解压出二进制放进 ~/.handmux/bin/ 后重试。',
111
- 'client.cpolarAuthFail': '✗ cpolar 拒绝了该 authtoken —— 到 https://dashboard.cpolar.com 核对一下。',
112
- 'client.natappManual': '✗ 未找到 natapp —— 到 https://natapp.cn 下载(需先登录,非公开下载),把 `natapp` 二进制放到 {dir}(或 PATH 上任意位置)后重试。',
113
-
114
- // 访问信息块(printAccess)
115
- 'access.noState': ' (无状态)',
116
- 'access.error': ' ✗ {msg}',
117
- 'access.tunnel': ' 隧道 {tunnel} · pid {pid}',
118
- 'access.open': ' 🌐 打开 {url}',
119
- 'access.pending': '(等待中…)',
120
- 'access.lan': ' 📶 局域网 {url}',
121
- 'access.local': ' 💻 本机 {url}',
122
- 'access.token': ' 🔑 令牌 {token}',
123
- 'access.reachable': ' ✓ 可访问',
124
- 'access.unreachable': ' ⚠ 隧道已起,但 {url} 没有响应 —— 检查服务端的反向代理 / DNS',
125
- 'access.hint': ' handmux status | stop',
126
-
127
- // hooks
128
- 'hooks.confirmEnable': '启用 Claude Code 状态与通知(收件箱)?',
129
- 'hooks.noClaude': '未检测到 Claude Code(缺少 ~/.claude)—— 无需安装。',
130
- 'hooks.installedClaude': '✓ Claude Code hooks 已安装 → ~/.claude/settings.json',
131
- 'hooks.installedHint': ' 重启或新开一个 Claude Code 会话以加载;窗格上报后收件箱就会亮起。',
132
- 'hooks.removed': '✓ Claude Code hooks 已移除。',
133
- 'hooks.usage': '用法:handmux hooks install|uninstall',
134
-
135
- // Claude statusLine 用量捕获(点亮手机用量页的 5h/周额度条)
136
- 'statusline.confirmEnable': '在手机上显示 Claude 的 5h/周额度?(会安装一个 Claude statusLine)',
137
- 'statusline.installed': '✓ Claude statusLine 已安装 → ~/.claude/settings.json',
138
- 'statusline.reload': ' 新开一个 Claude 会话以加载;用量页会随上报逐渐填上。',
139
- 'statusline.foreignHint': '你已经有自己的 Claude statusLine —— 保持不动。想同时点亮手机用量页,把它接到我们的捕获器后面:',
140
-
141
- // service
142
- 'service.usage': '用法:handmux service install [start-flags] | handmux service uninstall',
143
- 'service.installed': "handmux 现在会开机自启。移除请运行 'handmux service uninstall'。",
144
-
145
- // config 命令
146
- 'configcmd.file': '配置文件:{path}',
147
- 'configcmd.fileNone': '(无 —— 使用默认值;运行 `handmux setup` 可创建一个)',
148
- 'configcmd.legend': ' 来源:flag(仅本次)· file · env · default',
149
-
150
- // setup 向导
151
- 'setup.confirmStart': '现在启动 handmux 吗?',
152
- 'setup.later': "准备好后运行 'handmux start' 即可。",
153
- 'setup.laterRestart': "已在运行 —— 运行 'handmux restart' 让改动生效。",
154
- 'setup.needTty': 'handmux setup 需要一个交互式终端',
155
- 'setup.langQ': '命令行语言(handmux 终端输出)/ Command-line language',
156
- 'setup.lang1': ' 1) English',
157
- 'setup.lang2': ' 2) 中文',
158
- 'setup.askName': '应用名称(显示在浏览器标签 / 主屏图标;留空 = 默认)',
159
- 'setup.tunnelQ': '你的手机怎么连到这台机器?',
160
- 'setup.tunnel1': ' 1) none —— 仅同一 Wi-Fi / 局域网',
161
- 'setup.tunnel2': ' 2) cloudflare —— 即时、随机的临时 https 地址',
162
- 'setup.tunnel3': ' 3) cloudflare-named —— 用你自己的域名,稳定 HTTPS(最省心)',
163
- 'setup.tunnel4': ' 4) ssh (tunlite) —— 你自己的服务器 / 边缘',
164
- 'setup.tunnel5': ' 5) natapp —— 国内可用的隧道(需要 natapp authtoken)',
165
- 'setup.tunnel6': ' 6) cpolar —— 国内可用的隧道(自动安装;需要 cpolar authtoken)',
166
- 'setup.choose': '选择 1-6',
167
- 'setup.invalid': '无效选择',
168
- 'setup.askPort': '服务端口',
169
- 'setup.askHostname': '公网域名(例如 handmux.example.com)',
170
- 'setup.askTunnelName': '隧道名称',
171
- 'setup.askSshHost': 'ssh 主机(user@host[:port])',
172
- 'setup.askRemotePort': 'ssh 主机上的远程端口',
173
- 'setup.askPublicUrl': '公网地址,http(s):// 按情况填(留空 = http://host:remotePort)',
174
- 'setup.natappGuide': '去哪拿 authtoken:到 https://natapp.cn 免费注册 → 新建一条隧道 → 复制它的 authtoken(免费额度够上手)。',
175
- 'setup.cpolarGuide': '去哪拿 authtoken:到 https://cpolar.com 免费注册 → 打开后台 → 验证/Verify → 复制你的 authtoken。',
176
- 'setup.askAuthtoken': 'authtoken',
177
- 'setup.askFixed': '用固定域名吗?(否 = 免费临时域名,每次重启都会变)',
178
- 'setup.askNatappDomain': '你在 natapp 后台为该 authtoken 绑定的固定域名(例如 myapp.natapp1.cc)',
179
- 'setup.askCpolarDomain': '你保留的二级域名或绑定的自有域名(例如 myapp.cpolar.top)',
180
- 'setup.askCpolarRegion': 'cpolar 区域(cn = 中国大陆,国内更快;留空 = cpolar 默认)',
181
- 'setup.natappReady': '✓ natapp 客户端就绪',
182
- 'setup.cpolarReady': '✓ cpolar 客户端就绪',
183
- // setup 中枢(菜单模型)
184
- 'setup.welcome': 'handmux 开源、自身不设中转——手机连的始终是你自己的电脑,更私密安全;同一 Wi-Fi 下就是纯直连、开箱即用(选「直连」即可)。想在外网也能连,下面几个是内置帮你打通的方式,用你自己的免费账号即可。不确定就在高亮项上按 Enter,之后随时能改。',
185
- 'setup.hubTitle': '要改什么吗?(或直接保存并启动)',
186
- 'setup.secConnection': '连接',
187
- 'setup.secName': '名称',
188
- 'setup.secPort': '端口',
189
- 'setup.secToken': '令牌',
190
- 'setup.secBrowser': '网页预览器',
191
- 'setup.browserOff': '未配置 · 仅手机直连',
192
- 'setup.askBrowserDomain': '网页预览器代理域名(留空 = 仅手机直连)',
193
- 'setup.browserAbout': '填写代理域名(如 preview.example.com),并将其通配子域以 HTTPS 路由到 Handmux;留空则仅使用手机直连。',
194
- 'setup.tokenAuto': '自动 · 每次启动新生成',
195
- 'setup.tokenCustom': '自定义令牌…',
196
- 'setup.tokenRandom': '随机生成一个',
197
- 'setup.tokenReset': '恢复自动(每次启动新生成)',
198
- 'setup.askToken': '访问令牌 —— 会出现在手机打开的网址里',
199
- 'setup.tokenGenerated': '新令牌:{token}',
200
- 'setup.valToken': '请输入令牌',
201
- 'setup.valTokenSpace': '不能有空格 —— 令牌会放进网址',
202
- 'setup.secLanguage': '命令行语言 / CLI language',
203
- 'setup.secPush': '推送',
204
- 'setup.secVoice': '语音',
205
- 'setup.default': '(默认)',
206
- 'setup.on': '开',
207
- 'setup.off': '关',
208
- 'setup.yes': '是',
209
- 'setup.no': '否',
210
- 'setup.escBack': '(Esc 返回)',
211
- 'setup.actStart': '保存并启动',
212
- 'setup.actRestart': '保存并重启(立即生效)',
213
- 'setup.actSave': '保存',
214
- 'setup.actExit': '退出(丢弃改动)',
215
- 'setup.exited': '已取消 —— 未保存任何改动',
216
- 'setup.tunnelNone': '直连',
217
- 'setup.hintNone': '不经隧道,直连本机 —— 同一 Wi-Fi 或本机有公网 IP 均可;最简单',
218
- 'setup.hintCf': '在外面也能访问、免注册(临时地址;国内可能不稳)',
219
- 'setup.hintCfNamed': '你自己的域名,稳定 HTTPS(最省心)',
220
- 'setup.hintSsh': '你已经有一台服务器可以中转',
221
- 'setup.hintNatapp': '在外面也能访问、国内稳(需免费 natapp 账号)',
222
- 'setup.hintCpolar': '在外面也能访问、国内稳、自动安装(需免费 cpolar 账号)',
223
- 'setup.domainQ': '临时还是固定域名?',
224
- 'setup.domainTemp': '免费临时域名',
225
- 'setup.domainTempHint': '每次重启会变',
226
- 'setup.domainFixed': '固定/保留域名',
227
- 'setup.valPort': '端口需为 1–65535 的整数',
228
- 'setup.valRequired': '{label} 不能为空',
229
- 'setup.valHost': '请输入有效域名(如 myapp.example.com)',
230
- 'setup.valPreviewDomain': '只填域名,如 preview.example.com;不要填 http://、https://、端口或 *.',
231
- 'setup.valContact': '请填 mailto:you@example.com 或 https://你的站点(要真实域名 —— 苹果会拒收假域名)',
232
- 'setup.sumTemp': '临时',
233
- 'setup.sumFixed': '固定',
234
- 'setup.sumNoRelay': '无中转',
235
- // 连接迷你中枢(各隧道字段行)
236
- 'setup.connTunnel': '隧道',
237
- 'setup.connMode': '模式',
238
- 'setup.connHostname': '域名',
239
- 'setup.connTunnelName': '隧道名',
240
- 'setup.connSshHost': 'SSH 主机',
241
- 'setup.connRemotePort': '远程端口',
242
- 'setup.connPublicUrl': '公网地址',
243
- 'setup.connJump': '跳板机',
244
- 'setup.connDomain': '域名',
245
- 'setup.connRegion': '区域',
246
- 'setup.connNone': '(未设置)',
247
- 'setup.connAuto': '(自动)',
248
- 'setup.askSshJump': 'ssh 跳板机(user@host,留空=无)',
249
- 'setup.cfModeQ': 'cloudflare 地址',
250
- 'setup.cfTemp': '临时地址(免登录)',
251
- 'setup.cfTempHint': '即时随机 https,重启会变',
252
- 'setup.cfNamed': '命名隧道(你的域名)',
253
- 'setup.cfNamedHint': '稳定 HTTPS,需登录 Cloudflare',
254
- 'setup.wrote': '✓ 已写入 {path}',
255
- 'setup.pushKeep': '保留已配置的推送通知吗?',
256
- 'setup.pushSetup': '开启手机通知吗?(编程 agent 干完活时提醒你)',
257
- 'setup.pushAbout': 'handmux 会为你自动生成一把私有签名密钥,存在 ~/.handmux/config.json,始终不出本机。下面的联系方式只是给推送服务留的联系地址,别人看不到。',
258
- 'setup.pushContact': '给推送服务留的联系方式 —— mailto: 或 https://(用默认即可;保留 mailto: 前缀)',
259
- 'setup.pushGenerated': '✓ 已生成签名密钥对(存在本机配置里)',
260
- 'setup.pushContactLabel': '联系人',
261
- 'setup.pushRegen': '重新生成密钥',
262
- 'setup.pushRegenHint': '会重置所有手机的订阅',
263
- 'setup.pushRegenConfirm': '重新生成密钥?已订阅的每台手机都会收不到通知,直到重新打开网页 App 再订阅一次。只有密钥泄露时才需要这么做。',
264
- 'setup.pushRegenerated': '✓ 已重新生成密钥 —— 手机需重新订阅',
265
- 'setup.pushOff': '关闭推送',
266
- 'setup.voiceOff': '关闭语音',
267
- 'setup.voiceKeep': '保留已配置的语音输入吗?',
268
- 'setup.voiceSetup': '开启语音输入吗?(对着手机说话转文字;需讯飞账号)',
269
- 'setup.voiceAppId': 'xfyun appId',
270
- 'setup.voiceApiKey': 'xfyun apiKey',
271
- 'setup.voiceApiSecret': 'xfyun apiSecret',
272
- 'setup.voiceSkipped': ' (已跳过 —— 字段缺失)',
273
- 'setup.cfLogin': '→ 正在登录 Cloudflare(会打开一个浏览器)…',
274
- 'setup.cfReuse': '✓ 复用已有隧道 {name}({id})',
275
- 'setup.cfCredMissing1': '⚠ 本机找不到凭据文件 {file} —— 该隧道很可能是',
276
- 'setup.cfCredMissing2': ' 在别处创建的。运行 `{bin} tunnel delete {name}` 再重新 setup,即可在本机重建。',
277
- 'setup.cfCreate': '→ 正在创建隧道 {name} …',
278
- 'setup.cfRoute': '→ 正在把 {host} 路由到隧道 …',
279
- 'setup.cfRouteFail': '⚠ route dns 失败 —— {domain} 的 DNS 托管在 Cloudflare 上吗?',
280
- 'setup.cfRouteFail2': ' 在 Cloudflare 上添加该域名(免费)并把 nameserver 指过去,然后重新 setup。',
281
- 'setup.sshReady': '✓ 免密 SSH 已配置好',
282
- 'setup.sshSetup': '→ 正在配置到 {host} 的免密 SSH(你需要输入一次密码)…',
283
- 'setup.sshHelp1': '服务端(一次性):把反向代理指向被转发的 loopback 端口。',
284
- 'setup.sshHelpNginx': ' nginx: proxy_pass http://127.0.0.1:{port}; 并设置 proxy_http_version 1.1、Upgrade/Connection WebSocket 头、client_max_body_size 60m、proxy_read_timeout 90s;',
285
- 'setup.sshHelpCaddy': ' caddy: {url} { reverse_proxy 127.0.0.1:{port} }',
286
-
287
- // 工作区恢复
288
- 'restore.usage': '用法: handmux restore [--dry-run] [--checkpoint <id>] [--session <name>] | handmux restore --list',
289
- 'restore.badFlag': '未知的恢复参数 {flag}。',
290
- 'restore.unexpectedArgument': '未预期的恢复参数: {value}。',
291
- 'restore.unknownShortFlag': '未知的恢复参数 {flag}。',
292
- 'restore.flagBoolean': '{flag} 不接受参数值。',
293
- 'restore.checkpointValue': '--checkpoint 需要一个 id(或 latest)。',
294
- 'restore.checkpointId': '--checkpoint 必须是有效 id,只能包含字母、数字、点、下划线或连字符。',
295
- 'restore.sessionValue': '--session 需要一个非空会话名。',
296
- 'restore.listExclusive': '--list 必须独占使用,不能与 --dry-run、--checkpoint 或 --session 组合。',
297
- 'restore.listRow': '{id} {time} {sessions} 个会话 · {windows} 个窗口 · {panes} 个窗格 · {agents} 个 agent',
298
- 'restore.listUnavailable': '{id} 不可用: {error}',
299
- 'restore.listEmpty': '目前还没有已归档的工作区备份。',
300
- 'restore.selectCheckpoint': '选择要恢复的工作区备份',
301
- 'restore.selectHint': '{time} · {sessions} 个会话 · {windows} 个窗口 · {panes} 个窗格',
302
- 'restore.noCheckpoint': '没有可用的历史备份。handmux 正在静默保护当前工作区;请在电脑或 tmux 下次重启后再试。',
303
- 'restore.selectionCancelled': '没有选择备份。准备好后可再次运行 `{command}`。',
304
- 'restore.planCheckpoint': '备份: {time}({id};{sessions} 个会话,{windows} 个窗口,{panes} 个窗格)',
305
- 'restore.planCurrent': '当前: {sessions} 个在线会话',
306
- 'restore.planCreate': '+ {session}',
307
- 'restore.planRenamed': '+ {session} -> {target}(名称已被占用)',
308
- 'restore.planAlready': '= {session}(已经恢复)',
309
- 'restore.planUnavailable': '! {session}(不可用: {reason})',
310
- 'restore.reason.linkedWindows': '该备份含有当前版本无法安全恢复的链接窗口',
311
- 'restore.reason.invalidTopology': '该备份的 tmux 拓扑不完整或无效',
312
- 'restore.reason.unknown': '不支持的备份拓扑({reason})',
313
- 'restore.manualRecovery': '请改为手动恢复这个会话: {command}',
314
- 'restore.warning': '警告: {warning}',
315
- 'restore.sessionWarning': ' {session} 警告: {warning}',
316
- 'restore.nonDestructive': '不会停止或修改任何现有会话或进程。',
317
- 'restore.nonDestructivePast': '现有会话没有被修改。',
318
- 'restore.dryRunHint': '运行 `{command}` 继续。',
319
- 'restore.resultRestored': '✓ {session}',
320
- 'restore.resultRenamed': '✓ {session} 已恢复为 {target}',
321
- 'restore.resultAlready': '= {session}(已经恢复)',
322
- 'restore.sessionFailed': '备份 {checkpoint};会话 {session};阶段 {stage}: {error}',
323
- 'restore.operationFailed': '备份 {checkpoint};阶段 {stage}: {error}',
324
- 'restore.retrySession': '只重试这个会话: {command}',
325
- 'restore.resultSummary': '已恢复 {restored} 个会话;{already} 个已存在;{failed} 个失败。',
326
- 'restore.error': '备份 {checkpoint} 在恢复命令阶段失败: {error}',
327
- 'restore.retry': '修复上面报告的原因后,运行 `{command}` 重试。',
328
-
329
- // help
330
- 'help.body': `handmux —— 从手机驱动你的 tmux
331
-
332
- handmux start 直接运行(默认仅局域网;无需配置)
333
- handmux open <会话名> 接入 tmux 会话,不存在则新建——手机上建的也能一键接管
334
- handmux setup 配置隧道 / 名称 / 通知(写入配置;重跑即可修改)
335
- handmux shortcuts 配置命令/聊天模式的共享常用快捷项
336
- handmux stop | restart | status
337
- handmux logs [--follow] [--lines N]
338
- handmux push <标题> <正文> 从脚本推一条通知到手机(--session 会话 · --device 设备key · --tag · --url)
339
- handmux codex [参数...] 启动与对话视图同步的 Codex TUI
340
- handmux restore [--dry-run] [--checkpoint <id>] [--session <name>]
341
- handmux restore --list
342
- handmux update 升级到最新发布版本(npm i -g handmux@latest)
343
-
344
- 第一次用?跑 'handmux setup' —— 它会带你配好外网访问 + 手机通知。
345
- 模型:'start' 运行 · 'setup' 配置(写入 ~/.handmux/config.json)· 重跑 setup 即可修改。
346
-
347
- 更多:
348
- handmux config 显示生效的配置 + 每一项来自哪里
349
- handmux hooks install|uninstall 启用/停用 Claude Code 通知(收件箱)
350
- handmux service install|uninstall 开机自启(launchd/systemd)
351
- handmux help flags 一次性 flag + 环境变量(脚本 / 无头 / Docker)
352
- --config PATH · --lang en|zh · --version, -v
353
- `,
354
- 'help.flags': `handmux flag —— 一次性覆盖 & 无头配置
355
-
356
- 优先级:flag > 文件(~/.handmux/config.json)> 环境变量(HANDMUX_*)> 默认值。
357
- flag 仅本次覆盖某一项、绝不持久化 —— 要持久化请用 'handmux setup'。
358
- 环境变量是无头场景(Docker / systemd / CI)的配置面,会跨多次运行生效。
359
-
360
- start flag(括号内为对应环境变量):
361
- --tunnel none|cloudflare|cloudflare-named|ssh|natapp|cpolar 暴露方式(默认:none)
362
- --port N 服务端口(HANDMUX_PORT,默认:19999)
363
- --host H 绑定地址(HANDMUX_HOST,默认:0.0.0.0)
364
- --token S 鉴权令牌(HANDMUX_TOKEN,默认:每次启动自动生成)
365
- --name "My Box" 浏览器标签 + 主屏图标里的应用名(HANDMUX_APP_NAME)
366
- --public-url URL 对外公布的公网地址(HANDMUX_PUBLIC_URL;任意隧道均可,包括自建的 none;
367
- ssh 默认 http://host:remotePort;natapp/cpolar 填你的固定/保留域名 ——
368
- 留空则用临时域名)
369
- --ssh-host user@host[:port] ssh 隧道目标(HANDMUX_SSH_HOST)
370
- --remote-port N 绑定在 ssh 主机上的端口(HANDMUX_REMOTE_PORT,默认:--port)
371
- --ssh-jump u@h[,…] ssh 的可选跳板机(HANDMUX_SSH_JUMP)
372
- --cf-hostname H cloudflare-named 的公网域名(HANDMUX_CF_HOSTNAME)
373
- --cf-tunnel-name N cloudflare-named 的隧道名(HANDMUX_CF_TUNNEL_NAME,默认:handmux)
374
- --authtoken T natapp / cpolar 的 authtoken(HANDMUX_AUTHTOKEN)
375
- --cpolar-region R cpolar 边缘区域,如 cn(HANDMUX_CPOLAR_REGION)
376
- --foreground, -f 前台运行(不后台化)
377
- --no-qr 不渲染二维码
378
-
379
- 很少用到(环境变量或 flag):--static-dir / --upload-exts
380
- (HANDMUX_STATIC_DIR / HANDMUX_UPLOAD_EXTS)
381
-
382
- 'handmux service install' 也接受上面这些 start flag —— 它们会被烘进自启项。
383
- `,
384
- };
@@ -1,49 +0,0 @@
1
- // Cleanup for Handmux versions that used to install Codex lifecycle hooks. Current Codex integration is
2
- // App Server-only; this module can only remove Handmux's exact marked region and files, never install or
3
- // synchronize hooks and never touch user-owned Codex configuration outside the marker.
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { homedir } from 'node:os';
7
- import { writeFileAtomic } from './hookScaffold.js';
8
-
9
- const BEGIN = '# >>> handmux codex-hooks >>>';
10
- const END = '# <<< handmux codex-hooks <<<';
11
- const LEGACY_FILES = [
12
- 'handmux-notify.sh',
13
- 'handmux-write.cjs',
14
- 'handmux-codex-usage.cjs',
15
- 'handmux-notify.env',
16
- ];
17
-
18
- export function stripLegacyCodexHooks(toml) {
19
- const text = toml || '';
20
- const start = text.indexOf(BEGIN);
21
- if (start < 0) return text;
22
- const end = text.indexOf(END, start);
23
- if (end < 0) return text;
24
- return (text.slice(0, start) + text.slice(end + END.length)).replace(/\n{3,}/g, '\n\n');
25
- }
26
-
27
- export function removeLegacyCodexHooks(home = homedir()) {
28
- const codexDir = path.join(home, '.codex');
29
- const configuredPath = path.join(codexDir, 'config.toml');
30
- let changed = false;
31
- try {
32
- const configFile = fs.lstatSync(configuredPath).isSymbolicLink()
33
- ? fs.realpathSync(configuredPath) : configuredPath;
34
- const mode = fs.statSync(configFile).mode & 0o777;
35
- const before = fs.readFileSync(configFile, 'utf8');
36
- const after = stripLegacyCodexHooks(before);
37
- if (after !== before) {
38
- writeFileAtomic(configFile, after);
39
- fs.chmodSync(configFile, mode);
40
- changed = true;
41
- }
42
- } catch { /* no readable config */ }
43
-
44
- const hooksDir = path.join(codexDir, 'hooks');
45
- for (const name of LEGACY_FILES) {
46
- try { fs.unlinkSync(path.join(hooksDir, name)); changed = true; } catch { /* absent or user-inaccessible */ }
47
- }
48
- return { changed };
49
- }
@@ -1,211 +0,0 @@
1
- // Pure CLI option handling: a tiny hand-rolled flag parser (no dependency) + config resolution.
2
- // Resolution order is flags > config file > env > built-in default. The token is ALWAYS materialised
3
- // (generated when unset) so a public tunnel can never come up token-less.
4
- import crypto from 'node:crypto';
5
- import { normalizeShortcuts } from '../shortcutConfig.js';
6
-
7
- export const TUNNELS = ['none', 'cloudflare', 'cloudflare-named', 'ssh', 'natapp', 'cpolar'];
8
-
9
- const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
10
-
11
- // argv = process.argv.slice(2). `command` is the first bare word; flags are `--key value`,
12
- // `--key` (boolean true), `--no-key` (boolean false), and `-f` (alias for --foreground).
13
- export function parseArgs(argv) {
14
- const [command = 'help', ...rest] = argv;
15
- const flags = {};
16
- const positionals = [];
17
- const unknownShortFlags = [];
18
- const multiFlags = new Set(['session']);
19
- const assign = (key, value) => {
20
- if (!multiFlags.has(key) || flags[key] === undefined) { flags[key] = value; return; }
21
- flags[key] = Array.isArray(flags[key]) ? [...flags[key], value] : [flags[key], value];
22
- };
23
- for (let i = 0; i < rest.length; i++) {
24
- const a = rest[i];
25
- if (a === '-f') { flags.foreground = true; continue; }
26
- if (!a.startsWith('--')) {
27
- if (a.startsWith('-')) unknownShortFlags.push(a);
28
- else positionals.push(a);
29
- continue;
30
- }
31
- const key = a.slice(2);
32
- if (key.startsWith('no-')) { assign(camel(key.slice(3)), false); continue; }
33
- const next = rest[i + 1];
34
- if (next === undefined || next.startsWith('--')) { assign(camel(key), true); }
35
- else { assign(camel(key), next); i++; }
36
- }
37
- return {
38
- command,
39
- flags,
40
- ...(positionals.length ? { positionals } : {}),
41
- ...(unknownShortFlags.length ? { unknownShortFlags } : {}),
42
- };
43
- }
44
-
45
- // ssh 回退公网地址:SSH 不生成 URL,无 --public-url 时用 host:remotePort(去掉 user@ 与 :sshPort)。
46
- export function sshPublicFallback(sshHost, remotePort) {
47
- const host = String(sshHost).replace(/^[^@]*@/, '').replace(/:\d+$/, '');
48
- return `http://${host}:${remotePort}`;
49
- }
50
-
51
- export function resolveConfig(flags = {}, fileCfg = {}, env = process.env, gen = defaultGen) {
52
- const pick = (k, ...fallbacks) => {
53
- for (const v of [flags[k], fileCfg[k], ...fallbacks]) if (v !== undefined && v !== null) return v;
54
- return undefined;
55
- };
56
- const tunnel = pick('tunnel', 'none');
57
- if (!TUNNELS.includes(tunnel)) throw new Error(`unknown tunnel: ${tunnel} (use: ${TUNNELS.join(', ')})`);
58
-
59
- const port = Number(pick('port', env.HANDMUX_PORT, 19999));
60
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`bad port: ${pick('port', env.HANDMUX_PORT, 19999)}`);
61
-
62
- const cfg = {
63
- tunnel,
64
- port,
65
- name: pick('name', env.HANDMUX_APP_NAME) || null,
66
- host: pick('host', env.HANDMUX_HOST, '0.0.0.0'),
67
- token: pick('token', env.HANDMUX_TOKEN) || gen(),
68
- foreground: !!pick('foreground', false),
69
- qr: pick('qr', true) !== false,
70
- // Unified config — what used to live in .env. The supervisor injects these into the server child's
71
- // environment (HANDMUX_STATIC_DIR / VAPID_* / XFYUN_* …), which is exactly where the server reads them.
72
- staticDir: pick('staticDir', env.HANDMUX_STATIC_DIR) || null,
73
- uploadExts: pick('uploadExts', env.HANDMUX_UPLOAD_EXTS) || null,
74
- previewDomain: pick('previewDomain', env.HANDMUX_PREVIEW_DOMAIN) || null,
75
- vapid: fileCfg.vapid || null, // { public, private, subject } — push notifications
76
- xfyun: fileCfg.xfyun || null, // { appId, apiKey, apiSecret } — voice input
77
- shortcuts: normalizeShortcuts(fileCfg.shortcuts),
78
- // An explicit public URL is honoured for ANY tunnel mode — including 'none', so someone who runs their
79
- // own tunnel/reverse-proxy can still have handmux advertise (print + QR) their real domain. The
80
- // tunnel-specific blocks below only fill a *fallback* when it wasn't given.
81
- //
82
- // Guard: a publicUrl in the FILE was set for the file's tunnel, so it only carries over when the
83
- // resolved tunnel still matches (or the file pins no tunnel). Otherwise a `--tunnel B` override would
84
- // advertise tunnel A's URL. A flag/env publicUrl is explicit for THIS run and always wins.
85
- publicUrl: resolvePublicUrl(flags, fileCfg, env, tunnel),
86
- // tunnel-specific (null unless the relevant tunnel is selected)
87
- sshHost: null, remotePort: null, sshJump: null,
88
- cfHostname: null, cfTunnelName: null,
89
- authtoken: null, cpolarRegion: null,
90
- };
91
-
92
- if (tunnel === 'ssh') {
93
- cfg.sshHost = pick('sshHost', env.HANDMUX_SSH_HOST) || null;
94
- if (!cfg.sshHost) throw new Error('ssh tunnel needs --ssh-host user@host (or HANDMUX_SSH_HOST)');
95
- const rp = Number(pick('remotePort', env.HANDMUX_REMOTE_PORT, port));
96
- if (!Number.isInteger(rp) || rp < 1 || rp > 65535) throw new Error(`bad remote-port: ${pick('remotePort', env.HANDMUX_REMOTE_PORT, port)}`);
97
- cfg.remotePort = rp;
98
- cfg.sshJump = pick('sshJump', env.HANDMUX_SSH_JUMP) || null;
99
- cfg.publicUrl = cfg.publicUrl || sshPublicFallback(cfg.sshHost, rp);
100
- }
101
-
102
- if (tunnel === 'cloudflare-named') {
103
- cfg.cfHostname = pick('cfHostname', env.HANDMUX_CF_HOSTNAME) || null;
104
- if (!cfg.cfHostname) throw new Error('cloudflare-named needs --cf-hostname handmux.example.com (or HANDMUX_CF_HOSTNAME)');
105
- cfg.cfTunnelName = pick('cfTunnelName', env.HANDMUX_CF_TUNNEL_NAME) || 'handmux';
106
- cfg.publicUrl = cfg.publicUrl || `https://${cfg.cfHostname}`;
107
- }
108
-
109
- // natapp / cpolar: one shared credential (--authtoken), and named/fixed mode is just --public-url (the
110
- // reserved domain) — no separate concept. A bare host is normalised to https:// so the user can paste the
111
- // domain the provider gave them. cpolar also takes an optional edge --cpolar-region (cn = mainland China).
112
- if (tunnel === 'natapp' || tunnel === 'cpolar') {
113
- cfg.authtoken = pick('authtoken', env.HANDMUX_AUTHTOKEN) || null;
114
- if (!cfg.authtoken) {
115
- const where = tunnel === 'natapp' ? 'natapp.cn' : 'cpolar.com';
116
- throw new Error(`${tunnel} needs --authtoken <token> (or HANDMUX_AUTHTOKEN) — get it from ${where} after logging in`);
117
- }
118
- if (cfg.publicUrl && !/^https?:\/\//i.test(cfg.publicUrl)) cfg.publicUrl = `https://${cfg.publicUrl}`;
119
- if (tunnel === 'cpolar') cfg.cpolarRegion = pick('cpolarRegion', env.HANDMUX_CPOLAR_REGION) || null;
120
- }
121
-
122
- return cfg;
123
- }
124
-
125
- // publicUrl resolution with the cross-tunnel guard (see resolveConfig). flag > file(if tunnel matches) > env.
126
- function resolvePublicUrl(flags, fileCfg, env, tunnel) {
127
- if (flags.publicUrl != null) return flags.publicUrl;
128
- const fileTunnel = fileCfg.tunnel;
129
- const fileApplies = fileTunnel == null || fileTunnel === tunnel;
130
- if (fileApplies && fileCfg.publicUrl != null) return fileCfg.publicUrl;
131
- if (env.HANDMUX_PUBLIC_URL != null) return env.HANDMUX_PUBLIC_URL;
132
- return null;
133
- }
134
-
135
- // Default token: 8 chars from a typing-friendly alphabet (lowercase + digits, look-alikes 0/o/1/l dropped)
136
- // so it's quick to thumb in on a phone. A user-supplied token (flag/config/env) is used verbatim — any
137
- // length, never regenerated. 8 chars over a 32-char alphabet ≈ 40 bits, fine for a single secret URL.
138
- const TOKEN_ALPHABET = '23456789abcdefghijkmnpqrstuvwxyz';
139
- export function defaultGen() {
140
- let s = '';
141
- for (let i = 0; i < 8; i++) s += TOKEN_ALPHABET[crypto.randomInt(TOKEN_ALPHABET.length)];
142
- return s;
143
- }
144
-
145
- // Trace one key's value back to its source through the same flag > file > env > default precedence
146
- // resolveConfig uses. `null`/`undefined` at a layer is "unset" (skip), matching pick(). Returns the
147
- // winning value and a human origin label (the file path for a file hit, so the user sees exactly where).
148
- function trace(flags, fileCfg, env, cfgPath, key, envKey, def) {
149
- if (flags[key] != null) return { value: flags[key], origin: 'flag' };
150
- if (fileCfg[key] != null) return { value: fileCfg[key], origin: cfgPath || 'file' };
151
- if (envKey && env[envKey] != null) return { value: env[envKey], origin: 'env' };
152
- return { value: def, origin: 'default' };
153
- }
154
-
155
- // Build the rows for `handmux config`: the value each key WOULD resolve to plus where it came from.
156
- // Lenient (never throws — unlike resolveConfig — so a half-finished config still prints) and secret-safe
157
- // (token masked; push/voice shown only as on/off). Tunnel-specific rows appear only for the live tunnel.
158
- export function explainConfig(flags = {}, fileCfg = {}, cfgPath = null, env = process.env) {
159
- const rows = [];
160
- const mask = (s) => (String(s).length <= 8 ? '••••' : `••••${String(s).slice(-4)}`);
161
- const add = (key, t, display) => rows.push({ key, origin: t.origin, display: display ?? String(t.value) });
162
-
163
- const tunnel = trace(flags, fileCfg, env, cfgPath, 'tunnel', null, 'none');
164
- add('tunnel', tunnel);
165
- add('port', trace(flags, fileCfg, env, cfgPath, 'port', 'HANDMUX_PORT', 19999));
166
- add('host', trace(flags, fileCfg, env, cfgPath, 'host', 'HANDMUX_HOST', '0.0.0.0'));
167
-
168
- const name = trace(flags, fileCfg, env, cfgPath, 'name', 'HANDMUX_APP_NAME', null);
169
- add('name', name, name.value == null ? '(default)' : String(name.value));
170
-
171
- const lang = trace(flags, fileCfg, env, cfgPath, 'lang', 'HANDMUX_LANG', null);
172
- add('lang', lang, lang.value == null ? '(auto — shell locale)' : String(lang.value));
173
-
174
- const token = trace(flags, fileCfg, env, cfgPath, 'token', 'HANDMUX_TOKEN', null);
175
- add('token', token, token.value == null ? '(generated each start)' : mask(token.value));
176
-
177
- // publicUrl honours the same cross-tunnel guard as resolveConfig (file value only when tunnel matches).
178
- const t = tunnel.value;
179
- let pub = trace(flags, fileCfg, env, cfgPath, 'publicUrl', 'HANDMUX_PUBLIC_URL', null);
180
- if (pub.origin !== 'flag' && pub.origin !== 'env' && fileCfg.tunnel != null && fileCfg.tunnel !== t) {
181
- pub = { value: null, origin: 'default' };
182
- }
183
- add('publicUrl', pub, pub.value == null ? '(none — derived from tunnel if any)' : String(pub.value));
184
-
185
- const previewDomain = trace(flags, fileCfg, env, cfgPath, 'previewDomain', 'HANDMUX_PREVIEW_DOMAIN', null);
186
- add('previewDomain', previewDomain, previewDomain.value == null ? '(none)' : String(previewDomain.value));
187
-
188
- if (t === 'ssh') {
189
- add('sshHost', trace(flags, fileCfg, env, cfgPath, 'sshHost', 'HANDMUX_SSH_HOST', null));
190
- add('remotePort', trace(flags, fileCfg, env, cfgPath, 'remotePort', 'HANDMUX_REMOTE_PORT', '(= port)'));
191
- const jump = trace(flags, fileCfg, env, cfgPath, 'sshJump', 'HANDMUX_SSH_JUMP', null);
192
- add('sshJump', jump, jump.value == null ? '(none)' : String(jump.value));
193
- }
194
- if (t === 'cloudflare-named') {
195
- add('cfHostname', trace(flags, fileCfg, env, cfgPath, 'cfHostname', 'HANDMUX_CF_HOSTNAME', null));
196
- add('cfTunnelName', trace(flags, fileCfg, env, cfgPath, 'cfTunnelName', 'HANDMUX_CF_TUNNEL_NAME', 'handmux'));
197
- }
198
- if (t === 'natapp' || t === 'cpolar') {
199
- const at = trace(flags, fileCfg, env, cfgPath, 'authtoken', 'HANDMUX_AUTHTOKEN', null);
200
- add('authtoken', at, at.value == null ? '(required — from the provider dashboard)' : mask(at.value));
201
- }
202
- if (t === 'cpolar') {
203
- const rg = trace(flags, fileCfg, env, cfgPath, 'cpolarRegion', 'HANDMUX_CPOLAR_REGION', null);
204
- add('cpolarRegion', rg, rg.value == null ? '(cpolar default)' : String(rg.value));
205
- }
206
-
207
- // Integrations live only in the file (secrets); show presence, never the keys.
208
- rows.push({ key: 'push (vapid)', origin: fileCfg.vapid ? (cfgPath || 'file') : 'default', display: fileCfg.vapid ? 'on' : 'off' });
209
- rows.push({ key: 'voice (xfyun)', origin: fileCfg.xfyun ? (cfgPath || 'file') : 'default', display: fileCfg.xfyun ? 'on' : 'off' });
210
- return rows;
211
- }