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.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 可以显式指定解释器和前置参数

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

下一步建议 ​