@ultime/cli 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-11-12
4
+
5
+ ### Added
6
+
7
+ - `CliError` / `toErrorShape`:机器可读错误,带 `code` / `path` / `hint` / `details` / `exitCode`。
8
+ - `--json` 作为保留布尔 flag:错误以 `{ "error": { … } }` 只写 stderr。
9
+ - 未知命令给最近候选;单命令 help 缺省时按命令名生成。
10
+
11
+ ### Changed
12
+
13
+ - 退出码语义:0 成功 / 1 命令执行失败 / 2 用法或配置错误;无命令与未知命令从 1 改为 2。
14
+ - 本包不发布 bin、不做命令发现;各产品包自带入口。
15
+
3
16
  ## 0.1.4 - 2026-11-10
4
17
 
5
18
  - 构建改用 `vp pack`,产物同时生成 `dist/index.js` 与 `dist/index.d.ts`。
package/README.md CHANGED
@@ -40,17 +40,48 @@ process.exitCode = runCli(spec, process.argv.slice(2), processContext())
40
40
 
41
41
  | 入口 | 提供 |
42
42
  |---|---|
43
- | `@ultime/cli` | `parseArgs` / `runCli` / `processContext` / `CliContext` / `CommandSpec` / `CliSpec` |
43
+ | `@ultime/cli` | `parseArgs` / `runCli` / `processContext` / `CliError` / `toErrorShape` / `CliContext` / `CliSpec` / `CommandSpec` |
44
44
 
45
- `parseArgs` 只认 `command`、`--flag`、`--key value` / `--key=value`;`-h/--help` 与 `-v/--version` 是保留 flag。
45
+ `parseArgs` 只认 `command`、`--flag`、`--key value` / `--key=value`;`-h/--help`、`-v/--version` 与 `--json` 是保留 flag。
46
+
47
+ ## 错误与退出码
48
+
49
+ 命令用 `CliError` 抛机器可读错误;普通异常退化成 `code: 'ERROR'`:
50
+
51
+ ```ts
52
+ import { CliError } from '@ultime/cli'
53
+
54
+ throw new CliError('缺少 --config', {
55
+ code: 'USAGE',
56
+ path: '--config',
57
+ hint: '补上 --config <path>',
58
+ })
59
+ ```
60
+
61
+ | 情况 | 退出码 |
62
+ |---|---|
63
+ | 版本 / 帮助 | 0 |
64
+ | 命令返回的码 | 原样透传 |
65
+ | 命令抛普通异常 | 1,`code: 'ERROR'` |
66
+ | `CliError` | `exitCode`,默认 2(用法或配置错误) |
67
+ | 无命令、未知命令 | 2 |
68
+
69
+ - 未知命令给最近候选;没有候选时列出可用命令。
70
+ - `--json` 时错误只写 stderr:
71
+
72
+ ```json
73
+ { "error": { "code": "USAGE", "message": "缺少 --config", "path": "--config", "hint": "补上 --config <path>" } }
74
+ ```
75
+
76
+ - 人类文案与 JSON 同源,由 `toErrorShape` 统一转换。
46
77
 
47
78
  AI 入口:[`llms.txt`](./llms.txt)(路由)与 [`SKILLS.md`](./SKILLS.md)(能力表)随包发布。
48
79
 
49
80
  ## 边界
50
81
 
51
82
  - **零领域依赖**:不 import 任何领域包,也不认识具体业务对象。
52
- - **不引第三方解析 / 着色库**:解析用零依赖实现,着色直接用 ANSI;Node 内置的 `util.parseArgs` / `util.styleText` 是后续替换首选,等出现嵌套子命令或补全需求再评估 `commander` 一类工具。
53
- - **不内置命令**:具体命令由调用方包注册,各自发布自己的 bin。
83
+ - **不引第三方解析 / 着色库**:解析用零依赖实现,着色直接用 ANSI。Node 内置 `util.parseArgs` 表达不了自由 `--key value`,暂不切换。
84
+ - **不内置命令、不发 bin**:具体命令由调用方包注册,各自发布自己的 bin;本包不做命令发现与聚合。
54
85
  - **不做 IO 策略**:读盘写盘、配置路径由命令自己决定;本包只提供 `cwd` 与 `out/err`。
