@cofold/commands

中文文档
0.3.0发布于 2026/10/06MITTypeScriptES Module构建来源已验证近 30 天下载 2740

A command is data: the declaration, the registry, input, the argv grammar, coercion and schema, rendered by every surface.

原始使用文档
文档目录15 节
  1. What it is
  2. One schema
  3. One action, several surfaces
  4. Capabilities
  5. Packages
  6. Try it
  7. One declaration, three surfaces
  8. A local CLI
  9. Commands supplied by a server
  10. Turn an OpenAPI document into a CLI
  11. Examples
  12. Commands from a document
  13. Documentation
  14. Why
  15. License

@cofold/commands

Declare a command once. Run it anywhere.

npm node types dependencies license

The command framework of the cofold family: this package is the declaration, the registry and the input every surface shares; the surfaces are listed under Packages, all but the generated reference as sibling packages.

What it is

@cofold/commands is a TypeScript command framework where commands are reusable definitions rather than CLI-only handlers.

The same declaration can be exposed through CLI, HTTP endpoint, an MCP tool, generated documentation, shell completion, an agent-facing skill, or other adapters without redefining its inputs and behavior.

const registry = createRegistry()
  .provide("config", {
    resolve: () => loadConfig(),
  })
  .provide("store", {
    deps: ["config"],
    resolve: ({ config }) => open(config.path),
    dispose: (store) => store.flush(),
  });

registry.action({
  id: "note.list",
  summary: "List notes, newest first",

  needs: ["store"],

  input: {
    status: {
      type: "string",
      enum: ["open", "done"],
      description: "Only this status",
      cli: { short: "-s", value: "STATUS" },
    },

    limit: {
      type: "integer",
      minimum: 1,
      default: 20,
      description: "How many",
      cli: { short: "-n", value: "N" },
    },
  },

  surfaces: {
    cli:  { pattern: ["note", "list"] },
    http: { method: "GET", path: "/notes" },
    mcp: true,
  },

  run: ({ input, store }) =>
    output(store.all(input.status).slice(0, input.limit)),
});

registry.action({
  id: "note.add",
  summary: "Add a note",

  needs: ["store"],

  input: {
    title: {
      type: "string",
      minLength: 1,
      maxLength: 120,
      description: "What the note says",
    },

    status: {
      type: "string",
      enum: ["open", "done"],
      default: "open",
      description: "Where it starts",
      cli: { short: "-s", value: "STATUS" },
    },
  },
  required: ["title"],

  surfaces: {
    cli:  { pattern: ["note", "add", ":title"] },
    http: { method: "POST", path: "/notes" },
    mcp: true,
  },

  run: ({ input, store }) => output(store.add(input)),
});

There is no separate command definition for each surface.

flowchart TD
  action(["action"])
  action --> cli["CLI"]
  action --> http["HTTP"]
  action --> mcp["MCP"]
  cli --> help["help"]
  cli --> docs["docs"]
  cli --> completion["completion"]

The action remains a plain object. The different parts of @cofold/commands decide how to expose it.

One schema

input is the canonical description of the input, and the only statement of the rules.

title:  { type: "string",  minLength: 1, maxLength: 120 }
status: { type: "string",  enum: ["open", "done"], default: "open" }
limit:  { type: "integer", minimum: 1, default: 20 }

The CLI uses it to parse and coerce arguments. HTTP uses it to validate requests. MCP publishes it as the tool's input schema. Documentation gets the same defaults, enums, descriptions, and constraints.

So a rule is enforced the same way whichever road the value took:

notes note add ""                     # title must be 1 to 120 characters
curl -X POST /notes -d '{"title":""}' # 400 title must be 1 to 120 characters
note_add({ status: "later" })         # isError: status must be one of open, done

TypeScript derives the handler input from it too:

input.title  // string
input.status // "open" | "done"
input.limit  // number

There is only one copy of the rules to keep correct.

One action, several surfaces

From the declaration above, @cofold/commands can provide:

SurfaceResult
notes note list -s open -n 5parsed and coerced CLI input
notes note add "Ship it"the slot, the default and the length bound
GET /notes?status=openthe same action over HTTP
POST /notesthe same validation, answered with 400
notes note list --jsonstable machine-readable output
notes note list --quietbare output for scripts
notes note add --helpgenerated usage, options, defaults and enums
notes note <TAB>shell completion
notes docsgenerated Markdown reference
notes skillagent-facing command reference
note_list, note_addMCP tools backed by the same input schemas

Adding a surface does not mean adding another implementation.

surfaces: {
  cli:  { pattern: ["note", "add", ":title"] },
  http: { method: "POST", path: "/notes" },
  mcp: true,
}

