handmux 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/bin/handmux.js +58 -6
- package/package.json +22 -4
- package/public/assets/index-9fmzTpZ-.js +159 -0
- package/public/assets/index-CJZ28YE2.css +32 -0
- package/public/index.html +2 -2
- package/src/cli/cloudflared.js +2 -2
- package/src/cli/cpolarUrl.js +23 -0
- package/src/cli/drivers.js +38 -0
- package/src/cli/i18n/en.js +132 -34
- package/src/cli/i18n/zh.js +132 -34
- package/src/cli/natappUrl.js +11 -0
- package/src/cli/options.js +23 -1
- package/src/cli/prompt.js +17 -0
- package/src/cli/setupWizard.js +420 -90
- package/src/cli/tunnelClients.js +62 -0
- package/src/cli/updateCheck.js +37 -18
- package/src/cli/urlHost.js +12 -0
- package/src/docs.js +9 -3
- package/src/httpApi.js +20 -3
- package/public/assets/index-CrMq7ZZv.js +0 -159
- package/public/assets/index-y69e8WoM.css +0 -32
package/src/cli/i18n/zh.js
CHANGED
|
@@ -20,6 +20,8 @@ export default {
|
|
|
20
20
|
'tmux.explain2': ' 所以这台机器上必须先装好 tmux。',
|
|
21
21
|
'tmux.install': ' 安装: {hint}',
|
|
22
22
|
'tmux.thenStart': ' 然后再运行 `handmux start`。',
|
|
23
|
+
'open.usage': '用法: handmux open <会话名> 接入该 tmux 会话(不存在则新建)',
|
|
24
|
+
'open.insideTmux': '你已经在 tmux 里了——切换会话请用 tmux 自己的方式(如 `tmux switch-client -t <会话名>`),不要嵌套。',
|
|
23
25
|
'tmux.tooOld': '⚠ tmux {raw} 低于测试过的最低版本 {min};终端渲染可能有偏差',
|
|
24
26
|
|
|
25
27
|
// start —— 已在运行
|
|
@@ -54,6 +56,12 @@ export default {
|
|
|
54
56
|
// cloudflared 自动下载
|
|
55
57
|
'cf.downloading': ' ↓ 正在下载 cloudflared({file})…',
|
|
56
58
|
|
|
59
|
+
// natapp / cpolar 客户端二进制
|
|
60
|
+
'client.downloading': ' ↓ 正在下载 {name}({file})…',
|
|
61
|
+
'client.cpolarManual': '✗ cpolar 不可用 —— 到 https://www.cpolar.com/download 下载,解压出二进制放进 ~/.handmux/bin/ 后重试。',
|
|
62
|
+
'client.cpolarAuthFail': '✗ cpolar 拒绝了该 authtoken —— 到 https://dashboard.cpolar.com 核对一下。',
|
|
63
|
+
'client.natappManual': '✗ 未找到 natapp —— 到 https://natapp.cn 下载(需先登录,非公开下载),把 `natapp` 二进制放到 {dir}(或 PATH 上任意位置)后重试。',
|
|
64
|
+
|
|
57
65
|
// 访问信息块(printAccess)
|
|
58
66
|
'access.noState': ' (无状态)',
|
|
59
67
|
'access.error': ' ✗ {msg}',
|
|
@@ -97,8 +105,9 @@ export default {
|
|
|
97
105
|
// setup 向导
|
|
98
106
|
'setup.confirmStart': '现在启动 handmux 吗?',
|
|
99
107
|
'setup.later': "准备好后运行 'handmux start' 即可。",
|
|
108
|
+
'setup.laterRestart': "已在运行 —— 运行 'handmux restart' 让改动生效。",
|
|
100
109
|
'setup.needTty': 'handmux setup 需要一个交互式终端',
|
|
101
|
-
'setup.langQ': '
|
|
110
|
+
'setup.langQ': '命令行语言(handmux 终端输出)/ Command-line language',
|
|
102
111
|
'setup.lang1': ' 1) English',
|
|
103
112
|
'setup.lang2': ' 2) 中文',
|
|
104
113
|
'setup.askName': '应用名称(显示在浏览器标签 / 主屏图标;留空 = 默认)',
|
|
@@ -107,21 +116,97 @@ export default {
|
|
|
107
116
|
'setup.tunnel2': ' 2) cloudflare —— 即时、随机的临时 https 地址',
|
|
108
117
|
'setup.tunnel3': ' 3) cloudflare-named —— 用你自己的域名,稳定 HTTPS(最省心)',
|
|
109
118
|
'setup.tunnel4': ' 4) ssh (tunlite) —— 你自己的服务器 / 边缘',
|
|
110
|
-
'setup.
|
|
119
|
+
'setup.tunnel5': ' 5) natapp —— 国内可用的隧道(需要 natapp authtoken)',
|
|
120
|
+
'setup.tunnel6': ' 6) cpolar —— 国内可用的隧道(自动安装;需要 cpolar authtoken)',
|
|
121
|
+
'setup.choose': '选择 1-6',
|
|
111
122
|
'setup.invalid': '无效选择',
|
|
112
123
|
'setup.askPort': '服务端口',
|
|
113
124
|
'setup.askHostname': '公网域名(例如 handmux.example.com)',
|
|
114
125
|
'setup.askTunnelName': '隧道名称',
|
|
115
126
|
'setup.askSshHost': 'ssh 主机(user@host[:port])',
|
|
116
127
|
'setup.askRemotePort': 'ssh 主机上的远程端口',
|
|
117
|
-
'setup.askPublicUrl': '
|
|
128
|
+
'setup.askPublicUrl': '公网地址,http(s):// 按情况填(留空 = http://host:remotePort)',
|
|
129
|
+
'setup.natappGuide': '去哪拿 authtoken:到 https://natapp.cn 免费注册 → 新建一条隧道 → 复制它的 authtoken(免费额度够上手)。',
|
|
130
|
+
'setup.cpolarGuide': '去哪拿 authtoken:到 https://cpolar.com 免费注册 → 打开后台 → 验证/Verify → 复制你的 authtoken。',
|
|
131
|
+
'setup.askAuthtoken': 'authtoken',
|
|
132
|
+
'setup.askFixed': '用固定域名吗?(否 = 免费临时域名,每次重启都会变)',
|
|
133
|
+
'setup.askNatappDomain': '你在 natapp 后台为该 authtoken 绑定的固定域名(例如 myapp.natapp1.cc)',
|
|
134
|
+
'setup.askCpolarDomain': '你保留的二级域名或绑定的自有域名(例如 myapp.cpolar.top)',
|
|
135
|
+
'setup.askCpolarRegion': 'cpolar 区域(cn = 中国大陆,国内更快;留空 = cpolar 默认)',
|
|
136
|
+
'setup.natappReady': '✓ natapp 客户端就绪',
|
|
137
|
+
'setup.cpolarReady': '✓ cpolar 客户端就绪',
|
|
138
|
+
// setup 中枢(菜单模型)
|
|
139
|
+
'setup.welcome': 'handmux 开源、自身不设中转——手机连的始终是你自己的电脑,更私密安全;同一 Wi-Fi 下就是纯直连、开箱即用(选「直连」即可)。想在外网也能连,下面几个是内置帮你打通的方式,用你自己的免费账号即可。不确定就在高亮项上按 Enter,之后随时能改。',
|
|
140
|
+
'setup.hubTitle': '要改什么吗?(或直接保存并启动)',
|
|
141
|
+
'setup.secConnection': '连接',
|
|
142
|
+
'setup.secName': '名称',
|
|
143
|
+
'setup.secPort': '端口',
|
|
144
|
+
'setup.secLanguage': '命令行语言 / CLI language',
|
|
145
|
+
'setup.secPush': '推送',
|
|
146
|
+
'setup.secVoice': '语音',
|
|
147
|
+
'setup.default': '(默认)',
|
|
148
|
+
'setup.on': '开',
|
|
149
|
+
'setup.off': '关',
|
|
150
|
+
'setup.yes': '是',
|
|
151
|
+
'setup.no': '否',
|
|
152
|
+
'setup.escBack': '(Esc 返回)',
|
|
153
|
+
'setup.actStart': '保存并启动',
|
|
154
|
+
'setup.actRestart': '保存并重启(立即生效)',
|
|
155
|
+
'setup.actSave': '保存',
|
|
156
|
+
'setup.actExit': '退出(丢弃改动)',
|
|
157
|
+
'setup.exited': '已取消 —— 未保存任何改动',
|
|
158
|
+
'setup.tunnelNone': '直连',
|
|
159
|
+
'setup.hintNone': '不经隧道,直连本机 —— 同一 Wi-Fi 或本机有公网 IP 均可;最简单',
|
|
160
|
+
'setup.hintCf': '在外面也能访问、免注册(临时地址;国内可能不稳)',
|
|
161
|
+
'setup.hintCfNamed': '你自己的域名,稳定 HTTPS(最省心)',
|
|
162
|
+
'setup.hintSsh': '你已经有一台服务器可以中转',
|
|
163
|
+
'setup.hintNatapp': '在外面也能访问、国内稳(需免费 natapp 账号)',
|
|
164
|
+
'setup.hintCpolar': '在外面也能访问、国内稳、自动安装(需免费 cpolar 账号)',
|
|
165
|
+
'setup.domainQ': '临时还是固定域名?',
|
|
166
|
+
'setup.domainTemp': '免费临时域名',
|
|
167
|
+
'setup.domainTempHint': '每次重启会变',
|
|
168
|
+
'setup.domainFixed': '固定/保留域名',
|
|
169
|
+
'setup.valPort': '端口需为 1–65535 的整数',
|
|
170
|
+
'setup.valRequired': '{label} 不能为空',
|
|
171
|
+
'setup.valHost': '请输入有效域名(如 myapp.example.com)',
|
|
172
|
+
'setup.valContact': '请填 mailto:you@example.com 或 https://你的站点(要真实域名 —— 苹果会拒收假域名)',
|
|
173
|
+
'setup.sumTemp': '临时',
|
|
174
|
+
'setup.sumFixed': '固定',
|
|
175
|
+
'setup.sumNoRelay': '无中转',
|
|
176
|
+
// 连接迷你中枢(各隧道字段行)
|
|
177
|
+
'setup.connTunnel': '隧道',
|
|
178
|
+
'setup.connMode': '模式',
|
|
179
|
+
'setup.connHostname': '域名',
|
|
180
|
+
'setup.connTunnelName': '隧道名',
|
|
181
|
+
'setup.connSshHost': 'SSH 主机',
|
|
182
|
+
'setup.connRemotePort': '远程端口',
|
|
183
|
+
'setup.connPublicUrl': '公网地址',
|
|
184
|
+
'setup.connJump': '跳板机',
|
|
185
|
+
'setup.connDomain': '域名',
|
|
186
|
+
'setup.connRegion': '区域',
|
|
187
|
+
'setup.connNone': '(未设置)',
|
|
188
|
+
'setup.connAuto': '(自动)',
|
|
189
|
+
'setup.askSshJump': 'ssh 跳板机(user@host,留空=无)',
|
|
190
|
+
'setup.cfModeQ': 'cloudflare 地址',
|
|
191
|
+
'setup.cfTemp': '临时地址(免登录)',
|
|
192
|
+
'setup.cfTempHint': '即时随机 https,重启会变',
|
|
193
|
+
'setup.cfNamed': '命名隧道(你的域名)',
|
|
194
|
+
'setup.cfNamedHint': '稳定 HTTPS,需登录 Cloudflare',
|
|
118
195
|
'setup.wrote': '✓ 已写入 {path}',
|
|
119
196
|
'setup.pushKeep': '保留已配置的推送通知吗?',
|
|
120
|
-
'setup.pushSetup': '
|
|
121
|
-
'setup.
|
|
122
|
-
'setup.
|
|
197
|
+
'setup.pushSetup': '开启手机通知吗?(编程 agent 干完活时提醒你)',
|
|
198
|
+
'setup.pushAbout': 'handmux 会为你自动生成一把私有签名密钥,存在 ~/.handmux/config.json,始终不出本机。下面的联系方式只是给推送服务留的联系地址,别人看不到。',
|
|
199
|
+
'setup.pushContact': '给推送服务留的联系方式 —— mailto: 或 https://(用默认即可;保留 mailto: 前缀)',
|
|
200
|
+
'setup.pushGenerated': '✓ 已生成签名密钥对(存在本机配置里)',
|
|
201
|
+
'setup.pushContactLabel': '联系人',
|
|
202
|
+
'setup.pushRegen': '重新生成密钥',
|
|
203
|
+
'setup.pushRegenHint': '会重置所有手机的订阅',
|
|
204
|
+
'setup.pushRegenConfirm': '重新生成密钥?已订阅的每台手机都会收不到通知,直到重新打开网页 App 再订阅一次。只有密钥泄露时才需要这么做。',
|
|
205
|
+
'setup.pushRegenerated': '✓ 已重新生成密钥 —— 手机需重新订阅',
|
|
206
|
+
'setup.pushOff': '关闭推送',
|
|
207
|
+
'setup.voiceOff': '关闭语音',
|
|
123
208
|
'setup.voiceKeep': '保留已配置的语音输入吗?',
|
|
124
|
-
'setup.voiceSetup': '
|
|
209
|
+
'setup.voiceSetup': '开启语音输入吗?(对着手机说话转文字;需讯飞账号)',
|
|
125
210
|
'setup.voiceAppId': 'xfyun appId',
|
|
126
211
|
'setup.voiceApiKey': 'xfyun apiKey',
|
|
127
212
|
'setup.voiceApiSecret': 'xfyun apiSecret',
|
|
@@ -148,38 +233,51 @@ export default {
|
|
|
148
233
|
'help.body': `handmux —— 从手机驱动你的 tmux
|
|
149
234
|
|
|
150
235
|
handmux start 直接运行(默认仅局域网;无需配置)
|
|
236
|
+
handmux open <会话名> 接入 tmux 会话,不存在则新建——手机上建的也能一键接管
|
|
151
237
|
handmux setup 配置隧道 / 名称 / 通知(写入配置;重跑即可修改)
|
|
152
238
|
handmux stop | restart | status
|
|
153
239
|
handmux logs [--follow] [--lines N]
|
|
154
240
|
handmux update 升级到最新发布版本(npm i -g handmux@latest)
|
|
155
241
|
|
|
242
|
+
第一次用?跑 'handmux setup' —— 它会带你配好外网访问 + 手机通知。
|
|
156
243
|
模型:'start' 运行 · 'setup' 配置(写入 ~/.handmux/config.json)· 重跑 setup 即可修改。
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
handmux
|
|
161
|
-
handmux
|
|
162
|
-
handmux
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
--
|
|
173
|
-
|
|
174
|
-
--
|
|
175
|
-
--
|
|
176
|
-
--
|
|
177
|
-
--
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
--
|
|
181
|
-
--
|
|
182
|
-
--
|
|
183
|
-
--
|
|
244
|
+
|
|
245
|
+
更多:
|
|
246
|
+
handmux config 显示生效的配置 + 每一项来自哪里
|
|
247
|
+
handmux hooks install|uninstall 启用/停用 agent 通知(收件箱)
|
|
248
|
+
handmux service install|uninstall 开机自启(launchd/systemd)
|
|
249
|
+
handmux help flags 一次性 flag + 环境变量(脚本 / 无头 / Docker)
|
|
250
|
+
--config PATH · --lang en|zh · --version, -v
|
|
251
|
+
`,
|
|
252
|
+
'help.flags': `handmux flag —— 一次性覆盖 & 无头配置
|
|
253
|
+
|
|
254
|
+
优先级:flag > 文件(~/.handmux/config.json)> 环境变量(HANDMUX_*)> 默认值。
|
|
255
|
+
flag 仅本次覆盖某一项、绝不持久化 —— 要持久化请用 'handmux setup'。
|
|
256
|
+
环境变量是无头场景(Docker / systemd / CI)的配置面,会跨多次运行生效。
|
|
257
|
+
|
|
258
|
+
start flag(括号内为对应环境变量):
|
|
259
|
+
--tunnel none|cloudflare|cloudflare-named|ssh|natapp|cpolar 暴露方式(默认:none)
|
|
260
|
+
--port N 服务端口(HANDMUX_PORT,默认:19999)
|
|
261
|
+
--host H 绑定地址(HANDMUX_HOST,默认:0.0.0.0)
|
|
262
|
+
--token S 鉴权令牌(HANDMUX_TOKEN,默认:每次启动自动生成)
|
|
263
|
+
--name "My Box" 浏览器标签 + 主屏图标里的应用名(HANDMUX_APP_NAME)
|
|
264
|
+
--public-url URL 对外公布的公网地址(HANDMUX_PUBLIC_URL;任意隧道均可,包括自建的 none;
|
|
265
|
+
ssh 默认 http://host:remotePort;natapp/cpolar 填你的固定/保留域名 ——
|
|
266
|
+
留空则用临时域名)
|
|
267
|
+
--ssh-host user@host[:port] ssh 隧道目标(HANDMUX_SSH_HOST)
|
|
268
|
+
--remote-port N 绑定在 ssh 主机上的端口(HANDMUX_REMOTE_PORT,默认:--port)
|
|
269
|
+
--ssh-jump u@h[,…] ssh 的可选跳板机(HANDMUX_SSH_JUMP)
|
|
270
|
+
--cf-hostname H cloudflare-named 的公网域名(HANDMUX_CF_HOSTNAME)
|
|
271
|
+
--cf-tunnel-name N cloudflare-named 的隧道名(HANDMUX_CF_TUNNEL_NAME,默认:handmux)
|
|
272
|
+
--authtoken T natapp / cpolar 的 authtoken(HANDMUX_AUTHTOKEN)
|
|
273
|
+
--cpolar-region R cpolar 边缘区域,如 cn(HANDMUX_CPOLAR_REGION)
|
|
274
|
+
--preview-domain D 启用动态预览,需要通配子域名(HANDMUX_PREVIEW_DOMAIN)
|
|
275
|
+
--foreground, -f 前台运行(不后台化)
|
|
276
|
+
--no-qr 不渲染二维码
|
|
277
|
+
|
|
278
|
+
很少用到(环境变量或 flag):--static-dir / --upload-exts / --preview-ttl
|
|
279
|
+
(HANDMUX_STATIC_DIR / HANDMUX_UPLOAD_EXTS / HANDMUX_PREVIEW_TTL)
|
|
280
|
+
|
|
281
|
+
'handmux service install' 也接受上面这些 start flag —— 它们会被烘进自启项。
|
|
184
282
|
`,
|
|
185
283
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// natapp is an ngrok-derived domestic tunnel. On the FREE tier it hands out a random http(s)://<sub>.natappfree.cc
|
|
2
|
+
// each run, printed in natapp's `-log=stdout` output — scrape it exactly like the cloudflare quick tunnel.
|
|
3
|
+
// A reserved/paid domain (e.g. *.natapp1.cc or your own) is bound to the authtoken server-side and known up
|
|
4
|
+
// front (→ publicUrl, gated on that host appearing), so scraping is only ever for the free zone. Pure →
|
|
5
|
+
// unit-tested against captured log lines.
|
|
6
|
+
const RE = /https?:\/\/[a-z0-9][a-z0-9-]*\.natappfree\.cc/;
|
|
7
|
+
|
|
8
|
+
export function extractNatappUrl(text) {
|
|
9
|
+
const m = RE.exec(String(text || ''));
|
|
10
|
+
return m ? m[0] : null;
|
|
11
|
+
}
|
package/src/cli/options.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (generated when unset) so a public tunnel can never come up token-less.
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
export const TUNNELS = ['none', 'cloudflare', 'cloudflare-named', 'ssh'];
|
|
6
|
+
export const TUNNELS = ['none', 'cloudflare', 'cloudflare-named', 'ssh', 'natapp', 'cpolar'];
|
|
7
7
|
|
|
8
8
|
const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
9
9
|
|
|
@@ -69,6 +69,7 @@ export function resolveConfig(flags = {}, fileCfg = {}, env = process.env, gen =
|
|
|
69
69
|
// tunnel-specific (null unless the relevant tunnel is selected)
|
|
70
70
|
sshHost: null, remotePort: null, sshJump: null,
|
|
71
71
|
cfHostname: null, cfTunnelName: null,
|
|
72
|
+
authtoken: null, cpolarRegion: null,
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
if (tunnel === 'ssh') {
|
|
@@ -88,6 +89,19 @@ export function resolveConfig(flags = {}, fileCfg = {}, env = process.env, gen =
|
|
|
88
89
|
cfg.publicUrl = cfg.publicUrl || `https://${cfg.cfHostname}`;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// natapp / cpolar: one shared credential (--authtoken), and named/fixed mode is just --public-url (the
|
|
93
|
+
// reserved domain) — no separate concept. A bare host is normalised to https:// so the user can paste the
|
|
94
|
+
// domain the provider gave them. cpolar also takes an optional edge --cpolar-region (cn = mainland China).
|
|
95
|
+
if (tunnel === 'natapp' || tunnel === 'cpolar') {
|
|
96
|
+
cfg.authtoken = pick('authtoken', env.HANDMUX_AUTHTOKEN) || null;
|
|
97
|
+
if (!cfg.authtoken) {
|
|
98
|
+
const where = tunnel === 'natapp' ? 'natapp.cn' : 'cpolar.com';
|
|
99
|
+
throw new Error(`${tunnel} needs --authtoken <token> (or HANDMUX_AUTHTOKEN) — get it from ${where} after logging in`);
|
|
100
|
+
}
|
|
101
|
+
if (cfg.publicUrl && !/^https?:\/\//i.test(cfg.publicUrl)) cfg.publicUrl = `https://${cfg.publicUrl}`;
|
|
102
|
+
if (tunnel === 'cpolar') cfg.cpolarRegion = pick('cpolarRegion', env.HANDMUX_CPOLAR_REGION) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
return cfg;
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -164,6 +178,14 @@ export function explainConfig(flags = {}, fileCfg = {}, cfgPath = null, env = pr
|
|
|
164
178
|
add('cfHostname', trace(flags, fileCfg, env, cfgPath, 'cfHostname', 'HANDMUX_CF_HOSTNAME', null));
|
|
165
179
|
add('cfTunnelName', trace(flags, fileCfg, env, cfgPath, 'cfTunnelName', 'HANDMUX_CF_TUNNEL_NAME', 'handmux'));
|
|
166
180
|
}
|
|
181
|
+
if (t === 'natapp' || t === 'cpolar') {
|
|
182
|
+
const at = trace(flags, fileCfg, env, cfgPath, 'authtoken', 'HANDMUX_AUTHTOKEN', null);
|
|
183
|
+
add('authtoken', at, at.value == null ? '(required — from the provider dashboard)' : mask(at.value));
|
|
184
|
+
}
|
|
185
|
+
if (t === 'cpolar') {
|
|
186
|
+
const rg = trace(flags, fileCfg, env, cfgPath, 'cpolarRegion', 'HANDMUX_CPOLAR_REGION', null);
|
|
187
|
+
add('cpolarRegion', rg, rg.value == null ? '(cpolar default)' : String(rg.value));
|
|
188
|
+
}
|
|
167
189
|
|
|
168
190
|
// Integrations live only in the file (secrets); show presence, never the keys.
|
|
169
191
|
rows.push({ key: 'push (vapid)', origin: fileCfg.vapid ? (cfgPath || 'file') : 'default', display: fileCfg.vapid ? 'on' : 'off' });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The ONLY module that touches @clack/prompts — the interactive shell for `handmux setup`. Everything else
|
|
2
|
+
// imports from here, so the prompt library stays swappable behind one seam and the wizard's pure logic
|
|
3
|
+
// (configFromAnswers / validators / …) never depends on it.
|
|
4
|
+
import { select, text, password, confirm, note, intro, outro, cancel, isCancel } from '@clack/prompts';
|
|
5
|
+
|
|
6
|
+
export { select, text, password, confirm, note, intro, outro, cancel };
|
|
7
|
+
|
|
8
|
+
// Thrown by ask() when the user cancels a prompt (Esc / Ctrl-C), so the wizard catches ONCE at the top
|
|
9
|
+
// instead of checking isCancel after every call.
|
|
10
|
+
export const CANCELLED = Symbol('setup-cancelled');
|
|
11
|
+
|
|
12
|
+
// Await a clack prompt, converting a cancel into a throw. Usage: `const v = await ask(select({…}))`.
|
|
13
|
+
export async function ask(promptResult) {
|
|
14
|
+
const v = await promptResult;
|
|
15
|
+
if (isCancel(v)) throw CANCELLED;
|
|
16
|
+
return v;
|
|
17
|
+
}
|