55
86
 
56
87
  ## 目录
@@ -59,14 +90,16 @@ AI 入口:[`llms.txt`](./llms.txt)(路由)与 [`SKILLS.md`](./SKILLS.md)
59
90
  src/
60
91
  args.ts 参数解析(纯函数)
61
92
  context.ts 命令上下文(cwd / out / err)
93
+ error.ts 机器可读错误与统一转换
62
94
  run.ts 命令注册、help、未知命令与退出码
63
95
  index.ts 公共出口
64
- tests/ 参数与运行时分发
96
+ tests/ 参数、运行时分发与包内容边界
65
97
  ```
66
98
 
67
99
  ## 版本与兼容
68
100
 
69
- - 当前 `0.1.x`;0.x 阶段 API 仍可能调整。
101
+ - 当前 `0.2.x`;0.x 阶段 API 仍可能调整。
102
+ - 退出码语义:0 成功 / 1 命令执行失败 / 2 用法或配置错误;这是 0.2 起的新约定,升级时按本节目迁移。
70
103
  - 破坏性变更写进 [CHANGELOG](./CHANGELOG.md),并在本节目说明迁移方式。
71
104
  - 发布顺序:本包是基础设施,先于依赖它的包。
72
105
 
package/SKILLS.md CHANGED
@@ -1,23 +1,25 @@
1
1
  # @ultime/cli · 可调用能力
2
2
 
3
3
  > 定位:AI 可调用的通用命令设施。领域命令不在这里。
4
- > 适用版本:0.1.x
5
- > 同步基线:0.1.x(手写;API 变化时同批更新)
4
+ > 适用版本:0.2.x
5
+ > 同步基线:0.2.x(手写;API 变化时同批更新)
6
6
 
7
7
  ---
8
8
 
9
9
  ## 入口
10
10
 
11
11
  ```ts