If a surface is not declared, the action is not exposed there.

An action may also say what it does and to what: effect is read, add, change or remove, and resource names the kind of thing and the input field that names one. Every surface reads the same two fields: the reference prints them, MCP derives its hints, the terminal asks before a remove, and a client of the manifest draws a list, a create and item actions from them.

Capabilities

Actions can declare what they need instead of constructing dependencies themselves.

The usual case is an authenticated client that half the program needs and nobody wants to build twice.

const registry = createRegistry()
  .provide("config", {
    resolve: () => loadConfig(),
  })
  .provide("api", {
    deps: ["config"],
    resolve: ({ config }) => createClient({
      baseUrl: config.url,
      token: config.token,
    }),
    dispose: (api) => api.close(),
  });

Then:

registry.action({
  id: "case.show",
  needs: ["api"],

  input: {
    id: { type: "string", minLength: 1 },
  },
  required: ["id"],

  surfaces: {
    cli: { pattern: ["case", "show", ":id"] },
    mcp: true,
  },

  run: ({ input, api }) => output(api.get(`/cases/${input.id}`)),
});
flowchart LR
  config["config"] -->|deps| api["api"]
  api -->|needs| run["run"]
  run -.->|dispose| close["api.close"]

Capabilities are resolved before the handler runs, in dependency order, and can depend on other capabilities. Whatever they open is disposed afterwards, whether the handler returned or threw.

Nothing is constructed for an action that does not ask for it, so a command that fails on a bad argument never opens the connection.

This keeps actions declarative without turning the registry into a global bag of objects.

Packages

One package per surface, each with a single entry point (@cofold/mcp has three), except the generated reference, which is this package's ./docs entry point.

PackagePurpose
@cofold/commandsActions, schemas, capabilities, registry. No terminal knowledge.
@cofold/terminalargv parsing, help, completion, output contracts and exit codes
@cofold/mcpExpose actions as MCP tools without an MCP SDK dependency
@cofold/mcp/stdioServe those tools over stdio. Still no SDK, still no dependencies
@cofold/mcp/serverOptional SDK-backed Streamable HTTP, for mounting in an existing server
@cofold/commands/docsGenerate Markdown references for people and agents
@cofold/remoteMaterialise commands from remote manifests or OpenAPI
@cofold/yamlParse a YAML subset into plain data, with source-aware errors
@cofold/configFind the configuration file, and say which one a value came from

There are zero runtime dependencies in every package, including a working MCP server: npm install @cofold/commands @cofold/terminal installs two packages and nothing else. @cofold/mcp/server is the single exception and is opt-in, because Streamable HTTP is worth an SDK where stdio is not. It declares the official MCP SDK as an optional peer, so nothing installs it unless you ask for it.

Validation is optional and uses Standard Schema, so libraries such as Zod, Valibot and ArkType can be used without @cofold/commands depending on any of them.

Try it

pnpm install
pnpm examples

One declaration, three surfaces

node examples/commands/dist/petshop/cli.js pet add Rex -a 3 -b corgi

# see the MCP tools an agent receives
node examples/commands/dist/petshop/cli.js mcp tools

# expose the same actions over HTTP
node examples/commands/dist/petshop/cli.js serve --port 8799 &

curl -X POST localhost:8799/pets \
  -d '{"name":"Ada","age":99}'

A local CLI

node examples/commands/dist/kitchen-sink/cli.js note add "Ship cofold" -t work
node examples/commands/dist/kitchen-sink/cli.js note list

# generate the agent-facing reference
node examples/commands/dist/kitchen-sink/cli.js skill

Commands supplied by a server

node examples/commands/dist/clerver/cli.js serve --port 8787 &

node examples/commands/dist/clerver/cli.js pet list
node examples/commands/dist/clerver/cli.js pet add Rex --species dog --age 3

Turn an OpenAPI document into a CLI

The API does not need to know that @cofold/commands exists.

# any server that answers the document. clerver happens to be one
node examples/commands/dist/clerver/cli.js serve --port 8793 &

node examples/commands/dist/open-cli/cli.js \
  --spec examples/samples/petstore.json \
  pets --limit 2

Examples

The repository includes four working examples under examples/commands:

ExampleShows
petshopOne set of actions exposed through CLI, HTTP and MCP
kitchen-sinkThe local CLI feature set
clerverCommands discovered from a remote server
open-cliBuilding a CLI from an existing OpenAPI document

Commands from a document

Commands already arrive here from two documents this library did not write: a remote manifest and an OpenAPI specification. A YAML or JSON file is the same seam a third time, and it is the one that turns "commands are data" from a claim into a demonstrated property.

