news 2026/9/15 17:25:41

Effect 不稳定版 CLI 命令注解:用 `Command.annotate` 与 `Command.annotateMerge` 向 `HelpDoc` 注入自定义元数据

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Effect 不稳定版 CLI 命令注解:用 `Command.annotate` 与 `Command.annotateMerge` 向 `HelpDoc` 注入自定义元数据

Effect 不稳定版 CLI 命令注解:用Command.annotateCommand.annotateMergeHelpDoc注入自定义元数据

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

effect的 unstable CLI 模块(effect/unstable/cli)在 4.0.0 中新增了Command.annotateCommand.annotateMerge两个组合子,允许为命令挂载基于Context的自定义注解,并将这些注解透传到HelpDoc,供自定义帮助格式化器读取。本文结合 changeset 说明 与仓库源码、测试用例,讲解注解的 API 语义、内部数据流,以及如何编写自定义帮助格式化器消费这些元数据,帮助你在构建 CLI 应用时为命令附加团队归属、废弃状态、文档链接等机器可读信息。

一、这次 changeset 引入了什么

mean-dingos-share.md 是本次预发布(pre-release)变更记录,内容如下:

AddCommand.annotateandCommand.annotateMergeto unstable CLI commands, and include command annotations inHelpDocso custom help formatters can access command metadata.

翻译过来包含两个能力点:

  1. 在 unstable CLI 命令上新增Command.annotateCommand.annotateMerge两个 API,用于为命令附加注解;
  2. 命令注解会被包含进生成的HelpDoc,从而让自定义帮助格式化器(custom help formatters)能够访问命令元数据。

从源码看,这两个 API 定义在 packages/effect/src/unstable/cli/Command.ts,标注为@since 4.0.0,并且位于effect/unstable/cli模块——即 API 仍在演进中,使用前需注意其 unstable 属性。

二、命令注解的本质:基于Context的元数据

Command 结构中的 annotations 字段

