dsh-zen-remote 2.3.11 → 2.4.1
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/CHANGELOG.md +45 -78
- package/README.md +94 -84
- package/README.zh-CN.md +90 -81
- package/bin/zen-remote.mjs +5 -0
- package/docs/interface.md +6 -6
- package/docs/remote-access.en.md +6 -4
- package/docs/remote-access.md +6 -4
- package/dsh-push.mjs +96 -10
- package/lib/adapters/dsh-better-sidebar/index.js +7 -3
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/admin-routes.js +10 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/cli/args.js +251 -0
- package/lib/cli/args.js.map +1 -0
- package/lib/cli/bootstrap.js +449 -0
- package/lib/cli/bootstrap.js.map +1 -0
- package/lib/cli/command-setup.js +484 -0
- package/lib/cli/command-setup.js.map +1 -0
- package/lib/cli/commands-client.js +359 -0
- package/lib/cli/commands-client.js.map +1 -0
- package/lib/cli/commands-host.js +1108 -0
- package/lib/cli/commands-host.js.map +1 -0
- package/lib/cli/commands-status.js +279 -0
- package/lib/cli/commands-status.js.map +1 -0
- package/lib/cli/config-writer.js +334 -0
- package/lib/cli/config-writer.js.map +1 -0
- package/lib/cli/context.js +10 -0
- package/lib/cli/context.js.map +1 -0
- package/lib/cli/i18n.js +620 -0
- package/lib/cli/i18n.js.map +1 -0
- package/lib/cli/ipc-client.js +160 -0
- package/lib/cli/ipc-client.js.map +1 -0
- package/lib/cli/ipc-server.js +464 -0
- package/lib/cli/ipc-server.js.map +1 -0
- package/lib/cli/main.js +245 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/qr-terminal.js +48 -0
- package/lib/cli/qr-terminal.js.map +1 -0
- package/lib/client-data/qr.js +286 -0
- package/lib/client-data/qr.js.map +1 -0
- package/lib/client-routes.js +308 -25
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +1013 -229
- package/lib/config.js +50 -12
- package/lib/config.js.map +1 -1
- package/lib/debug-report.js +28 -1
- package/lib/debug-report.js.map +1 -1
- package/lib/fetch-route-intercept.js +334 -26
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +608 -375
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +1833 -280
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +36 -10
- package/lib/merge-streams.js +103 -11
- package/lib/merge-streams.js.map +1 -1
- package/lib/plugin-route-intercept.js +93 -63
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/relay-access.js +8 -1
- package/lib/relay-access.js.map +1 -1
- package/lib/relay-client.js +133 -4
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-pool.js +275 -0
- package/lib/relay-pool.js.map +1 -0
- package/lib/relay-server.js +269 -17
- package/lib/relay-server.js.map +1 -1
- package/lib/restart-fields.js +6 -4
- package/lib/restart-fields.js.map +1 -1
- package/lib/share-ops.js +68 -0
- package/lib/share-ops.js.map +1 -1
- package/lib/share-store.js +65 -27
- package/lib/share-store.js.map +1 -1
- package/lib/state-store.js +400 -0
- package/lib/state-store.js.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/cli/args.d.ts +78 -0
- package/lib/types/cli/args.d.ts.map +1 -0
- package/lib/types/cli/bootstrap.d.ts +170 -0
- package/lib/types/cli/bootstrap.d.ts.map +1 -0
- package/lib/types/cli/command-setup.d.ts +72 -0
- package/lib/types/cli/command-setup.d.ts.map +1 -0
- package/lib/types/cli/commands-client.d.ts +36 -0
- package/lib/types/cli/commands-client.d.ts.map +1 -0
- package/lib/types/cli/commands-host.d.ts +82 -0
- package/lib/types/cli/commands-host.d.ts.map +1 -0
- package/lib/types/cli/commands-status.d.ts +46 -0
- package/lib/types/cli/commands-status.d.ts.map +1 -0
- package/lib/types/cli/config-writer.d.ts +92 -0
- package/lib/types/cli/config-writer.d.ts.map +1 -0
- package/lib/types/cli/context.d.ts +39 -0
- package/lib/types/cli/context.d.ts.map +1 -0
- package/lib/types/cli/i18n.d.ts +32 -0
- package/lib/types/cli/i18n.d.ts.map +1 -0
- package/lib/types/cli/ipc-client.d.ts +83 -0
- package/lib/types/cli/ipc-client.d.ts.map +1 -0
- package/lib/types/cli/ipc-server.d.ts +113 -0
- package/lib/types/cli/ipc-server.d.ts.map +1 -0
- package/lib/types/cli/main.d.ts +57 -0
- package/lib/types/cli/main.d.ts.map +1 -0
- package/lib/types/cli/qr-terminal.d.ts +17 -0
- package/lib/types/cli/qr-terminal.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +4 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -1
- package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +47 -3
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/settings/settings-css.d.ts +1 -1
- package/lib/types/client/settings/settings-css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
- package/lib/types/client-data/model-group-side.d.ts +23 -2
- package/lib/types/client-data/model-group-side.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +47 -1
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +85 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +56 -1
- package/lib/types/client-data/wizard.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +25 -1
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +40 -14
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/debug-report.d.ts.map +1 -1
- package/lib/types/fetch-route-intercept.d.ts +66 -8
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +72 -10
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts +79 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +25 -9
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/relay-access.d.ts +3 -1
- package/lib/types/relay-access.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +23 -2
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-pool.d.ts +148 -0
- package/lib/types/relay-pool.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +30 -3
- package/lib/types/relay-server.d.ts.map +1 -1
- package/lib/types/restart-fields.d.ts +5 -5
- package/lib/types/restart-fields.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +47 -1
- package/lib/types/share-ops.d.ts.map +1 -1
- package/lib/types/share-store.d.ts.map +1 -1
- package/lib/types/state-store.d.ts +179 -0
- package/lib/types/state-store.d.ts.map +1 -0
- package/lib/types/upload-limits.d.ts +15 -0
- package/lib/types/upload-limits.d.ts.map +1 -1
- package/lib/types/virtual-id.d.ts +21 -0
- package/lib/types/virtual-id.d.ts.map +1 -1
- package/lib/types/wizard/probe.d.ts.map +1 -1
- package/lib/types/wizard/proxy-snippets.d.ts +3 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
- package/lib/types/wizard/routes.d.ts +1 -0
- package/lib/types/wizard/routes.d.ts.map +1 -1
- package/lib/types/wizard/status.d.ts +9 -0
- package/lib/types/wizard/status.d.ts.map +1 -1
- package/lib/types/wizard/verify.d.ts +3 -0
- package/lib/types/wizard/verify.d.ts.map +1 -1
- package/lib/upload-limits.js +15 -0
- package/lib/upload-limits.js.map +1 -1
- package/lib/virtual-id.js +60 -0
- package/lib/virtual-id.js.map +1 -1
- package/lib/wizard/probe.js +46 -1
- package/lib/wizard/probe.js.map +1 -1
- package/lib/wizard/proxy-snippets.js +14 -1
- package/lib/wizard/proxy-snippets.js.map +1 -1
- package/lib/wizard/routes.js +23 -4
- package/lib/wizard/routes.js.map +1 -1
- package/lib/wizard/status.js +21 -0
- package/lib/wizard/status.js.map +1 -1
- package/lib/wizard/verify.js +31 -8
- package/lib/wizard/verify.js.map +1 -1
- package/package.json +8 -4
- package/pwa/icons/badge-96.png +0 -0
- package/pwa/icons/badge.svg +1 -0
- package/pwa/manifest.json +2 -1
- package/pwa/sw.js +3 -1
- package/scripts/check-debug-report.mjs +41 -1
- package/scripts/check-settings-form.mjs +14 -3
- package/scripts/check-settings-view.mjs +247 -0
- package/scripts/check-wizard-ui.mjs +37 -0
- package/src/adapters/dsh-better-sidebar/index.ts +7 -3
- package/src/admin-routes.ts +10 -2
- package/src/cli/args.ts +289 -0
- package/src/cli/bootstrap.ts +569 -0
- package/src/cli/command-setup.ts +540 -0
- package/src/cli/commands-client.ts +369 -0
- package/src/cli/commands-host.ts +1127 -0
- package/src/cli/commands-status.ts +296 -0
- package/src/cli/config-writer.ts +354 -0
- package/src/cli/context.ts +38 -0
- package/src/cli/i18n.ts +645 -0
- package/src/cli/ipc-client.ts +216 -0
- package/src/cli/ipc-server.ts +531 -0
- package/src/cli/main.ts +285 -0
- package/src/cli/qr-terminal.ts +49 -0
- package/src/client/RemoteStatusIcon.tsx +4 -1
- package/src/client/current-session-report.ts +28 -4
- package/src/client/effects/model-group-side.ts +19 -5
- package/src/client/effects/remote-session.ts +26 -6
- package/src/client/locales.ts +96 -6
- package/src/client/settings/SettingsSection.tsx +287 -92
- package/src/client/settings/settings-css.ts +18 -0
- package/src/client/styles/remote-session.css.ts +15 -1
- package/src/client/wizard/GatewayWizard.tsx +470 -109
- package/src/client/wizard/cards/ProxyCard.tsx +2 -1
- package/src/client/wizard/wizard-css.ts +23 -0
- package/src/client-data/model-group-side.ts +37 -10
- package/src/client-data/remote-status.ts +112 -17
- package/src/client-data/settings-form.ts +164 -8
- package/src/client-data/wizard.ts +112 -2
- package/src/client-routes.ts +334 -26
- package/src/config.ts +62 -16
- package/src/debug-report.ts +27 -1
- package/src/fetch-route-intercept.ts +381 -32
- package/src/index.ts +608 -380
- package/src/intercept.ts +1866 -286
- package/src/merge-streams.ts +155 -10
- package/src/plugin-route-intercept.ts +120 -71
- package/src/relay-access.ts +14 -3
- package/src/relay-client.ts +163 -5
- package/src/relay-pool.ts +387 -0
- package/src/relay-server.ts +312 -16
- package/src/restart-fields.ts +6 -4
- package/src/share-ops.ts +82 -1
- package/src/share-store.ts +59 -26
- package/src/state-store.ts +428 -0
- package/src/upload-limits.ts +16 -0
- package/src/virtual-id.ts +57 -0
- package/src/wizard/probe.ts +54 -2
- package/src/wizard/proxy-snippets.ts +17 -2
- package/src/wizard/routes.ts +24 -5
- package/src/wizard/status.ts +28 -0
- package/src/wizard/verify.ts +33 -11
- package/AGENTS.md +0 -185
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
- package/docs/adr/0002-relay-emulates-websocket.md +0 -8
package/src/cli/i18n.ts
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2 bilingual dictionary and language resolution. Per SPEC the IPC
|
|
3
|
+
* channel only ever carries machine-readable codes and structured data —
|
|
4
|
+
* every terminal string the user sees is rendered HERE, on the CLI side, in
|
|
5
|
+
* the selected language. Placeholder syntax is `{name}`, filled from the
|
|
6
|
+
* `params` argument.
|
|
7
|
+
*
|
|
8
|
+
* Vocabulary discipline (SPEC): the copy speaks ONLY of `host` / `client` /
|
|
9
|
+
* `peer` — `server` / `both` and similar aliases must not leak into any
|
|
10
|
+
* string, in either language. The test file scans both tables for the banned
|
|
11
|
+
* English words to keep it that way.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type CliLang = 'zh' | 'en'
|
|
15
|
+
|
|
16
|
+
export type CliTranslator = ((key: string, params?: Record<string, string | number>) => string) & {
|
|
17
|
+
/** The language's raw table, for tests and for key inventory checks. */
|
|
18
|
+
readonly keys: Readonly<Record<string, string>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ZH: Readonly<Record<string, string>> = {
|
|
22
|
+
// ---- help -----------------------------------------------------------------
|
|
23
|
+
'help.title': 'dsh-zen-remote —— DeepSeek Harness 远程接入命令行',
|
|
24
|
+
'help.usage': '用法:dsh-zen-remote <命令> [参数]',
|
|
25
|
+
'help.description': '无需打开浏览器即可完成远程接入配置、设备配对与连接管理。',
|
|
26
|
+
'help.commands': '命令:',
|
|
27
|
+
'help.command.setup': 'setup 交互式配置向导(角色、接入方式、配对)',
|
|
28
|
+
'help.command.status': 'status 查看当前角色、网关与连接状态',
|
|
29
|
+
'help.command.hostDevice': 'host device pair|list|revoke host:配对 / 列出 / 吊销接入设备',
|
|
30
|
+
'help.command.hostIngress': 'host ingress start|stop|verify|snippet host:管理公网接入',
|
|
31
|
+
'help.command.clientPair': 'client pair <hostUrl> client:配对一台远端 host',
|
|
32
|
+
'help.command.clientUnpair': 'client unpair [<hostUrl>|<entryId>|--all] client:移除已配对的 host',
|
|
33
|
+
'help.command.clientList': 'client list client:列出已配对的 host',
|
|
34
|
+
'help.options': '参数:',
|
|
35
|
+
'help.option.lang': '--lang zh|en|auto 界面语言(默认 auto)',
|
|
36
|
+
'help.option.json': '--json 机器可读 JSON 输出',
|
|
37
|
+
'help.option.socket': '--socket <path> 指定 IPC socket 路径(默认自动探测)',
|
|
38
|
+
'help.option.wait': '--wait <seconds> 等待 DSH 启动 / 重载的秒数(默认 20)',
|
|
39
|
+
'help.option.yes': '--yes, -y 非交互模式:全部采用默认值,绝不挂起等输入',
|
|
40
|
+
'help.option.noWait': '--no-wait 跳过最后的等待环节(如等待手机完成配对)',
|
|
41
|
+
'help.option.debug': 'status --debug [outPath] 生成脱敏诊断报告 JSON',
|
|
42
|
+
'help.option.role': '--role host|client 本机运行角色(peer 尚未启用)',
|
|
43
|
+
'help.option.profile': '--profile p1,p2 要安装插件的 DSH profile 列表',
|
|
44
|
+
'help.option.ingress': '--ingress, --mode caddy|cloudflared|external|lan 公网接入方式',
|
|
45
|
+
'help.option.domain': '--domain <host> 域名',
|
|
46
|
+
'help.option.cfToken': '--cf-token <token> Cloudflare API 令牌',
|
|
47
|
+
'help.option.email': '--email <email> 证书通知邮箱(可选)',
|
|
48
|
+
'help.option.httpsPort': '--https-port <port> 对外 HTTPS 端口(默认 443)',
|
|
49
|
+
'help.option.publicUrl': '--public-url <origin> 公网访问地址(https:// 开头)',
|
|
50
|
+
'help.option.tunnelId': '--tunnel-id <id> 复用已有的 Cloudflare Tunnel ID',
|
|
51
|
+
'help.option.proxy': '--proxy nginx|caddy|lucky external 接入的反代类型',
|
|
52
|
+
'help.option.deviceRole': '--device-role web|desktop-client 待配对的设备类型',
|
|
53
|
+
'help.option.hostUrl': '--host-url <url> 远端 host 地址',
|
|
54
|
+
'help.option.code': '--code <8位码> 配对码',
|
|
55
|
+
'help.option.name': '--name <name> 本机显示名',
|
|
56
|
+
'help.option.all': '--all 作用于全部(设备 / host 连接)',
|
|
57
|
+
'help.footer': '提示:不带命令运行即显示本帮助;详见 docs/remote-access.md。',
|
|
58
|
+
|
|
59
|
+
// ---- common -----------------------------------------------------------------
|
|
60
|
+
'common.yes': '是',
|
|
61
|
+
'common.no': '否',
|
|
62
|
+
'common.done': '完成',
|
|
63
|
+
'common.cancel': '取消',
|
|
64
|
+
'common.retry': '重试',
|
|
65
|
+
'common.back': '返回',
|
|
66
|
+
|
|
67
|
+
// ---- errors & gates -----------------------------------------------------------
|
|
68
|
+
'err.unknownCommand': '未知命令:{command}。运行 dsh-zen-remote --help 查看全部命令。',
|
|
69
|
+
'err.role.peerNotEnabled': 'peer 角色尚未启用:CLI 暂不提供 peer 入口,请使用 host 或 client。',
|
|
70
|
+
'err.role.invalid': '未知角色:{value}(可选 host 或 client;peer 尚未启用)。',
|
|
71
|
+
'err.roleGate.hostRequired': '此命令需要 host 角色,当前角色是 {role}。请先用 setup 切换角色。',
|
|
72
|
+
'err.roleGate.clientRequired': '此命令需要 client 角色,当前角色是 {role}。请先用 setup 切换角色。',
|
|
73
|
+
'err.roleGate.clientCommands': '当前 role 是 {role},不连接远端 host(可用 dsh-zen-remote setup --role client 切换)。',
|
|
74
|
+
'err.envShadowed': '字段 {field} 当前被环境变量 {envVar} 遮蔽:请修改该环境变量或启动脚本后再试。',
|
|
75
|
+
'err.crossUserSocket': '检测到 DSH 以其他系统用户运行,无法访问它的控制 socket:请用同一用户运行本命令,或执行 sudo -E dsh-zen-remote … 重试。',
|
|
76
|
+
'err.missingOption': '缺少必填参数 --{option}:非交互模式不会挂起等待输入,请补上参数或改在终端交互运行。',
|
|
77
|
+
'err.desktopProfile': 'desktop profile 由 DeepSeek Harness App 管理,不能通过 CLI 安装插件:{profiles}。',
|
|
78
|
+
'err.protocolVersion': '本机 DSH 插件的 CLI 协议版本({found})与本 CLI({expected})不一致:请升级 dsh-zen-remote 插件或 CLI。',
|
|
79
|
+
'err.notRunning': '未检测到运行中的 DSH 插件:请先启动 DSH(可尝试 dsh --profile {profile} web)再运行本命令。',
|
|
80
|
+
|
|
81
|
+
// ---- profile install -----------------------------------------------------------
|
|
82
|
+
'install.scanning': '正在扫描 <DSH_HOME>/profiles 下的候选 profile …',
|
|
83
|
+
'install.installed': '已安装插件的 profile:{profiles}',
|
|
84
|
+
'install.noneInstalled': '尚未在任何 profile 安装 dsh-zen-remote。',
|
|
85
|
+
'install.selectQuestion': '选择要安装 dsh-zen-remote 的 profile',
|
|
86
|
+
'install.customEntry': '手动输入其他 profile 名称',
|
|
87
|
+
'install.running': '正在安装:dsh plugin --profile {profile} add dsh-zen-remote …',
|
|
88
|
+
'install.failed': '为 {profile} 安装失败(退出码 {code})。请手动执行:{command}',
|
|
89
|
+
'install.done': '安装完成:{profiles}',
|
|
90
|
+
|
|
91
|
+
// ---- daemon launch --------------------------------------------------------------
|
|
92
|
+
'launch.found': '检测到运行中的 DSH(角色 {role},{socketPath})。',
|
|
93
|
+
'launch.probing': '未发现运行中的 DSH,尝试拉起系统守护 / 应用 …',
|
|
94
|
+
'launch.linuxUserUnit': '发现 systemd 用户单元 {unit},正在启动 …',
|
|
95
|
+
'launch.linuxSystemUnit': '发现系统级 systemd 单元 {unit},正在启动 …',
|
|
96
|
+
'launch.macosService': '发现 launchd 服务 ai.deepseek.dsh.web,正在 kickstart …',
|
|
97
|
+
'launch.macosApp': '正在启动 DeepSeek Harness 应用 …',
|
|
98
|
+
'launch.waiting': '等待 DSH 就绪(最多 {seconds} 秒)…',
|
|
99
|
+
'launch.timeout': '等待 DSH 启动超时({seconds} 秒)。请确认 DSH 正常启动后重试。',
|
|
100
|
+
'launch.manual': '无法自动拉起 DSH,请手动启动后重试,例如:\n dsh --profile {profile} web\n或配置一个 systemd 用户单元(~/.config/systemd/user/dsh-web.service):\n [Unit]\n Description=DeepSeek Harness\n [Service]\n ExecStart=dsh --profile {profile} web\n [Install]\n WantedBy=default.target\n(macOS 可直接打开 DeepSeek Harness 应用;Windows 请在终端运行 dsh 命令。)',
|
|
101
|
+
|
|
102
|
+
// ---- prompts(默认 readline 交互的脚手架文案)-----------------------------------
|
|
103
|
+
'prompt.defaultLabel': '默认',
|
|
104
|
+
'prompt.selectOne': '输入序号 [1-{max}](回车 = {default}):',
|
|
105
|
+
'prompt.multiInputPlain': '输入序号(逗号/空格分隔,回车确认默认):',
|
|
106
|
+
'prompt.multiInputCustom': '输入序号(逗号/空格分隔,回车确认默认;也可输入 a 手动添加):',
|
|
107
|
+
'prompt.customEntryLine': ' a. {entry}',
|
|
108
|
+
'prompt.customName': 'profile 名称(回车结束):',
|
|
109
|
+
'prompt.invalid': '无效输入:{value}',
|
|
110
|
+
'prompt.requiredEmpty': '此项必填,不能为空。',
|
|
111
|
+
'prompt.confirmY': '[Y/n]',
|
|
112
|
+
'prompt.confirmN': '[y/N]',
|
|
113
|
+
|
|
114
|
+
// ---- status ----------------------------------------------------------------------
|
|
115
|
+
'status.role.host': 'host(作为服务端:供手机 / 其他电脑接入)',
|
|
116
|
+
'status.role.client': 'client(连接远端 host)',
|
|
117
|
+
'status.role.peer': 'peer(手工设置,未启用)',
|
|
118
|
+
'status.line.role': '当前角色:{role}',
|
|
119
|
+
'status.machineName': '本机名称:{name}',
|
|
120
|
+
'status.pluginVersion': '插件版本:{version}',
|
|
121
|
+
'status.host.title': 'host 状态:',
|
|
122
|
+
'status.host.gatewayUp': '网关监听中(端口 {port})',
|
|
123
|
+
'status.host.gatewayUpNoPort': '网关监听中',
|
|
124
|
+
'status.host.gatewayDown': '网关未在监听',
|
|
125
|
+
'status.host.ingress': '接入方式:{mode}',
|
|
126
|
+
'status.host.publicUrl': '公开地址:{publicUrl}',
|
|
127
|
+
'status.host.processCaddy': 'Caddy 进程:{state}',
|
|
128
|
+
'status.host.processTunnel': 'Cloudflare Tunnel 进程:{state}',
|
|
129
|
+
'status.process.running': '运行中',
|
|
130
|
+
'status.process.stopped': '未运行',
|
|
131
|
+
'status.host.devices': '已配对设备:{count} 台',
|
|
132
|
+
'status.host.deviceLine': ' · {name}({deviceRole})',
|
|
133
|
+
'status.host.sharedSessions': '已开启远程共享的会话:{count} 个',
|
|
134
|
+
'status.client.title': 'client 状态:',
|
|
135
|
+
'status.client.pairedCount': '已配对的 host:{count} / {max} 台',
|
|
136
|
+
'status.state.revoked': '已吊销',
|
|
137
|
+
'status.state.incompatible': '版本不兼容',
|
|
138
|
+
'status.client.compatOk': '接口指纹一致',
|
|
139
|
+
'status.client.compatDiff': '接口指纹差异 {count} 项',
|
|
140
|
+
'status.client.compatUnavailable': '接口指纹不可用',
|
|
141
|
+
'status.client.bridgeCount': '已桥接 {count} 个插件',
|
|
142
|
+
'status.client.lastError': '上次错误:{error}',
|
|
143
|
+
'status.debugFailed': '诊断报告获取失败:{error}',
|
|
144
|
+
'status.debugWriteFailed': '诊断报告写文件失败:{error}',
|
|
145
|
+
'status.notRunning': '未检测到运行中的 DSH 插件。',
|
|
146
|
+
'status.paired': '已配对 {count} 台',
|
|
147
|
+
'status.unpaired': '未配对',
|
|
148
|
+
'status.online': '在线',
|
|
149
|
+
'status.offline': '离线',
|
|
150
|
+
'status.connecting': '连接中',
|
|
151
|
+
'status.profile': 'profile:{profile}',
|
|
152
|
+
'status.debugWritten': '诊断报告已写出:{path}',
|
|
153
|
+
|
|
154
|
+
// ---- setup wizard ------------------------------------------------------------------
|
|
155
|
+
'setup.roleQuestion': '选择本机运行角色:',
|
|
156
|
+
'setup.roleHost': 'host —— 作为服务端运行(开放手机 / 其他电脑连入)',
|
|
157
|
+
'setup.roleClient': 'client —— 作为客户端运行(连接远端 host)',
|
|
158
|
+
'setup.switchingToClient': '切换到 client:先停掉托管的 Caddy / Cloudflare 隧道,再写入配置 …',
|
|
159
|
+
'setup.switchingToHost': '切换到 host:写入配置并等待网关就绪 …',
|
|
160
|
+
'setup.ingressQuestion': '选择公网接入方式:',
|
|
161
|
+
'setup.ingressKeep': '保持当前接入配置,直接去验证 / 生成配对码',
|
|
162
|
+
'setup.ingressCaddy': 'caddy —— 内置 Caddy 自动证书',
|
|
163
|
+
'setup.ingressCloudflared': 'cloudflared —— Cloudflare Tunnel 内网穿透',
|
|
164
|
+
'setup.ingressExternal': 'external —— 已有反向代理(Nginx / Caddy / Lucky)',
|
|
165
|
+
'setup.ingressLan': 'lan —— 局域网 / Tailscale 直连',
|
|
166
|
+
'setup.domainQuestion': '域名:',
|
|
167
|
+
'setup.cfTokenQuestion': 'Cloudflare API 令牌:',
|
|
168
|
+
'setup.emailQuestion': '证书通知邮箱(可留空):',
|
|
169
|
+
'setup.httpsPortQuestion': '对外 HTTPS 端口(默认 443):',
|
|
170
|
+
'setup.publicUrlQuestion': '公网访问地址(https:// 开头):',
|
|
171
|
+
'setup.tunnelConflict': '检测到同名隧道(id {tunnelId}),是否复用?',
|
|
172
|
+
'setup.verifyTitle': '三灯自检:',
|
|
173
|
+
'setup.verifyLight1': '第 1 盏:本地网关就绪 —— {state}',
|
|
174
|
+
'setup.verifyLight2': '第 2 盏:公开地址可达 —— {state}',
|
|
175
|
+
'setup.verifyLight3': '第 3 盏:真实配对完成 —— {state}',
|
|
176
|
+
'setup.verifyFailedOptions': '验证未通过:[1] 重试探测 [2] 保留配置退出(稍后用 host ingress verify 重验)',
|
|
177
|
+
'setup.pairingQr': '用手机浏览器 / 微信扫码,或访问该地址:',
|
|
178
|
+
'setup.pairingCode': '配对码:{code}(手机 / 浏览器 Web 应用端)',
|
|
179
|
+
'setup.desktopPairingCode': '桌面客户端配对码:{code}(另一台电脑 DSH 桌面端 / CLI)',
|
|
180
|
+
'setup.desktopPairingCmd': '另一台电脑终端一键配对:dsh-zen-remote client pair {url} --code {code}',
|
|
181
|
+
'setup.pairingWait': '等待设备完成配对(手机扫码或另一台电脑桌面端连接均可,Ctrl-C 或 --no-wait 可跳过)…',
|
|
182
|
+
'setup.pairingDone': '配对完成,三灯全亮。',
|
|
183
|
+
'setup.pairingSkipped': '已跳过等待;前两灯结果保留,稍后可用 host ingress verify 重验。',
|
|
184
|
+
'setup.roleSwitchTimeout': '等待角色切换生效超时({seconds} 秒):请确认 DSH 正常运行后重试。',
|
|
185
|
+
'setup.ingressRecommend': '不确定选哪个?回答两个问题帮我推荐',
|
|
186
|
+
'setup.recommend.publicIp': '是否有公网 IP(或已在路由器做端口映射)?',
|
|
187
|
+
'setup.recommend.cfDomain': '域名是否托管在 Cloudflare?',
|
|
188
|
+
'setup.recommend.result': '根据回答推荐:{mode}',
|
|
189
|
+
'setup.verifyRetry': '重试验证',
|
|
190
|
+
'setup.verifyKeepExit': '保留配置退出(稍后运行 dsh-zen-remote host ingress verify)',
|
|
191
|
+
'setup.desktopPairQuestion': '是否顺手生成一个供另一台电脑使用的 desktop-client 配对码?',
|
|
192
|
+
'setup.clientRepairQuestion': '选择要重新配对的 host:',
|
|
193
|
+
'setup.summary.title': '配置完成,当前状态:',
|
|
194
|
+
'setup.summary.role': '角色:{role}',
|
|
195
|
+
'setup.summary.ingress': '接入方式:{mode} · {publicUrl}',
|
|
196
|
+
'setup.summary.client': '已配对的 host:{count} / {max} 台',
|
|
197
|
+
'setup.summary.hint': '提示:dsh-zen-remote status 查看状态;host device pair / client pair 管理配对。',
|
|
198
|
+
'err.internal': '命令执行失败:{error}',
|
|
199
|
+
|
|
200
|
+
// ---- client role ---------------------------------------------------------------------
|
|
201
|
+
'client.prompt.hostUrl': '远端 host 地址(如 http://10.0.0.2:3080 或 https://tea.example.com):',
|
|
202
|
+
'client.prompt.code': '8 位桌面配对码:',
|
|
203
|
+
'client.prompt.name': '本机显示名(默认 {default}):',
|
|
204
|
+
'client.error.urlInvalid': '地址格式无效:请输入如 http://10.0.0.2:3080 或 https://tea.example.com 的完整地址。',
|
|
205
|
+
'client.error.insecureHttp': '公网地址必须使用 https://(仅内网 / Tailscale / localhost 允许 http://)。',
|
|
206
|
+
'client.error.codeFormat': '配对码是 8 位字母或数字。',
|
|
207
|
+
'client.error.nameWriteFailed': '本机显示名写入失败。',
|
|
208
|
+
'client.error.badCode': '配对码错误或已过期。',
|
|
209
|
+
'client.error.roleMismatch': '该配对码不是桌面客户端配对码(角色不匹配)。',
|
|
210
|
+
'client.error.locked': '配对码尝试次数过多,已临时锁定,请约 {seconds} 秒后再试。',
|
|
211
|
+
'client.error.unreachable': '连不上该 host:请确认地址、网络与防火墙。',
|
|
212
|
+
'client.error.unexpected': '对端返回了无法识别的应答。',
|
|
213
|
+
'client.pairedWith': '对端 host:{name}',
|
|
214
|
+
'clientList.header': '已配对的 host({count} / {max})',
|
|
215
|
+
'clientList.empty': '还没有配对任何 host。',
|
|
216
|
+
'clientList.fetchFailed': '读取连接状态失败:{error}',
|
|
217
|
+
'clientUnpair.confirmAll': '确定移除全部 {count} 台已配对的 host?',
|
|
218
|
+
'clientUnpair.removedAll': '已移除全部 {count} 台已配对的 host。',
|
|
219
|
+
'clientUnpair.removed': '已移除 {name}。',
|
|
220
|
+
'clientUnpair.notFound': '没有找到 {target} 对应的已配对 host。',
|
|
221
|
+
'clientUnpair.missingTarget': '请指定要移除的 host(地址或条目 id),或用 --all 全部移除。',
|
|
222
|
+
'clientUnpair.selectQuestion': '选择要移除的 host:',
|
|
223
|
+
'clientUnpair.failed': '移除请求失败。',
|
|
224
|
+
'client.menuTitle': '已配对的 host:',
|
|
225
|
+
'client.menuAdd': '新增配对一台 host',
|
|
226
|
+
'client.menuRepair': '重新配对已有 host',
|
|
227
|
+
'client.menuRemove': '移除已配对 host',
|
|
228
|
+
'client.menuDone': '完成退出',
|
|
229
|
+
'client.limitReached': '已达 8 台上限:请先移除一台,或覆盖重配已有地址。',
|
|
230
|
+
'client.claiming': '正在兑换配对码并握手 …',
|
|
231
|
+
'client.claimFailed': '配对失败:{reason}。请重试配对码(已输入的地址保留)。',
|
|
232
|
+
'client.claimOk': '配对成功,等待连接就绪(最多 10 秒)…',
|
|
233
|
+
'client.lightCredentials': '本地配对凭证 ✓ —— {state}',
|
|
234
|
+
'client.lightConnection': 'host 连接 ✓ —— {state}',
|
|
235
|
+
'client.lightSync': '会话与插件同步 ✓ —— {state}',
|
|
236
|
+
'client.addAnother': '是否继续添加下一台 host?',
|
|
237
|
+
|
|
238
|
+
// ---- host role -----------------------------------------------------------------------
|
|
239
|
+
'host.device.pairTitle': 'host 设备配对',
|
|
240
|
+
'host.device.prompt.role': '选择要配对的设备类型:',
|
|
241
|
+
'host.device.role.web': 'web —— 手机 / 浏览器 Web 应用端',
|
|
242
|
+
'host.device.role.desktop': 'desktop-client —— 另一台 DSH 桌面客户端',
|
|
243
|
+
'host.device.error.badRole': '设备类型只能是 web 或 desktop-client:{value} 无效。',
|
|
244
|
+
'host.device.minting': '正在生成本机配对码 …',
|
|
245
|
+
'host.device.pairFailed': '配对码生成失败。',
|
|
246
|
+
'host.device.pairingCode': '配对码:{code}({minutes} 分钟内有效)',
|
|
247
|
+
'host.device.noAddress': '未检测到可公开访问的地址:请在浏览器打开网关地址后输入上述配对码完成配对。',
|
|
248
|
+
'host.device.desktopCommand': '在另一台 DSH 机器的终端运行以下命令完成配对:',
|
|
249
|
+
'host.device.desktopWizard': '或在网页向导的「设备配对」里输入上述配对码。',
|
|
250
|
+
'host.device.listTitle': '已配对设备:',
|
|
251
|
+
'host.device.listEmpty': '还没有已配对的设备。',
|
|
252
|
+
'host.device.listFailed': '读取设备列表失败:网关未在运行。',
|
|
253
|
+
'host.device.deviceLine': ' [{id}] {name}({deviceRole} · {kind})',
|
|
254
|
+
'host.device.deviceTiming': ' 创建于 {created},最后活跃 {lastSeen}',
|
|
255
|
+
'host.device.selectQuestion': '选择要吊销的设备:',
|
|
256
|
+
'host.device.revokeConfirmAll': '确定吊销全部已配对设备?',
|
|
257
|
+
'host.device.revokeMissingTarget': '请指定要吊销的设备 id,或用 --all 吊销全部。',
|
|
258
|
+
'host.device.revoked': '已吊销 {count} 台设备。',
|
|
259
|
+
'host.device.revokedOne': '已吊销设备 {id}。',
|
|
260
|
+
'host.device.revokeFailed': '吊销请求失败。',
|
|
261
|
+
'host.ingress.started': '公网接入已启动({mode})。',
|
|
262
|
+
'host.ingress.stopped': '公网接入已停止。',
|
|
263
|
+
'host.ingress.confirmStop': '确定停止公网接入(托管的 Caddy / Cloudflare Tunnel 将退出)?',
|
|
264
|
+
'host.ingress.stopFailed': '停止请求失败。',
|
|
265
|
+
'host.ingress.error.badMode': '接入方式只能是 caddy、cloudflared、external 或 lan:{value} 无效。',
|
|
266
|
+
'host.ingress.error.badProxy': '反代类型只能是 nginx、caddy 或 lucky:{value} 无效。',
|
|
267
|
+
'host.ingress.error.badDomain': '域名格式无效:请输入裸域名(如 tea.example.com)。',
|
|
268
|
+
'host.ingress.snippetQuestion': '选择要输出的反向代理配置片段:',
|
|
269
|
+
'host.ingress.snippetTitle': '{proxy} 反向代理配置片段(网关端口 {port}):',
|
|
270
|
+
'host.ingress.configWriteFailed': '配置写入失败。',
|
|
271
|
+
'host.ingress.cfTokenFailed': 'Cloudflare API 令牌保存失败。',
|
|
272
|
+
'host.ingress.verifyTitle': '验证公开地址 {publicUrl} …',
|
|
273
|
+
'host.ingress.lightFail': '未通过',
|
|
274
|
+
'host.ingress.verifySkipped': '已跳过手机配对等待,前两盏灯验证结果已保留。',
|
|
275
|
+
'host.ingress.verifyCookieInsecure': '反代未正确传递 X-Forwarded-Proto: https:配对 Cookie 缺少 Secure 标记。请在反代配置补上该头后重新验证。',
|
|
276
|
+
'host.ingress.verifyExpired': '配对码已过期,期间未检测到手机配对:请重新运行 host ingress verify。',
|
|
277
|
+
'host.ingress.verifyStale': '验证期间接入配置发生变化:请重新运行 host ingress verify。',
|
|
278
|
+
'host.ingress.verifyReason.noPublicUrl': '未提供公开地址。',
|
|
279
|
+
'host.ingress.verifyReason.badPublicUrl': '公开地址格式无效。',
|
|
280
|
+
'host.ingress.verifyReason.runtimeNotReady': '托管入口进程未就绪。',
|
|
281
|
+
'host.ingress.verifyReason.gatewayUnreachable': '本地网关不可达。',
|
|
282
|
+
'host.ingress.verifyReason.probeFailed': '公开地址探测未通过:管理面未按预期返回 403(请检查反代是否带 X-Forwarded-* 头)。',
|
|
283
|
+
'host.ingress.verifyReason.pairRefused': '配对码生成被拒({detail})。',
|
|
284
|
+
'host.ingress.verifyReason.generic': '验证失败({reason})。',
|
|
285
|
+
'host.ingress.caddyError.alreadyRunning': 'Caddy 已在运行,无需重复启动。',
|
|
286
|
+
'host.ingress.caddyError.missingToken': '缺少 Cloudflare API 令牌:请用 --cf-token 提供,或先在交互模式下输入。',
|
|
287
|
+
'host.ingress.caddyError.badConfig': 'Caddyfile 生成失败:{detail}',
|
|
288
|
+
'host.ingress.caddyError.downloadFailed': 'Caddy 下载失败:{detail}(手动安装指引:{installUrl})',
|
|
289
|
+
'host.ingress.caddyError.validateFailed': 'Caddy 配置校验失败:{detail}',
|
|
290
|
+
'host.ingress.caddyError.spawnFailed': 'Caddy 进程启动失败:{detail}',
|
|
291
|
+
'host.ingress.caddyError.generic': 'Caddy 启动失败({error}):{detail}',
|
|
292
|
+
'host.ingress.tunnelCheckFailed': '域名验证失败({reason})。',
|
|
293
|
+
'host.ingress.tunnelSetupFailed': '隧道创建失败({reason})。',
|
|
294
|
+
'host.ingress.tunnelConflictNonInteractive': '检测到同名隧道(现有 id:{tunnelId}):非交互模式请用 --tunnel-id {tunnelId} 复用它。',
|
|
295
|
+
'host.ingress.trustProxyQuestion': '检测到来自 {ip} 的未信任反代来源:把它加入信任列表?',
|
|
296
|
+
'host.ingress.trustProxyAuto': '检测到未信任反代来源 {ip},已自动加入信任列表。',
|
|
297
|
+
'host.ingress.trustProxyDone': '已将 {ip} 加入信任列表。',
|
|
298
|
+
'host.ingress.trustProxyFailed': '信任列表更新失败。',
|
|
299
|
+
'host.ingress.lanWaiting': '等待网关在 0.0.0.0 重新就绪(最多 {seconds} 秒)…',
|
|
300
|
+
'host.ingress.lanTimeout': '等待网关重启超时({seconds} 秒):请确认 DSH 正常运行后重试。',
|
|
301
|
+
'setup.divider': '────────────────────────────────────────────────────────────',
|
|
302
|
+
'setup.step1Title': '[步骤 1/3] 选择本机运行角色 (host / client)',
|
|
303
|
+
'setup.step2HostTitle': '[步骤 2/3] 配置 host 接入方式',
|
|
304
|
+
'setup.step2ClientTitle': '[步骤 2/3] 连接并配对远端 host',
|
|
305
|
+
'setup.step3HostTitle': '[步骤 3/3] 连通性验证与设备配对',
|
|
306
|
+
'setup.cfTokenGuide.title': ' Cloudflare API 令牌权限要求(https://dash.cloudflare.com/profile/api-tokens → 创建令牌 →「编辑区域 DNS」模板):',
|
|
307
|
+
'setup.cfTokenGuide.caddy': ' • 区域 (Zone) · DNS · 编辑 (Edit) —— 必选(区域资源选「包括 · 特定区域 · 你的域名」)',
|
|
308
|
+
'setup.cfTokenGuide.tunnel': ' • 区域 (Zone) · DNS · 编辑 (Edit) —— 写入隧道 CNAME 解析记录\n • 区域 (Zone) · 区域 (Zone) · 读取 (Read) —— 定位域名所属 Zone ID\n • 账户 (Account) · Cloudflare Tunnel · 编辑 (Edit) —— 创建与配置隧道\n • 账户 (Account) · 账户设置 (Account Settings) · 读取 (Read) —— 读取账户 ID',
|
|
309
|
+
'host.ingress.lanPickQuestion': '检测到多个内网 / Tailscale 地址,请选择要使用的访问地址:',
|
|
310
|
+
'host.ingress.lanPickCustom': '手动输入其他局域网 / Tailscale 地址',
|
|
311
|
+
'host.ingress.verifyLanProbeWarn': '第 2 盏:公开地址可达 —— 本机自检受限(虚拟网卡 / 路由隔离),已直接生成配对码供外部设备连接验证',
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const EN: Readonly<Record<string, string>> = {
|
|
315
|
+
// ---- help -----------------------------------------------------------------
|
|
316
|
+
'help.title': 'dsh-zen-remote — DeepSeek Harness remote access command line',
|
|
317
|
+
'help.usage': 'Usage: dsh-zen-remote <command> [options]',
|
|
318
|
+
'help.description': 'Configure remote access, pair devices and manage connections without opening the web UI.',
|
|
319
|
+
'help.commands': 'Commands:',
|
|
320
|
+
'help.command.setup': 'setup Interactive setup wizard (role, ingress, pairing)',
|
|
321
|
+
'help.command.status': 'status Show the current role, gateway and connection status',
|
|
322
|
+
'help.command.hostDevice': 'host device pair|list|revoke host: pair / list / revoke access devices',
|
|
323
|
+
'help.command.hostIngress': 'host ingress start|stop|verify|snippet host: manage public ingress',
|
|
324
|
+
'help.command.clientPair': 'client pair <hostUrl> client: pair with a remote host',
|
|
325
|
+
'help.command.clientUnpair': 'client unpair [<hostUrl>|<entryId>|--all] client: remove a paired host',
|
|
326
|
+
'help.command.clientList': 'client list client: list paired hosts',
|
|
327
|
+
'help.options': 'Options:',
|
|
328
|
+
'help.option.lang': '--lang zh|en|auto UI language (default auto)',
|
|
329
|
+
'help.option.json': '--json machine-readable JSON output',
|
|
330
|
+
'help.option.socket': '--socket <path> explicit IPC socket path (default: auto-discover)',
|
|
331
|
+
'help.option.wait': '--wait <seconds> seconds to wait for DSH to start / reload (default 20)',
|
|
332
|
+
'help.option.yes': '--yes, -y non-interactive mode: take defaults, never wait for input',
|
|
333
|
+
'help.option.noWait': '--no-wait skip the final wait (e.g. waiting for the phone to pair)',
|
|
334
|
+
'help.option.debug': 'status --debug [outPath] write the redacted debug report JSON',
|
|
335
|
+
'help.option.role': '--role host|client this machine\'s role (peer is not enabled yet)',
|
|
336
|
+
'help.option.profile': '--profile p1,p2 DSH profiles to install the plugin into',
|
|
337
|
+
'help.option.ingress': '--ingress, --mode caddy|cloudflared|external|lan public ingress mode',
|
|
338
|
+
'help.option.domain': '--domain <host> domain name',
|
|
339
|
+
'help.option.cfToken': '--cf-token <token> Cloudflare API token',
|
|
340
|
+
'help.option.email': '--email <email> certificate notification email (optional)',
|
|
341
|
+
'help.option.httpsPort': '--https-port <port> public HTTPS port (default 443)',
|
|
342
|
+
'help.option.publicUrl': '--public-url <origin> public origin (starts with https://)',
|
|
343
|
+
'help.option.tunnelId': '--tunnel-id <id> reuse an existing Cloudflare Tunnel ID',
|
|
344
|
+
'help.option.proxy': '--proxy nginx|caddy|lucky reverse-proxy flavor for external ingress',
|
|
345
|
+
'help.option.deviceRole': '--device-role web|desktop-client the kind of device to pair',
|
|
346
|
+
'help.option.hostUrl': '--host-url <url> remote host address',
|
|
347
|
+
'help.option.code': '--code <8 digits> pairing code',
|
|
348
|
+
'help.option.name': '--name <name> this machine\'s display name',
|
|
349
|
+
'help.option.all': '--all apply to everything (devices / host connections)',
|
|
350
|
+
'help.footer': 'Hint: running without a command shows this help; see docs/remote-access.md.',
|
|
351
|
+
|
|
352
|
+
// ---- common -----------------------------------------------------------------
|
|
353
|
+
'common.yes': 'yes',
|
|
354
|
+
'common.no': 'no',
|
|
355
|
+
'common.done': 'done',
|
|
356
|
+
'common.cancel': 'cancel',
|
|
357
|
+
'common.retry': 'retry',
|
|
358
|
+
'common.back': 'back',
|
|
359
|
+
|
|
360
|
+
// ---- errors & gates -----------------------------------------------------------
|
|
361
|
+
'err.unknownCommand': 'Unknown command: {command}. Run dsh-zen-remote --help for the command list.',
|
|
362
|
+
'err.role.peerNotEnabled': 'The peer role is not enabled yet: the CLI has no peer entry point — use host or client.',
|
|
363
|
+
'err.role.invalid': 'Unknown role: {value} (host or client; peer is not enabled yet).',
|
|
364
|
+
'err.roleGate.hostRequired': 'This command requires the host role; the current role is {role}. Switch roles with setup first.',
|
|
365
|
+
'err.roleGate.clientRequired': 'This command requires the client role; the current role is {role}. Switch roles with setup first.',
|
|
366
|
+
'err.roleGate.clientCommands': 'The current role is {role}, which does not connect to a remote host (run dsh-zen-remote setup --role client to switch).',
|
|
367
|
+
'err.envShadowed': 'The field {field} is shadowed by the environment variable {envVar}: change that variable or your launch script first.',
|
|
368
|
+
'err.crossUserSocket': 'DSH appears to be running as another system user, so its control socket is not accessible: run this command as the same user, or retry with sudo -E dsh-zen-remote …',
|
|
369
|
+
'err.missingOption': 'Missing required option --{option}: a non-interactive run never waits for input — pass the option or run interactively in a terminal.',
|
|
370
|
+
'err.desktopProfile': 'The desktop profile is managed by the DeepSeek Harness app and cannot receive plugins from the CLI: {profiles}.',
|
|
371
|
+
'err.protocolVersion': 'The local DSH plugin speaks CLI protocol {found} but this CLI speaks {expected}: upgrade the dsh-zen-remote plugin or the CLI.',
|
|
372
|
+
'err.notRunning': 'No running DSH plugin found: start DSH first (try dsh --profile {profile} web), then run this command again.',
|
|
373
|
+
|
|
374
|
+
// ---- profile install -----------------------------------------------------------
|
|
375
|
+
'install.scanning': 'Scanning candidate profiles under <DSH_HOME>/profiles …',
|
|
376
|
+
'install.installed': 'Profiles with the plugin installed: {profiles}',
|
|
377
|
+
'install.noneInstalled': 'dsh-zen-remote is not installed in any profile yet.',
|
|
378
|
+
'install.selectQuestion': 'Select the profiles to install dsh-zen-remote into',
|
|
379
|
+
'install.customEntry': 'Type another profile name manually',
|
|
380
|
+
'install.running': 'Installing: dsh plugin --profile {profile} add dsh-zen-remote …',
|
|
381
|
+
'install.failed': 'Installing into {profile} failed (exit code {code}). Run it manually: {command}',
|
|
382
|
+
'install.done': 'Installed into: {profiles}',
|
|
383
|
+
|
|
384
|
+
// ---- daemon launch --------------------------------------------------------------
|
|
385
|
+
'launch.found': 'Found a running DSH (role {role}, {socketPath}).',
|
|
386
|
+
'launch.probing': 'No running DSH found — trying the system daemon or app …',
|
|
387
|
+
'launch.linuxUserUnit': 'Found the systemd user unit {unit} — starting it …',
|
|
388
|
+
'launch.linuxSystemUnit': 'Found the system-level systemd unit {unit} — starting it …',
|
|
389
|
+
'launch.macosService': 'Found the launchd service ai.deepseek.dsh.web — kickstarting it …',
|
|
390
|
+
'launch.macosApp': 'Launching the DeepSeek Harness app …',
|
|
391
|
+
'launch.waiting': 'Waiting for DSH to come up (up to {seconds}s) …',
|
|
392
|
+
'launch.timeout': 'Timed out waiting for DSH ({seconds}s). Make sure DSH started, then retry.',
|
|
393
|
+
'launch.manual': 'Could not launch DSH automatically. Start it manually, e.g.:\n dsh --profile {profile} web\nor create a systemd user unit (~/.config/systemd/user/dsh-web.service):\n [Unit]\n Description=DeepSeek Harness\n [Service]\n ExecStart=dsh --profile {profile} web\n [Install]\n WantedBy=default.target\n(On macOS simply open the DeepSeek Harness app; on Windows run the dsh command in a terminal.)',
|
|
394
|
+
|
|
395
|
+
// ---- prompts -----------------------------------------------------------------
|
|
396
|
+
'prompt.defaultLabel': 'default',
|
|
397
|
+
'prompt.selectOne': 'Pick 1-{max} (Enter = {default}): ',
|
|
398
|
+
'prompt.multiInputPlain': 'Pick numbers (comma/space separated; Enter accepts the default): ',
|
|
399
|
+
'prompt.multiInputCustom': 'Pick numbers (comma/space separated; Enter accepts the default; a to add a name): ',
|
|
400
|
+
'prompt.customEntryLine': ' a. {entry}',
|
|
401
|
+
'prompt.customName': 'Profile name (Enter to finish): ',
|
|
402
|
+
'prompt.invalid': 'Invalid input: {value}',
|
|
403
|
+
'prompt.requiredEmpty': 'This one is required and cannot be empty.',
|
|
404
|
+
'prompt.confirmY': '[Y/n]',
|
|
405
|
+
'prompt.confirmN': '[y/N]',
|
|
406
|
+
|
|
407
|
+
// ---- status ----------------------------------------------------------------------
|
|
408
|
+
'status.role.host': 'host (serves other machines: phones / computers connect in)',
|
|
409
|
+
'status.role.client': 'client (connects out to a remote host)',
|
|
410
|
+
'status.role.peer': 'peer (manual config, not enabled)',
|
|
411
|
+
'status.line.role': 'Current role: {role}',
|
|
412
|
+
'status.machineName': 'Machine name: {name}',
|
|
413
|
+
'status.pluginVersion': 'Plugin version: {version}',
|
|
414
|
+
'status.host.title': 'host status:',
|
|
415
|
+
'status.host.gatewayUp': 'gateway listening (port {port})',
|
|
416
|
+
'status.host.gatewayUpNoPort': 'gateway listening',
|
|
417
|
+
'status.host.gatewayDown': 'gateway not listening',
|
|
418
|
+
'status.host.ingress': 'Ingress mode: {mode}',
|
|
419
|
+
'status.host.publicUrl': 'Public address: {publicUrl}',
|
|
420
|
+
'status.host.processCaddy': 'Caddy process: {state}',
|
|
421
|
+
'status.host.processTunnel': 'Cloudflare Tunnel process: {state}',
|
|
422
|
+
'status.process.running': 'running',
|
|
423
|
+
'status.process.stopped': 'not running',
|
|
424
|
+
'status.host.devices': 'Paired devices: {count}',
|
|
425
|
+
'status.host.deviceLine': ' · {name} ({deviceRole})',
|
|
426
|
+
'status.host.sharedSessions': 'Shared sessions: {count}',
|
|
427
|
+
'status.client.title': 'client status:',
|
|
428
|
+
'status.client.pairedCount': 'Paired hosts: {count} / {max}',
|
|
429
|
+
'status.state.revoked': 'revoked',
|
|
430
|
+
'status.state.incompatible': 'incompatible',
|
|
431
|
+
'status.client.compatOk': 'interface fingerprints identical',
|
|
432
|
+
'status.client.compatDiff': '{count} interface fingerprint difference(s)',
|
|
433
|
+
'status.client.compatUnavailable': 'interface fingerprints unavailable',
|
|
434
|
+
'status.client.bridgeCount': '{count} plugin(s) bridged',
|
|
435
|
+
'status.client.lastError': 'last error: {error}',
|
|
436
|
+
'status.debugFailed': 'Fetching the debug report failed: {error}',
|
|
437
|
+
'status.debugWriteFailed': 'Writing the debug report failed: {error}',
|
|
438
|
+
'status.notRunning': 'No running DSH plugin found.',
|
|
439
|
+
'status.paired': '{count} paired',
|
|
440
|
+
'status.unpaired': 'unpaired',
|
|
441
|
+
'status.online': 'online',
|
|
442
|
+
'status.offline': 'offline',
|
|
443
|
+
'status.connecting': 'connecting',
|
|
444
|
+
'status.profile': 'profile: {profile}',
|
|
445
|
+
'status.debugWritten': 'Debug report written: {path}',
|
|
446
|
+
|
|
447
|
+
// ---- setup wizard ------------------------------------------------------------------
|
|
448
|
+
'setup.roleQuestion': 'Choose this machine\'s role:',
|
|
449
|
+
'setup.roleHost': 'host — serve other machines (phones / computers connect in)',
|
|
450
|
+
'setup.roleClient': 'client — connect out to a remote host',
|
|
451
|
+
'setup.switchingToClient': 'Switching to client: stopping the managed Caddy / Cloudflare tunnel first, then writing the config …',
|
|
452
|
+
'setup.switchingToHost': 'Switching to host: writing the config and waiting for the gateway …',
|
|
453
|
+
'setup.ingressQuestion': 'Choose the public ingress mode:',
|
|
454
|
+
'setup.ingressKeep': 'Keep the current ingress settings — go straight to verify / pairing',
|
|
455
|
+
'setup.ingressCaddy': 'caddy — bundled Caddy with automatic certificates',
|
|
456
|
+
'setup.ingressCloudflared': 'cloudflared — Cloudflare Tunnel',
|
|
457
|
+
'setup.ingressExternal': 'external — an existing reverse proxy (Nginx / Caddy / Lucky)',
|
|
458
|
+
'setup.ingressLan': 'lan — LAN / Tailscale direct',
|
|
459
|
+
'setup.domainQuestion': 'Domain name: ',
|
|
460
|
+
'setup.cfTokenQuestion': 'Cloudflare API token: ',
|
|
461
|
+
'setup.emailQuestion': 'Certificate notification email (optional): ',
|
|
462
|
+
'setup.httpsPortQuestion': 'Public HTTPS port (default 443): ',
|
|
463
|
+
'setup.publicUrlQuestion': 'Public origin (starts with https://): ',
|
|
464
|
+
'setup.tunnelConflict': 'A tunnel with this name already exists (id {tunnelId}) — reuse it?',
|
|
465
|
+
'setup.verifyTitle': 'Three-light check:',
|
|
466
|
+
'setup.verifyLight1': 'Light 1: local gateway ready — {state}',
|
|
467
|
+
'setup.verifyLight2': 'Light 2: public address reachable — {state}',
|
|
468
|
+
'setup.verifyLight3': 'Light 3: a real pairing completed — {state}',
|
|
469
|
+
'setup.verifyFailedOptions': 'Verification failed: [1] probe again [2] keep the config and exit (re-verify later with host ingress verify)',
|
|
470
|
+
'setup.pairingQr': 'Scan with the phone\'s browser, or open this address:',
|
|
471
|
+
'setup.pairingCode': 'Pairing code: {code} (phone / browser Web app)',
|
|
472
|
+
'setup.desktopPairingCode': 'Desktop client pairing code: {code} (another computer\'s DSH app / CLI)',
|
|
473
|
+
'setup.desktopPairingCmd': 'Command on the other computer: dsh-zen-remote client pair {url} --code {code}',
|
|
474
|
+
'setup.pairingWait': 'Waiting for a device to pair (phone or desktop client; Ctrl-C or --no-wait to skip) …',
|
|
475
|
+
'setup.pairingDone': 'Paired — all three lights are on.',
|
|
476
|
+
'setup.pairingSkipped': 'Wait skipped; the first two lights\' results are kept — re-verify later with host ingress verify.',
|
|
477
|
+
'setup.roleSwitchTimeout': 'Timed out waiting for the role switch to take effect ({seconds}s): make sure DSH is running, then retry.',
|
|
478
|
+
'setup.ingressRecommend': 'Not sure? Answer two questions and get a recommendation',
|
|
479
|
+
'setup.recommend.publicIp': 'Do you have a public IP (or a router port forward)?',
|
|
480
|
+
'setup.recommend.cfDomain': 'Is the domain hosted on Cloudflare?',
|
|
481
|
+
'setup.recommend.result': 'Recommended from your answers: {mode}',
|
|
482
|
+
'setup.verifyRetry': 'Retry the verification',
|
|
483
|
+
'setup.verifyKeepExit': 'Keep the config and exit (re-verify later with dsh-zen-remote host ingress verify)',
|
|
484
|
+
'setup.desktopPairQuestion': 'Also mint a desktop-client pairing code for another computer?',
|
|
485
|
+
'setup.clientRepairQuestion': 'Pick the host to re-pair:',
|
|
486
|
+
'setup.summary.title': 'Setup finished. Current state:',
|
|
487
|
+
'setup.summary.role': 'Role: {role}',
|
|
488
|
+
'setup.summary.ingress': 'Ingress: {mode} · {publicUrl}',
|
|
489
|
+
'setup.summary.client': 'Paired hosts: {count} / {max}',
|
|
490
|
+
'setup.summary.hint': 'Hint: run dsh-zen-remote status to inspect; pair with host device pair / client pair.',
|
|
491
|
+
'err.internal': 'The command failed: {error}',
|
|
492
|
+
|
|
493
|
+
// ---- client role ---------------------------------------------------------------------
|
|
494
|
+
'client.prompt.hostUrl': 'Remote host address (e.g. http://10.0.0.2:3080 or https://tea.example.com): ',
|
|
495
|
+
'client.prompt.code': 'The 8-digit desktop pairing code: ',
|
|
496
|
+
'client.prompt.name': 'This machine\'s display name (default {default}): ',
|
|
497
|
+
'client.error.urlInvalid': 'That address does not parse: enter a full origin like http://10.0.0.2:3080 or https://tea.example.com.',
|
|
498
|
+
'client.error.insecureHttp': 'Public addresses must use https:// (plain http is only allowed on the LAN / Tailscale / localhost).',
|
|
499
|
+
'client.error.codeFormat': 'The pairing code is 8 letters/digits.',
|
|
500
|
+
'client.error.nameWriteFailed': 'Writing this machine\'s display name failed.',
|
|
501
|
+
'client.error.badCode': 'The pairing code is wrong or expired.',
|
|
502
|
+
'client.error.roleMismatch': 'That code is not a desktop-client pairing code (role mismatch).',
|
|
503
|
+
'client.error.locked': 'Too many pairing attempts — temporarily locked; retry in about {seconds}s.',
|
|
504
|
+
'client.error.unreachable': 'The host is unreachable: check the address, network and firewall.',
|
|
505
|
+
'client.error.unexpected': 'The host answered with an unrecognized response.',
|
|
506
|
+
'client.pairedWith': 'Connected host: {name}',
|
|
507
|
+
'clientList.header': 'Paired hosts ({count} / {max})',
|
|
508
|
+
'clientList.empty': 'No host is paired yet.',
|
|
509
|
+
'clientList.fetchFailed': 'Reading the connection status failed: {error}',
|
|
510
|
+
'clientUnpair.confirmAll': 'Remove all {count} paired host(s)?',
|
|
511
|
+
'clientUnpair.removedAll': 'Removed all {count} paired host(s).',
|
|
512
|
+
'clientUnpair.removed': 'Removed {name}.',
|
|
513
|
+
'clientUnpair.notFound': 'No paired host matches {target}.',
|
|
514
|
+
'clientUnpair.missingTarget': 'Name the host to remove (its address or entry id), or pass --all to remove everything.',
|
|
515
|
+
'clientUnpair.selectQuestion': 'Pick the host to remove:',
|
|
516
|
+
'clientUnpair.failed': 'The removal request failed.',
|
|
517
|
+
'client.menuTitle': 'Paired hosts:',
|
|
518
|
+
'client.menuAdd': 'Pair a new host',
|
|
519
|
+
'client.menuRepair': 'Re-pair an existing host',
|
|
520
|
+
'client.menuRemove': 'Remove a paired host',
|
|
521
|
+
'client.menuDone': 'Done — exit',
|
|
522
|
+
'client.limitReached': 'The 8-host limit is reached: remove one first, or re-pair over an existing address.',
|
|
523
|
+
'client.claiming': 'Redeeming the pairing code and shaking hands …',
|
|
524
|
+
'client.claimFailed': 'Pairing failed: {reason}. Retry the code (the address you typed is kept).',
|
|
525
|
+
'client.claimOk': 'Paired — waiting for the connection to settle (up to 10s) …',
|
|
526
|
+
'client.lightCredentials': 'Local pairing credential ✓ — {state}',
|
|
527
|
+
'client.lightConnection': 'host connection ✓ — {state}',
|
|
528
|
+
'client.lightSync': 'Sessions and plugin sync ✓ — {state}',
|
|
529
|
+
'client.addAnother': 'Add another host?',
|
|
530
|
+
|
|
531
|
+
// ---- host role -----------------------------------------------------------------------
|
|
532
|
+
'host.device.pairTitle': 'host device pairing',
|
|
533
|
+
'host.device.prompt.role': 'Choose the kind of device to pair:',
|
|
534
|
+
'host.device.role.web': 'web — a phone / browser web app',
|
|
535
|
+
'host.device.role.desktop': 'desktop-client — another DSH desktop client',
|
|
536
|
+
'host.device.error.badRole': 'The device role is web or desktop-client only: {value} is not valid.',
|
|
537
|
+
'host.device.minting': 'Minting a pairing code …',
|
|
538
|
+
'host.device.pairFailed': 'Minting the pairing code failed.',
|
|
539
|
+
'host.device.pairingCode': 'Pairing code: {code} (valid for {minutes} minutes)',
|
|
540
|
+
'host.device.noAddress': 'No publicly reachable address detected: open the gateway address in a browser and enter the pairing code above.',
|
|
541
|
+
'host.device.desktopCommand': 'Run this command in a terminal on the other DSH machine:',
|
|
542
|
+
'host.device.desktopWizard': 'Or enter the pairing code above in the web wizard\'s device-pairing card.',
|
|
543
|
+
'host.device.listTitle': 'Paired devices:',
|
|
544
|
+
'host.device.listEmpty': 'No device is paired yet.',
|
|
545
|
+
'host.device.listFailed': 'Reading the device list failed: the gateway is not running.',
|
|
546
|
+
'host.device.deviceLine': ' [{id}] {name} ({deviceRole} · {kind})',
|
|
547
|
+
'host.device.deviceTiming': ' created {created}, last seen {lastSeen}',
|
|
548
|
+
'host.device.selectQuestion': 'Pick the device to revoke:',
|
|
549
|
+
'host.device.revokeConfirmAll': 'Revoke ALL paired devices?',
|
|
550
|
+
'host.device.revokeMissingTarget': 'Name the device id to revoke, or pass --all to revoke everything.',
|
|
551
|
+
'host.device.revoked': 'Revoked {count} device(s).',
|
|
552
|
+
'host.device.revokedOne': 'Revoked device {id}.',
|
|
553
|
+
'host.device.revokeFailed': 'The revocation request failed.',
|
|
554
|
+
'host.ingress.started': 'Public ingress started ({mode}).',
|
|
555
|
+
'host.ingress.stopped': 'Public ingress stopped.',
|
|
556
|
+
'host.ingress.confirmStop': 'Stop public ingress (the managed Caddy / Cloudflare tunnel will exit)?',
|
|
557
|
+
'host.ingress.stopFailed': 'The stop request failed.',
|
|
558
|
+
'host.ingress.error.badMode': 'The ingress mode is caddy, cloudflared, external or lan only: {value} is not valid.',
|
|
559
|
+
'host.ingress.error.badProxy': 'The proxy flavor is nginx, caddy or lucky only: {value} is not valid.',
|
|
560
|
+
'host.ingress.error.badDomain': 'That domain does not parse: enter a bare hostname (e.g. tea.example.com).',
|
|
561
|
+
'host.ingress.snippetQuestion': 'Pick the reverse-proxy snippet to print:',
|
|
562
|
+
'host.ingress.snippetTitle': '{proxy} reverse-proxy snippet (gateway port {port}):',
|
|
563
|
+
'host.ingress.configWriteFailed': 'Writing the config failed.',
|
|
564
|
+
'host.ingress.cfTokenFailed': 'Saving the Cloudflare API token failed.',
|
|
565
|
+
'host.ingress.verifyTitle': 'Verifying the public address {publicUrl} …',
|
|
566
|
+
'host.ingress.lightFail': 'failed',
|
|
567
|
+
'host.ingress.verifySkipped': 'Skipped the pairing wait — the first two lights\' results are kept.',
|
|
568
|
+
'host.ingress.verifyCookieInsecure': 'The reverse proxy does not pass X-Forwarded-Proto: https — the pairing cookie lacks the Secure flag. Fix the proxy header and verify again.',
|
|
569
|
+
'host.ingress.verifyExpired': 'The pairing code expired with no phone pairing detected — run host ingress verify again.',
|
|
570
|
+
'host.ingress.verifyStale': 'The ingress config changed during verification — run host ingress verify again.',
|
|
571
|
+
'host.ingress.verifyReason.noPublicUrl': 'No public address was provided.',
|
|
572
|
+
'host.ingress.verifyReason.badPublicUrl': 'The public address does not parse.',
|
|
573
|
+
'host.ingress.verifyReason.runtimeNotReady': 'The managed ingress process is not ready.',
|
|
574
|
+
'host.ingress.verifyReason.gatewayUnreachable': 'The local gateway is unreachable.',
|
|
575
|
+
'host.ingress.verifyReason.probeFailed': 'The public-address probe failed: the admin surface did not answer 403 as expected (check the proxy\'s X-Forwarded-* headers).',
|
|
576
|
+
'host.ingress.verifyReason.pairRefused': 'Minting the pairing code was refused ({detail}).',
|
|
577
|
+
'host.ingress.verifyReason.generic': 'Verification failed ({reason}).',
|
|
578
|
+
'host.ingress.caddyError.alreadyRunning': 'Caddy is already running.',
|
|
579
|
+
'host.ingress.caddyError.missingToken': 'The Cloudflare API token is missing: pass --cf-token, or enter it in an interactive run first.',
|
|
580
|
+
'host.ingress.caddyError.badConfig': 'Generating the Caddyfile failed: {detail}',
|
|
581
|
+
'host.ingress.caddyError.downloadFailed': 'Downloading Caddy failed: {detail} (manual install: {installUrl})',
|
|
582
|
+
'host.ingress.caddyError.validateFailed': 'Validating the Caddyfile failed: {detail}',
|
|
583
|
+
'host.ingress.caddyError.spawnFailed': 'Starting the Caddy process failed: {detail}',
|
|
584
|
+
'host.ingress.caddyError.generic': 'Starting Caddy failed ({error}): {detail}',
|
|
585
|
+
'host.ingress.tunnelCheckFailed': 'The domain check failed ({reason}).',
|
|
586
|
+
'host.ingress.tunnelSetupFailed': 'Setting up the tunnel failed ({reason}).',
|
|
587
|
+
'host.ingress.tunnelConflictNonInteractive': 'A tunnel with this name already exists (id {tunnelId}): pass --tunnel-id {tunnelId} in a non-interactive run.',
|
|
588
|
+
'host.ingress.trustProxyQuestion': 'An untrusted reverse proxy from {ip} was detected: add it to the trust list?',
|
|
589
|
+
'host.ingress.trustProxyAuto': 'An untrusted reverse proxy from {ip} was detected and added to the trust list automatically.',
|
|
590
|
+
'host.ingress.trustProxyDone': 'Added {ip} to the trust list.',
|
|
591
|
+
'host.ingress.trustProxyFailed': 'Updating the trust list failed.',
|
|
592
|
+
'host.ingress.lanWaiting': 'Waiting for the gateway to come back on 0.0.0.0 (up to {seconds}s) …',
|
|
593
|
+
'host.ingress.lanTimeout': 'Timed out waiting for the gateway restart ({seconds}s): make sure DSH is running, then retry.',
|
|
594
|
+
'setup.divider': '────────────────────────────────────────────────────────────',
|
|
595
|
+
'setup.step1Title': '[Step 1/3] Choose this machine\'s role (host / client)',
|
|
596
|
+
'setup.step2HostTitle': '[Step 2/3] Configure host ingress',
|
|
597
|
+
'setup.step2ClientTitle': '[Step 2/3] Pair with a remote host',
|
|
598
|
+
'setup.step3HostTitle': '[Step 3/3] Connectivity verification & device pairing',
|
|
599
|
+
'setup.cfTokenGuide.title': ' Cloudflare API token permissions (https://dash.cloudflare.com/profile/api-tokens → Create Token → "Edit zone DNS" template):',
|
|
600
|
+
'setup.cfTokenGuide.caddy': ' • Zone · DNS · Edit — required (Zone Resources: Include · Specific zone · your domain)',
|
|
601
|
+
'setup.cfTokenGuide.tunnel': ' • Zone · DNS · Edit — write the tunnel CNAME record\n • Zone · Zone · Read — locate the domain Zone ID\n • Account · Cloudflare Tunnel · Edit — create and configure the tunnel\n • Account · Account Settings · Read — read the Account ID',
|
|
602
|
+
'host.ingress.lanPickQuestion': 'Multiple LAN / Tailscale addresses detected — pick the address to use:',
|
|
603
|
+
'host.ingress.lanPickCustom': 'Enter another LAN / Tailscale address manually',
|
|
604
|
+
'host.ingress.verifyLanProbeWarn': 'Light 2: public address reachable — local self-probe skipped (virtual interface / routing isolation); pairing code minted for direct device verification',
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const DICTIONARIES: Readonly<Record<CliLang, Readonly<Record<string, string>>>> = { zh: ZH, en: EN }
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Resolve the CLI language: `--lang` wins (`zh` / `en`; `auto` and unknown
|
|
611
|
+
* values fall through), then `ZEN_REMOTE_LANG`, then the locale variables in
|
|
612
|
+
* the standard `LC_ALL` > `LC_MESSAGES` > `LANG` order — anything matching
|
|
613
|
+
* `/^zh/i` is Chinese, everything else (including `C`, `POSIX`, empty) is
|
|
614
|
+
* English.
|
|
615
|
+
*/
|
|
616
|
+
export function resolveCliLang(flagLang: string | undefined, env?: NodeJS.ProcessEnv): CliLang {
|
|
617
|
+
const flag = flagLang?.trim().toLowerCase()
|
|
618
|
+
if (flag === 'zh') return 'zh'
|
|
619
|
+
if (flag === 'en') return 'en'
|
|
620
|
+
const vars = env ?? process.env
|
|
621
|
+
const zen = vars.ZEN_REMOTE_LANG?.trim().toLowerCase()
|
|
622
|
+
if (zen === 'zh') return 'zh'
|
|
623
|
+
if (zen === 'en') return 'en'
|
|
624
|
+
const locale = (vars.LC_ALL || vars.LC_MESSAGES || vars.LANG || '').trim()
|
|
625
|
+
return /^zh/i.test(locale) ? 'zh' : 'en'
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Build a translator for `lang`. Missing keys fall back to the English table
|
|
630
|
+
* and then to the key itself — a forgotten key must be VISIBLE in the output,
|
|
631
|
+
* never silently empty.
|
|
632
|
+
*/
|
|
633
|
+
export function createTranslator(lang: CliLang): CliTranslator {
|
|
634
|
+
const table = DICTIONARIES[lang]
|
|
635
|
+
const translate = (key: string, params?: Record<string, string | number>): string => {
|
|
636
|
+
let text = table[key] ?? DICTIONARIES.en[key] ?? key
|
|
637
|
+
if (params !== undefined) {
|
|
638
|
+
for (const [name, value] of Object.entries(params)) {
|
|
639
|
+
text = text.replaceAll(`{${name}}`, String(value))
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return text
|
|
643
|
+
}
|
|
644
|
+
return Object.assign(translate, { keys: table })
|
|
645
|
+
}
|