Skip to content

命令执行

返回总览

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.stdoutresult.stderr 仍保留原始分流
  • 可以通过 taskLog.limit 和 taskLog.retainLog 调整保留策略

如果想统一设置这类策略,可以在 createWorkflowKit({ taskLog }) 上声明全局默认值

优先级是 defineStep.command().taskLog > createWorkflowKit().taskLog > 默认值

如果需要显式取消 limit,可以把 taskLog.limit 设成 nullundefined,这时会回退到底层 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.commandwhen 回调拿到的是完整 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 并行执行 这类场景,某一条命令失败后会直接以对应退出码结束当前进程

如果同时配置了 allowFailureexitProcessOnFailure 优先

如果同时配置了 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 可以显式指定解释器和前置参数

需要跨平台、跨目录运行脚本时,先把这三者分开考虑,问题会少很多

下一步建议