Command接口本身就声明了注解字段(Command.ts#L163-L166):

/** * Custom annotations associated with this command. */ readonly annotations: Context.Context<never>

注解的类型是Context.Context<never>——也就是说,注解不是简单的字符串键值对,而是 Effect 的依赖注入Context,可以用任意Context.Key作为键、以任意类型作为值。这与 Effect 生态中服务注册、Cause 注解(Cause.annotate)等设计一脉相承,类型安全且可组合。

Command.Any类型中也保留了同样的字段(Command.ts#L355),保证无论命令的具体泛型参数如何,注解能力始终可用。

定义注解键

注解键使用Context.Service(或Context.Tag/Context.Reference)创建。仓库测试 Command.test.ts#L53-L54 展示了标准用法:

const Team = Context.Service<never, string>("effect/test/unstable/cli/Team") const Priority = Context.Service<never, number>("effect/test/unstable/cli/Priority")

这里Team的键关联一个字符串值(如团队名),Priority关联一个数字值(如优先级)。自定义帮助格式化器正是通过这些键从HelpDoc的注解上下文中取值。

三、Command.annotate:为命令挂载单个注解

Command.annotate用于给命令附加一个Context.Key对应的值,签名如下(Command.ts#L1273-L1292):

export const annotate: { <I, S>( service: Context.Key<I, S>, value: NoInfer<S> ): <Name extends string, Input, E, R, ContextInput>( self: Command<Name, Input, ContextInput, E, R> ) => Command<Name, Input, ContextInput, E, R> <Name extends string, Input, E, R, ContextInput, I, S>( self: Command<Name, Input, ContextInput, E, R>, service: Context.Key<I, S>, value: NoInfer<S> ): Command<Name, Input, ContextInput, E, R> }

它是用dual实现的,因此既支持 pipe 风格,也支持直接传参风格。底层实现非常直接(Command.ts#L1285-L1292):

const impl = toImpl(self) return makeCommand({ ...impl, annotations: Context.add(impl.annotations, service, value) })

即取出命令内部实现,在其注解Context上执行Context.add,再重建命令。

使用示例

import { Context } from "effect" import { Command } from "effect/unstable/cli" const Team = Context.Service<never, string>("app/cli/Team") const deploy = Command.make("deploy").pipe( Command.annotate(Team, "runtime") ) // 读取命令注解 const team = Context.get(deploy.annotations, Team) // => "runtime"

需要注意的陷阱

源码文档明确标注了一个 Gotcha(Command.ts#L1266):

Adding the sameContext.Keyagain replaces the earlier value.

即对同一个Context.Key重复调用annotate,新值会替换旧值,而不是叠加。这源于Context.add的语义——一个键在Context中只能存在一个值。

四、Command.annotateMerge:合并已有的注解 Context

annotateMerge用于将一整个已构建好的Context.Context<I>合并进命令注解(Command.ts#L1317-L1333):

export const annotateMerge: { <I>( annotations: Context.Context<I> ): <Name extends string, Input, E, R, ContextInput>( self: Command<Name, Input, ContextInput, E, R> ) => Command<Name, Input, ContextInput, E, R> <Name extends string, Input, E, R, ContextInput, I>( self: Command<Name, Input, ContextInput, E, R>, annotations: Context.Context<I> ): Command<Name, Input, ContextInput, E, R> }

底层实现使用Context.merge(Command.ts#L1327-L1333):

const impl = toImpl(self) return makeCommand({ ...impl, annotations: Context.merge(impl.annotations, annotations) })

使用示例

import { Context } from "effect" import { Command } from "effect/unstable/cli" const Team = Context.Service<never, string>("app/cli/Team") const Priority = Context.Service<never, number>("app/cli/Priority") const deploy = Command.make("deploy").pipe( Command.annotate(Team, "runtime"), Command.annotateMerge(Context.make(Priority, 2)) )

合并优先级

源码文档同样记录了一个 Gotcha(Command.ts#L1308-L1310):

If both contexts contain the sameContext.Key, the incoming annotations context wins.

当命令已有注解与传入的Context中存在相同键时,传入的新Context胜出。这与annotate的"后写覆盖"语义保持一致,方便用一组默认注解构造Context,再按需覆盖单个键。

五、注解如何流入HelpDoc:内部数据流

理解整条链路需要看三层源码:

1. 命令内部实现持有注解

在 internal/command.ts#L110-L229 的makeCommand中,注解被持久化到命令内部:

const annotations = options.annotations ?? Context.empty()

所有组合子(包括withSubcommandswithSharedFlags等)在重建命令时都会通过...impl保留annotations字段(例如 Command.ts#L933),因此注解不会在管道操作中丢失。

2.buildHelpDoc将注解写入 HelpDoc

makeCommand内部定义了buildHelpDoc(internal/command.ts#L130-L202),它负责把命令的描述、用法、参数、标志、子命令、示例以及注解组装成结构化HelpDoc。关键的返回对象(internal/command.ts#L193-L201):

return { description: options.description ?? "", usage, flags, annotations, ...(args.length > 0 && { args }), ...(subcommandDocs.length > 0 && { subcommands: subcommandDocs }), ...(examples.length > 0 && { examples }) }

注意annotations直接原样透传。此外,buildHelpDoc还承担了隐藏标志(hidden)与不列出子命令(unlisted)的过滤逻辑(internal/command.ts#L165-L189),这些与注解是正交的功能。

3.HelpDoc类型声明注解字段

HelpDoc接口在 HelpDoc.ts#L87-L90 声明:

/** * Custom command annotations. */ readonly annotations: Context.Context<never>

这意味着任何拿到HelpDoc的消费者(默认帮助输出、自定义格式化器、文档生成工具)都能读取注解。

4. 帮助文档生成入口

当用户执行--help时,getHelpForCommandPath会沿命令路径找到当前命令,调用其buildHelpDoc生成基础文档,再叠加共享标志与全局标志(internal/help.ts#L152-L183):

const baseDoc = toImpl(currentCommand).buildHelpDoc(commandPath) // ... return { ...baseDoc, flags: [...sharedFlags, ...baseDoc.flags], globalFlags: globalFlagDocs }

由于使用...baseDoc展开,annotations字段天然保留在最终文档中。

六、消费注解:编写自定义 HelpDoc 格式化器

注解存在的意义是"让自定义 help formatters 可以访问命令元数据"。默认格式化器defaultFormatterformatHelpDoc实现(CliOutput.ts#L474-L630)会输出DESCRIPTIONUSAGEARGUMENTSFLAGSGLOBAL FLAGSSUBCOMMANDSEXAMPLES等区块,但不会渲染annotations——注解是给程序化消费方(而非终端用户)准备的元数据。

Formatter 服务

CliOutput.Formatter是一个Context.Reference(CliOutput.ts#L227-L229),默认实现即defaultFormatter()。接口定义(CliOutput.ts#L52-L93)包含formatErrorformatVersionformatHelpDoc等方法。

有两种方式注入自定义格式化器:

  • 通过CliOutput.layer(formatter)提供 Layer(CliOutput.ts#L259):const layer = (formatter: Formatter): Layer.Layer<never> => Layer.succeed(Formatter)(formatter)
  • 通过Effect.provideService(CliOutput.Formatter, formatter)直接提供服务——仓库测试采用的就是这种方式。

完整示例:在帮助头部渲染团队归属

import { Context, Effect, Option } from "effect" import { CliOutput, Command } from "effect/unstable/cli" import { NodeRuntime, NodeServices } from "@effect/platform-node" const Team = Context.Service<never, string>("app/cli/Team") const formatter: CliOutput.Formatter = { ...CliOutput.defaultFormatter({ colors: false }), formatHelpDoc: (doc) => { const team = Context.getOption(doc.annotations, Team) const header = Option.match(team, { onNone: () => "Team: unassigned\n", onSome: (t) => `Team: ${t}\n` }) // 拼接默认帮助输出 return header + CliOutput.defaultFormatter({ colors: false }).formatHelpDoc(doc) } } const deploy = Command.make("deploy").pipe( Command.annotate(Team, "runtime"), Command.withDescription("Deploy the service to a target environment") ) const app = Command.make("app").pipe( Command.withSubcommands([deploy]), Command.run({ version: "1.0.0" }), Effect.provideService(CliOutput.Formatter, formatter), Effect.provide(NodeServices.layer), NodeRuntime.runMain )

这里使用了Context.getOption(Context.ts#L1093 附近定义)——它返回Option,缺失时是Option.none而不是抛错,非常适合格式化器这种"注解可有可无"的消费场景。如果不希望使用getOption,也可先用Context.has判断再Context.get

当用户运行app deploy --help时,输出会以Team: runtime开头,随后是默认的DESCRIPTION/USAGE等区块。

七、源码级验证:测试用例

仓库测试 packages/effect/test/unstable/cli/Command.test.ts#L49-L115 为注解功能提供了两组针对性用例,直接印证了 changeset 描述的行为。

用例 1:注解应暴露在帮助文档中

it.effect("should expose annotations in help docs", () => Effect.gen(function*() { const Team = Context.Service<never, string>("effect/test/unstable/cli/Team") const Priority = Context.Service<never, number>("effect/test/unstable/cli/Priority") const docs: Array<Parameters<CliOutput.Formatter["formatHelpDoc"]>[0]> = [] const formatter: CliOutput.Formatter = { ...CliOutput.defaultFormatter({ colors: false }), formatHelpDoc: (doc) => { docs.push(doc) return "" } } const command = Command.make("deploy").pipe( Command.annotate(Team, "runtime"), Command.annotateMerge(Context.make(Priority, 2)) ) yield* Command.runWith(command, { version: "1.0.0" })(["--help"]).pipe( Effect.provide(TestLayerWithoutFormatter), Effect.provideService(CliOutput.Formatter, formatter) ) assert.strictEqual(docs.length, 1) const annotations = docs[0].annotations assert.strictEqual(Context.get(annotations, Team), "runtime") assert.strictEqual(Context.get(annotations, Priority), 2) }))

该用例验证了:annotateannotateMerge设置的注解,在--help生成的HelpDoc.annotations中都能通过原Context.Key读到,且类型无损。

用例 2:添加子命令后注解得以保留

it.effect("should keep annotations when adding subcommands", () => Effect.gen(function*() { const Scope = Context.Service<never, string>("effect/test/unstable/cli/Scope") const docs: Array<Parameters<CliOutput.Formatter["formatHelpDoc"]>[0]> = [] // ... 自定义 formatter 收集 doc ... const child = Command.make("child").pipe(Command.annotate(Scope, "child")) const command = Command.make("root").pipe( Command.annotate(Scope, "root"), Command.withSubcommands([child]) ) const run = Command.runWith(command, { version: "1.0.0" }) yield* run(["--help"]).pipe(/* 提供 formatter */) yield* run(["child", "--help"]).pipe(/* 提供 formatter */) assert.strictEqual(docs.length, 2) assert.strictEqual(Context.get(docs[0].annotations, Scope), "root") assert.strictEqual(Context.get(docs[1].annotations, Scope), "child") }))

这个用例揭示了一个重要语义:注解是命令作用域的,不会被子命令继承root --help拿到Scope = "root"child --help拿到Scope = "child"——每个命令维护自己的注解Context。如果你的格式化器需要跨层级聚合元数据,需要自行沿HelpDocsubcommands结构或命令路径遍历,框架不做隐式继承。

八、与现有 CLI 应用的结合实践

仓库的 ai-docs/src/70_cli/10_basics.ts 给出了完整的 CLI 应用骨架:用Flag.String/Flag.Boolean/Flag.Literals定义参数,Command.make创建命令,Command.withSharedFlags共享父级标志,Command.withSubcommands组合子命令,最后用Command.run+NodeServices.layer+NodeRuntime.runMain启动。

注解能力可以无缝嵌入这一流程,常见用法包括:

  • 团队/模块归属:标注命令由哪个团队负责,便于帮助输出或 CI 检查;
  • 废弃状态Command.annotate(Deprecated, true),自定义格式化器可渲染 "DEPRECATED" 提示或警告;
  • 文档链接与元数据:挂载内部文档 URL、SLI 指标名、权限等级等机器可读信息,供文档生成器或自动化脚本消费;
  • 诊断信息:与--verbose等全局标志配合,帮助输出中附带命令内部信息。

这些注解与Command.withExamplesCommand.withDescription等元数据组合子正交共存——buildHelpDoc会同时收集它们(见 internal/command.ts#L130-L202),互不干扰。

九、注意事项总结

  1. unstable 模块effect/unstable/cli中的 API(包括Command.annotate/annotateMerge)尚不稳定,可能随版本演进调整签名或语义,升级依赖时请关注 CHANGELOG.md。
  2. 版本前提:两个 API 标注@since 4.0.0,需使用 effect 4.0.0 及以上版本。
  3. 键冲突语义annotate重复同键会替换旧值;annotateMerge遇到同键时传入的Context胜出。
  4. 作用域:注解按命令隔离,子命令不会继承父命令注解;withSubcommands等组合操作会保留各自已有的注解。
  5. 消费方:默认帮助输出不渲染注解,注解主要面向自定义CliOutput.Formatter、文档生成器等程序化消费场景;读取时推荐使用Context.getOption处理可选性。

一句话总结:Command.annotate/Command.annotateMerge为 Effect CLI 命令补上了结构化元数据通道,配合HelpDoc.annotations与自定义Formatter,你可以在保持类型安全的前提下,为每一个命令注入并读取任意元数据,让帮助系统从"给人看"升级为"人机两用"。

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 17:25:10

新手入门选wordpress微信模板的5个避坑指南

新手入门选wordpress微信模板的5个避坑指南 网站做好了没人访问,这是很多新手老板最头疼的事。你花大价钱请人做站,结果上线三个月,后台看数据,每天就几个IP,还全是自己人点的。这时候才想起来,当初选模板太随意,根本没考虑手机端的体验。对于西北地区的中小企业来说,客户大多在手机上刷朋友圈、逛微信…

作者头像 李华
网站建设 2026/9/15 17:24:30

AD域用户无法改密码?账户属性与密码策略排查指南

域账号密码改不了这事&#xff0c;在AD环境里几乎每周都能遇到。用户一脸茫然地跑过来说“密码明明没错&#xff0c;就是改不动”&#xff0c;Helpdesk查了一圈也不知道卡在哪。问题看起来简单&#xff0c;但背后的原因可以排出一长串——账户属性勾错了、策略不达标、复制没同…

作者头像 李华
网站建设 2026/9/15 17:21:58

PostHog 实验定性反馈指南:用变体分拆问卷倾听用户真实感受

PostHog 实验定性反馈指南&#xff1a;用变体分拆问卷倾听用户真实感受 【免费下载链接】posthog :hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments…

作者头像 李华
网站建设 2026/9/15 17:21:17

UI-TARS 桌面自动化实战:从视觉识别到精准点击

UI-TARS 桌面自动化实战&#xff1a;从视觉识别到精准点击 【免费下载链接】UI-TARS Pioneering Automated GUI Interaction with Native Agents 项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS UI-TARS 是一套 GUI 桌面自动化管线&#xff1a;多模态模型看…

作者头像 李华