document -> ActionDefinition -> registry -> CLI / HTTP / MCP

Everything a document-defined command needs already exists except one thing. run is a function, and a file cannot hold one, so execution is stated as data: named executors and an ordered list of steps.

commands:
  release.deploy:
    summary: Deploy the application

    input:
      env: { type: string, enum: [staging, production], default: production, cli: { short: -e } }

    surfaces:
      cli: { pattern: [release, deploy] }

    run:
      - exec: { command: ./scripts/deploy.sh, args: ["{env}"] }

input is the same map of JSON Schema registry.action takes and surfaces is the same declaration, so the terminal, an HTTP request and an MCP tool call are held to one rule exactly as they are in code.

The front end that reads these is s2cmd, a package of its own. The author of the document gets validation, help, completion, --json, a generated reference, HTTP routes and MCP tools without implementing any of those surfaces, and without writing a handler. Its README documents the executors, the interpolation rules, and why a document that runs a process is not an agent tool by default.

Documentation

The full manual lives in docs/commands/.

Start with:

What is not built yet, and the questions still open, are in ROADMAP.md.

Why

Not because parsing is hard. Parsing is the solved part.

Take any program that has been maintained for a year and count the lines that are about its commands against the lines that are about being a program at all:

  • loading a config file, and a profile or target within it
  • credentials, and never printing them in an error
  • --json that stays stable, --quiet that prints one identifier
  • exit codes a script can switch on
  • reading stdin when something is piped in
  • NO_COLOR, TTY detection, terminal width
  • completion that knows this installation's ids, not just the flag names
  • a reference document that is still true
  • and now MCP tools, for the agent that will drive it

A parser gives you the first third of the first line. Everything after that, every program writes again.

Most CLI libraries also make the command declaration part of the setup process:

program
  .command("note list")
  .option("-s, --status <status>")
  .action(handler);

The description is a string handed to a builder and the behaviour is a closure. After that call returns there is nothing left to ask: no object that knows the command exists, what it takes, what it means, or what it needs. So help drifts, because a usage line is a comment and comments rot. Cross-cutting behaviour has nowhere to live, so --json is written again per action and one of them prints a stray console.log. And nothing can be checked, because there is no registry to check against.

That works well when the terminal is the only consumer.

It stops working the moment the terminal is not the only caller. A typed command, an HTTP request and an MCP tool call are the same call: the same arguments, the same defaults, the same validations. Only the way they arrive is different.

Declared per surface, that agreement is written several times and maintained several times. The day one copy drifts, an agent is shown a rule the request is never held to, and a bound enforced in the terminal is missing from the API.

The callers are no longer only people. An agent has to be told what a command accepts, as a schema, before it can call anything. The information that .action(handler) throws away is exactly the information the agent era needs kept.

@cofold/commands keeps that information alive.

const action = {
  id: "note.list",
  input: { /* ... */ },
  surfaces: { /* ... */ },
  run: handler,
};

The CLI is a view of that action.

So is HTTP.

So is MCP.

The cost is one constraint on handlers: a handler receives a canonical input object and returns a value, rather than reading process.argv and printing. That is the whole discipline, and everything else follows from it, because a handler that never touches the terminal can be run by something that is not a terminal.

@cofold/mcp is the test of that. A complete MCP surface, JSON Schema generation included, with no dependencies, because it had nothing to invent: the actions already knew. @cofold/mcp/stdio is the read loop that speaks it, and it is hand-written for the same reason the YAML subset is - newline-delimited JSON-RPC over two pipes is not worth a web framework.

The command is the data. The interfaces are adapters.

License

Copyright © 2026 Softov.

Licensed under the MIT License.

版本与发布记录

@cofold/commands 共发布 4 个版本,首次发布于 2026/09/23;过去 12 个月发布了 4 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.0latest2026/10/061 个月前
0.2.22026/09/271 个月前
0.2.12026/09/271 个月前
0.2.02026/09/231 个月前

关于 @cofold/commands 的常见问题

@cofold/commands 是什么?

A command is data: the declaration, the registry, input, the argv grammar, coercion and schema, rendered by every surface.

如何安装 @cofold/commands?

在项目目录运行 npm install @cofold/commands,或使用 pnpm add / yarn add / bun add。

@cofold/commands 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cofold/commands 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cofold/commands 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cofold/commands 可以商用吗?

@cofold/commands 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cofold/commands 的下载量有多少?

根据 npm 官方下载统计,@cofold/commands 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,740 次,日均约 91 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cofold/commands 还在维护吗?

最新版本 0.3.0 发布于 2026/10/06(1 个月前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/09/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。