目标
交互式命令行对人类很友好,遇到管道就变成灾难。能同时活在两个世界的架构是:旗标是契约,提示只补缺失值,校验在提问循环里就地完成,再加一道 TTY 检测把“无限挂起”变成一条可行动的报错。CI 里两秒钟退出,人在终端里被友好地问。
步骤要点
- 先检测 stdin.isTTY,非终端环境且必填旗标缺失时打印“在脚本与 CI 中请显式传 –name”并以退出码 2 结束。
- 实现 ask 循环:问题带可见默认值后缀,空回车取默认,validate 回调返回错误文案就打印“invalid: 原因”并重新提问,finally 关闭 readline 保证 Ctrl-C 干净退出。
- 接线时让旗标驱动一切:args.name 存在就不问,region 有默认永不问,确认步骤对交互场景开放且支持 –yes 旁路。
- 用管道实测非交互路径:echo 空串加旗标能跑通,/dev/null 无旗标时两秒内报错退出而非挂起。
- 给提问循环本身加脚本化端到端检查:通过 stdin 先送非法值再送合法值,断言重试提示出现且最终成功,无需真人键盘。
排错提示
CI 里挂起是 TTY 检查缺失或做得太晚,上线前用 < /dev/null 自测,stdin 非终端且有必填旗标未给就提前退出;带空格引号的答案炸脚本说明用了裸 read,shell 用 read -r、Node 用 readline,插值全部加引号并先做形状校验;破坏性确认被手滑 y 过关就改成“输入资源名以确认”,默认值只留给安全操作,销毁类提示默认中止。
适用边界
交互提示只属于人类,每多一个问题脚本就多一个答不了的空,旗标才是真正的契约;输入资源名确认也会被录屏与粘贴绕过,硬删除要靠显式 id 而不只是一个词。
代码示例
interactive session:
$ node cli.mjs
project name (demo-app): BAD NAME!
invalid: lowercase letters, digits and dashes only
project name (demo-app): demo-app
create demo-app in us-east-1? (yes): yes
created.
non-interactive session (CI):
$ node cli.mjs < /dev/null
error: --name is required when stdin is not a TTY
pass --name explicitly in scripts and CI
exit=2 (2 seconds, not a hang)