12
- import { parseArgs, runCli, processContext } from '@ultime/cli'
13
- import type { ParsedArgs, CliContext, CliSpec, CommandSpec } from '@ultime/cli'
12
+ import { parseArgs, runCli, processContext, CliError } from '@ultime/cli'
13
+ import type { ParsedArgs, CliContext, CliSpec, CommandSpec, CliErrorOptions, CliErrorShape } from '@ultime/cli'
14
14
  ```
15
15
 
16
16
  | 能力 | 输入 | 输出 |
17
17
  |---|---|---|
18
18
  | `parseArgs(argv)` | 字符串数组 | `{ command, flags, positional }` |
19
- | `runCli(spec, argv, ctx?)` | `CliSpec` + 参数 | 退出码;版本 / 帮助 / 未知命令 / 异常由它处理 |
19
+ | `runCli(spec, argv, ctx?)` | `CliSpec` + 参数 | 退出码;版本 / 帮助 / 未知命令 / 错误由它处理 |
20
20
  | `processContext()` | — | `{ cwd, out, err }` |
21
+ | `CliError(message, options)` | 文案 + `code` / `path` / `hint` / `details` / `exitCode` | 可被 `runCli` 统一输出的错误 |
22
+ | `toErrorShape(error)` | 任意异常 | `{ code, message, path?, hint?, details? }` |
21
23
 
22
24
  ## `CliSpec`
23
25
 
@@ -32,11 +34,15 @@ import type { ParsedArgs, CliContext, CliSpec, CommandSpec } from '@ultime/cli'
32
34
 
33
35
  ## 错误语义
34
36
 
35
- - 未知命令:`err` 写错误,`out` 写 usage,返回 1;
36
- - 命令抛异常:`err` 写 message,返回 1;
37
+ - 未知命令:`err` 写错误与候选,`out` 写 usage,返回 2;
38
+ - 无命令:`out` 写 usage,返回 2;
39
+ - 普通异常:`err` 写 message,`code: 'ERROR'`,返回 1;
40
+ - `CliError`:带 `code` / `path` / `hint`,返回 `exitCode`(默认 2);
41
+ - `--json`:错误以 `{ "error": { … } }` 只写 stderr;
37
42
  - 版本 / 帮助:返回 0,不执行命令。
38
43
 
39
44
  ## 反例
40
45
 
41
46
  - 把 `--check` 当成 cli 内置 flag:它属于调用方的命令。
42
47
  - 在 cli 里读具体项目配置:IO 与配置语义属于调用方命令。
48
+ - 在 cli 里发现或聚合其他包的 bin:每个包自带入口,本包不发 bin。
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * ---------------------------------------------------------------------------
4
4
  * 只认三种形态:`command`、`--flag`、`--key value` / `--key=value`;
5
5
  * 不引第三方解析库 —— CLI 必须零依赖可跑。
6
+ * `--json` 是通用输出开关,固定按布尔解析,不吞后面的命令名。
6
7
  */
7
8
  interface ParsedArgs {
8
9
  command: string;
@@ -18,17 +19,51 @@ interface CliContext {
18
19
  err: (line: string) => void;
19
20
  }
20
21
  export declare function processContext(): CliContext;
22
+ /**
23
+ * error.ts — 机器可读错误
24
+ * ---------------------------------------------------------------------------
25
+ * 命令抛 `CliError`,`runCli` 统一转成人类文案或 `--json` 结构;
26
+ * 普通异常没有字段信息,退化成 `code: 'ERROR'`,退出码 1。
27
+ */
28
+ interface CliErrorOptions {
29
+ /** 稳定错误码,例如 `USAGE` / `CONFIG`;上层可追加自己的码。 */
30
+ code: string;
31
+ /** 出错位置:字段路径或参数名。 */
32
+ path?: string;
33
+ /** 可执行的修复建议。 */
34
+ hint?: string;
35
+ /** 附加诊断数据,原样进入 JSON 输出。 */
36
+ details?: unknown;
37
+ /** 退出码,默认 2(用法或配置错误)。 */
38
+ exitCode?: number;
39
+ }
40
+ export declare class CliError extends Error {
41
+ readonly code: string;
42
+ readonly path?: string;
43
+ readonly hint?: string;
44
+ readonly details?: unknown;
45
+ readonly exitCode: number;
46
+ constructor(message: string, options: CliErrorOptions);
47
+ }
48
+ interface CliErrorShape {
49
+ code: string;
50
+ message: string;
51
+ path?: string;
52
+ hint?: string;
53
+ details?: unknown;
54
+ }
55
+ export declare function toErrorShape(error: unknown): CliErrorShape;
21
56
  interface CommandSpec {
22
- /** 单条命令的 help;缺省时回落到全局 usage */
57
+ /** 单条命令的 help;缺省时生成 `<name> <command> [options]` */
23
58
  help?: string;
24
59
  run: (args: ParsedArgs, ctx: CliContext) => number;
25
60
  }
26
61
  interface CliSpec {
27
- /** 用于错误与帮助文案,例如 `ultime` */
62
+ /** 用于错误与帮助文案,例如 `my-cli` */
28
63
  name: string;
29
64
  version: string;
30
65
  usage: string;
31
66
  commands: Record<string, CommandSpec>;
32
67
  }
33
68
  export declare function runCli(spec: CliSpec, argv: string[], ctx?: CliContext): number;
34
- export type { CliContext, CliSpec, CommandSpec, ParsedArgs };
69
+ export type { CliContext, CliErrorOptions, CliErrorShape, CliSpec, CommandSpec, ParsedArgs };
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // src/args.ts
2
+ /** 通用布尔 flag:只表达输出形态,不承载业务语义。 */
3
+ const BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json"]);
2
4
  function parseArgs(argv) {
3
5
  const flags = {};
4
6
  const positional = [];
@@ -20,6 +22,10 @@ function parseArgs(argv) {
20
22
  flags[body.slice(0, equal)] = body.slice(equal + 1);
21
23
  continue;
22
24
  }
25
+ if (BOOLEAN_FLAGS.has(body)) {
26
+ flags[body] = true;
27
+ continue;
28
+ }
23
29
  const next = argv[i + 1];
24
30
  if (next && !next.startsWith("-")) {
25
31
  flags[body] = next;
@@ -46,39 +52,116 @@ function processContext() {
46
52
  };
47
53
  }
48
54
 
55
+ // src/error.ts
56
+ var CliError = class extends Error {
57
+ code;
58
+ path;
59
+ hint;
60
+ details;
61
+ exitCode;
62
+ constructor(message, options) {
63
+ super(message);
64
+ this.name = "CliError";
65
+ this.code = options.code;
66
+ this.path = options.path;
67
+ this.hint = options.hint;
68
+ this.details = options.details;
69
+ this.exitCode = options.exitCode ?? 2;
70
+ }
71
+ };
72
+ function toErrorShape(error) {
73
+ if (error instanceof CliError) return {
74
+ code: error.code,
75
+ message: error.message,
76
+ ...error.path === void 0 ? {} : { path: error.path },
77
+ ...error.hint === void 0 ? {} : { hint: error.hint },
78
+ ...error.details === void 0 ? {} : { details: error.details }
79
+ };
80
+ return {
81
+ code: "ERROR",
82
+ message: error instanceof Error ? error.message : String(error)
83
+ };
84
+ }
85
+
49
86
  // src/run.ts
50
87
  /**
51
- * run.ts — 命令注册与退出码
88
+ * run.ts — 命令注册、退出码与错误输出
52
89
  * ---------------------------------------------------------------------------
53
- * 通用设施:只负责版本 / 帮助 / 未知命令 / 异常转退出码;
90
+ * 通用设施:只负责版本 / 帮助 / 未知命令 / 错误转换;
54
91
  * 具体命令由调用方注册,cli 包不出现任何领域命令。
55
92
  */
93
+ /** 未知命令的候选阈值:太小漏提示,太大乱提示。 */
94
+ const SUGGEST_DISTANCE = 2;
95
+ function distance(left, right) {
96
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
97
+ for (let row = 1; row <= left.length; row += 1) {
98
+ const current = [row];
99
+ for (let col = 1; col <= right.length; col += 1) {
100
+ const cost = left[row - 1] === right[col - 1] ? 0 : 1;
101
+ current[col] = Math.min(previous[col] + 1, current[col - 1] + 1, previous[col - 1] + cost);
102
+ }
103
+ previous = current;
104
+ }
105
+ return previous[right.length];
106
+ }
107
+ function nearest(input, candidates) {
108
+ let best;
109
+ for (const name of candidates) {
110
+ const value = distance(input, name);
111
+ if (value > SUGGEST_DISTANCE) continue;
112
+ if (!best || value < best.distance) best = {
113
+ name,
114
+ distance: value
115
+ };
116
+ }
117
+ return best?.name;
118
+ }
119
+ function reportError(ctx, error, json) {
120
+ const shape = toErrorShape(error);
121
+ if (json) {
122
+ ctx.err(JSON.stringify({ error: shape }));
123
+ return;
124
+ }
125
+ ctx.err(`✗ ${shape.message}`);
126
+ if (shape.path) ctx.err(` 位置:${shape.path}`);
127
+ if (shape.hint) ctx.err(` 提示:${shape.hint}`);
128
+ }
129
+ function exitCodeOf(error) {
130
+ return error instanceof CliError ? error.exitCode : 1;
131
+ }
56
132
  function runCli(spec, argv, ctx = processContext()) {
57
133
  const args = parseArgs(argv);
134
+ const json = args.flags.json === true;
58
135
  if (args.flags.version) {
59
136
  ctx.out(spec.version);
60
137
  return 0;
61
138
  }
62
139
  if (args.flags.help) {
63
- ctx.out(args.command && spec.commands[args.command]?.help ? spec.commands[args.command].help : spec.usage);
140
+ const command = args.command ? spec.commands[args.command] : void 0;
141
+ ctx.out(command ? command.help ?? `${spec.name} ${args.command} [options]` : spec.usage);
64
142
  return 0;
65
143
  }
66
144
  if (!args.command) {
67
145
  ctx.out(spec.usage);
68
- return 1;
146
+ return 2;
69
147
  }
70
148
  const command = spec.commands[args.command];
71
149
  if (!command) {
72
- ctx.err(`✗ 未知命令:${args.command}\n`);
73
- ctx.out(spec.usage);
74
- return 1;
150
+ const suggestion = nearest(args.command, Object.keys(spec.commands));
151
+ reportError(ctx, new CliError(`未知命令:${args.command}`, {
152
+ code: "USAGE",
153
+ path: args.command,
154
+ hint: suggestion ? `是否想用 ${suggestion}?` : `可用命令:${Object.keys(spec.commands).join(" / ")}`
155
+ }), json);
156
+ if (!json) ctx.out(spec.usage);
157
+ return 2;
75
158
  }
76
159
  try {
77
160
  return command.run(args, ctx);
78
161
  } catch (error) {
79
- ctx.err(`✗ ${error instanceof Error ? error.message : String(error)}`);
80
- return 1;
162
+ reportError(ctx, error, json);
163
+ return exitCodeOf(error);
81
164
  }
82
165
  }
83
166
 
84
- export { parseArgs, processContext, runCli };
167
+ export { CliError, parseArgs, processContext, runCli, toErrorShape };
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ultime/cli · AI 索引
2
2
 
3
- 适用版本:0.1.x
3
+ 适用版本:0.2.x
4
4
 
5
5
  ## 去哪找
6
6
 
@@ -9,8 +9,9 @@
9
9
 
10
10
  ## 机器入口
11
11
 
12
- - 公共出口:`@ultime/cli` 的 `parseArgs` / `runCli` / `processContext` 与类型。
12
+ - 公共出口:`@ultime/cli` 的 `parseArgs` / `runCli` / `processContext` / `CliError` / `toErrorShape` 与类型。
13
13
 
14
14
  ## 边界
15
15
 
16
16
  - 通用设施,不含任何领域命令;读盘写盘归调用方。
17
+ - 不发 bin、不做命令发现;每个产品包发布自己的入口。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultime/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Ultime · cli:通用命令行设施(参数 / 命令注册 / 上下文 / 退出码),零领域依赖",
5
5
  "keywords": [
6
6
  "argument-parser",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "scripts": {
43
43
  "build": "vp pack",
44
- "check": "vp check && checks run && pnpm cycles",
45
- "checks": "checks run",
44
+ "check": "vp check && ultime-checks run && pnpm cycles",
45
+ "checks": "ultime-checks run",
46
46
  "cycles": "vp lint --import-plugin --deny=import/no-cycle src",
47
47
  "test": "vp test",
48
48
  "size": "size-limit --json",
@@ -53,7 +53,7 @@
53
53
  "@arethetypeswrong/core": "^0.18.5",
54
54
  "@size-limit/preset-small-lib": "^14.3.0",
55
55
  "@types/node": "^22.20.5",
56
- "@ultime/checks": "^0.2.0",
56
+ "@ultime/checks": "^0.3.0",
57
57
  "publint": "^0.3.25",
58
58
  "size-limit": "^14.3.0",
59
59
  "typescript": "^5.9.3",