Appearance
命令执行
command 步骤是 clack-kit 从交互流程走向真实工作流的分水岭
这一页重点回答五个问题
- command、scriptFile、run 该怎么选
- renderer 有什么区别
- 并行、重试和失败处理怎么配
- 结果写到哪里
- io 提供哪些能力
三种入口怎么选
| 入口 | 适合场景 |
|---|---|
| command | 一条完整 shell 命令就够了 |
| scriptFile | 仓库里已经有脚本文件 |
| run(context, io) | 需要多步控制、动态拼装参数、读写更多上下文 |
command
ts
defineStep.command({
id: 'install',
command: 'bun install',
title: '安装依赖',
});如果一条 shell 文本不够直观,也可以直接返回字符串数组,clack-kit 会按换行拼成最终执行脚本,同时在预览 note 里保留多行结构
ts
defineStep.command({
id: 'bootstrap',
title: '初始化目录',
command: ['mkdir -p ./demo', 'cd ./demo', 'bun init -y'],
});scriptFile
ts
defineStep.command({
id: 'prepareDemo',
scriptFile: 'scripts/setup-demo.sh',
title: '准备示例环境',
});run(context, io)
ts
defineStep.command({
id: 'bootstrap',
title: '初始化项目',
async run(context, io) {
await io.exec(`mkdir -p ${String(context.values.projectDir)}`);
await io.exec('bun init -y', { cwd: String(context.values.projectDir) });
return { bootstrapped: true };
},
});renderer 怎么选
| renderer | 适合场景 |
|---|---|
| task-log | 默认方案,适合大多数 workflow |
| quiet | 只关心结果,不想实时展开输出 |
| inherit | 把子进程输出直接写回当前终端 |
task-log
最适合 workflow 场景
- 输出按步骤组织
- 并行命令不会把日志搅成一团
- 实时展示会按可见行渲染,
result.stdout和result.stderr仍保留原始分流 - 可以通过 taskLog.limit 和 taskLog.retainLog 调整保留策略
如果想统一设置这类策略,可以在 createWorkflowKit({ taskLog }) 上声明全局默认值
优先级是 defineStep.command().taskLog > createWorkflowKit().taskLog > 默认值
如果需要显式取消 limit,可以把 taskLog.limit 设成 null 或 undefined,这时会回退到底层 taskLog() 的未设置行为
quiet
适合输出很多、但运行过程不需要持续盯着看的步骤
inherit
适合希望把子进程输出直接写回当前终端的场景
如果没有显式提供 title,clack-kit 会按原来的方式展示命令步骤,再把后续日志直接交给终端
如果 command step 配了 title,执行前会优先把待执行命令展示出来
inherit/tty: 'passthrough'会用一条 note 展示task-log会把命令作为第一条可见日志写进当前 task-log
如果命令真的依赖原生 TTY 行为,比如 docker build、交互式进度条或终端控制动画,还要再配 tty: 'passthrough'
什么时候要开 tty: 'passthrough'
当命令会主动探测自己是不是连在真实终端上时,只开 renderer: 'inherit' 还不够
这时应该显式启用原生终端模式
ts
defineStep.command({
id: 'docker-build',
command: 'docker build -t demo .',
tty: 'passthrough',
});只要设置了 tty: 'passthrough',clack-kit 就会自动按 renderer: 'inherit' 的语义处理这次命令执行,所以不需要再重复声明 renderer
这样 clack-kit 仍然接管步骤编排、退出码和后续流程,但 stdout、stderr、combined 在执行记录里可能为空或不完整,因为输出已经直接交给当前 TTY 了
如果要保留完整日志,就别开 tty: 'passthrough'
因为这类模式本质上是把子进程直接挂到真实终端上,想同时保留原生 TTY 效果和完整日志,需要额外的 PTY 录制层,单靠普通 spawn 的 pipe 或 inherit 做不到
io 里有哪些能力
run(context, io) 中最常用的是这几项
| 方法 | 作用 |
|---|---|
| io.exec | 直接执行一条命令字符串 |
| io.execScriptFile | 执行一个脚本文件 |
| io.stdout | 写 stdout |
| io.stderr | 写 stderr |
| io.sleep | 休眠 |
| io.setExitCode | 显式设置当前步骤退出码 |
| io.note | 输出说明块 |
| io.box | 输出 box |
| io.log | 输出结构化日志 |
| io.colors | 终端着色工具 |
这些能力的价值在于,命令步骤仍然留在 workflow 的统一运行时里,不必跳回脚本世界重新组织状态
when 里怎么判断命令是否可用
defineStep.command 的 when 回调拿到的是完整 context
它不直接暴露 io.exec 这种执行接口,但现在可以通过 context.tools.commandExists() 做无副作用探测
ts
defineStep.command({
id: 'dockerBuild',
command: 'docker build -t demo .',
title: '构建 Docker 镜像',
when(context) {
return context.tools.commandExists('docker');
},
});如果在 workflow 外也要复用这段判断,可以直接使用根入口导出的 commandExists()
ts
import { commandExists } from 'clack-kit';
const hasDocker = await commandExists('docker');command.confirm 什么时候值得开
当某条命令有明显副作用,或者你希望在 when 通过之后再让用户做一次最终确认时,可以打开 confirm
ts
defineStep.command({
id: 'release',
command: 'npm publish',
confirm: {
active: '确认发布',
defaultValue: false,
message: '确定要发布到 npm 吗?',
},
title: '发布包',
});confirm: true 仍然可用,此时会使用默认确认文案
如果传对象,它接受 defineStep.confirm() 除 id 外的全部选项
它的执行顺序是 when -> command.confirm.when -> confirm -> command
如果用户选择取消执行,这个步骤会按 skipped 记录,不会真正启动命令
结果写入规则
需要先分清两层结果
- 命令执行元数据进入事件和日志
- 命令步骤显式返回值进入 results
ts
defineStep.command({
id: 'buildImage',
title: '构建镜像',
async run(_, io) {
const execution = await io.exec('docker build -t demo .');
return {
code: execution.code,
ok: execution.code === 0,
};
},
});workflow 结束后,可以从 result.results.buildImage 读取这个对象
如果使用 command 或 scriptFile,也可以通过 result(execution) 映射最终返回值
allowFailure 适合什么场景
默认情况下,命令失败会中断 workflow
allowFailure 适合“失败也要继续收集后续信息”的步骤,例如非阻断型检查
不适合真正必须成功的发布动作
exitProcessOnFailure 什么时候要开
如果某条命令一旦失败,就应该立刻结束整个 CLI,而不是继续等同组并行命令或后续步骤跑完,可以打开 exitProcessOnFailure
它适合 multiselect -> 多条 command 并行执行 这类场景,某一条命令失败后会直接以对应退出码结束当前进程
如果同时配置了 allowFailure,exitProcessOnFailure 优先
如果同时配置了 retry,会在最后一次失败后再退出
parallel 的行为边界
同层、连续、parallel 分组相同的 command 步骤会一起执行
ts
defineSteps([
defineStep.command({ id: 'lint', parallel: 'checks', title: 'Lint', command: 'bun run lint' }),
defineStep.command({ id: 'test', parallel: 'checks', title: 'Test', command: 'bun run test' }),
defineStep.command({ id: 'build', parallel: 'checks', title: 'Build', command: 'bun run build' }),
]);并行只影响执行方式,不改变结果归档方式,每个步骤仍然保有自己的事件和结果位置
retry 什么时候值得开
retry 适合面对临时性失败
- 网络抖动
- 远端资源短暂不可用
- 缓慢启动的服务探测
常用字段包括
- maxAttempts
- delayMs
- backoff,支持 fixed、linear、exponential
shell、cwd 和 projectRoot 的关系
- cwd 控制单条命令的工作目录
- createWorkflowKit 的 cwd 会成为运行时的 projectRoot
- projectRoot 控制相对路径的解析根目录
- env 未提供时继承当前进程环境变量,显式传对象时不会自动与 process.env 合并
- shell 可以显式指定解释器和前置参数
需要跨平台、跨目录运行脚本时,先把这三者分开考虑,问题会少很多