@agenzo/merchant-cli 0.4.1 → 0.4.2

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/README.md CHANGED
@@ -1,197 +1,197 @@
1
1
  # @agenzo/merchant-cli
2
2
 
3
- > **商户履约产品线 CLI** — binary `agenzo-merchant-cli`,落地于 monorepo `agenzo-cli/apps/merchant-cli`。
4
- > 覆盖两个 noun group:能力发现(`services`)与叫车业务闭环(`ride-elife`),共 7 条命令。
3
+ > **Merchant-fulfillment product-line CLI** — binary `agenzo-merchant-cli`, lives in the monorepo `agenzo-cli/apps/merchant-cli`.
4
+ > Covers two noun groups: capability discovery (`services`) and the ride-hailing business loop (`ride-elife`), 7 commands in total.
5
5
 
6
6
  ---
7
7
 
8
- ## 项目简介
8
+ ## Overview
9
9
 
10
- `agenzo-merchant-cli` 是 Agent 接入 Agenzo 商户履约能力的命令行入口。本期上线两个 noun group:
10
+ `agenzo-merchant-cli` is the command-line entry point for Agents to access Agenzo's merchant-fulfillment capabilities. This iteration ships two noun groups:
11
11
 
12
- - **`services`**(能力发现):让 Agent 发现可用的商户履约能力(该调哪个 noun/verb、调用流程)。本期数据源为 **CLI 内置注册表**(后端 `/services` discovery endpoint 属 pending impl)。
13
- - **`ride-elife`**(叫车,唯一在线 provider):报价 → 下单 → 查询/轮询 → 取消 → 列单的完整闭环。
12
+ - **`services`** (capability discovery): lets an Agent discover available merchant-fulfillment capabilities (which noun/verb to call, and the call flow). The data source this iteration is the **CLI-bundled registry** (the backend `/services` discovery endpoint is pending impl).
13
+ - **`ride-elife`** (ride hailing, the only online provider): the full loop of quote → book → query/poll → cancel → list-orders.
14
14
 
15
- 所有通用基础设施(HTTP 客户端、输出渲染、错误体系、退出码、版本协商、配置)均复用 `@agenzo/cli-core`,app 内只保留命令处理与商户域专属逻辑(ride body 组装、NDJSON watch、verb-schema help、services 注册表、幂等键策略)。
15
+ All shared infrastructure (HTTP client, output rendering, error system, exit codes, version negotiation, config) is reused from `@agenzo/cli-core`; the app only keeps command handling and merchant-domain-specific logic (ride body assembly, NDJSON watch, verb-schema help, services registry, idempotency-key policy).
16
16
 
17
- > 本 CLI 与 `agenzo-admin-cli` / `agenzo-token-cli` 结构、命名、构建配置保持一致(4-CLI consistency)。
17
+ > This CLI keeps its structure, naming, and build configuration consistent with `agenzo-admin-cli` / `agenzo-token-cli` (4-CLI consistency).
18
18
 
19
- ## 功能特性
19
+ ## Features
20
20
 
21
- | Noun | Verb | 类型 | HTTP | 说明 |
21
+ | Noun | Verb | Type | HTTP | Description |
22
22
  |---|---|---|---|---|
23
- | `services` | `list` | Read | —(内置注册表) | 列出可用商户履约能力 |
24
- | `services` | `get <service-id>` | Read | —(内置注册表) | 查看单个能力完整元数据(命中失败 → `SERVICE_NOT_FOUND`) |
25
- | `ride-elife` | `quote` | Read | `POST /ride/quote` | 两点间报价(`vehicle_classes[]` + meet-and-greet) |
26
- | `ride-elife` | `book` | Write/Y | `POST /ride/book` | 按 quote 下单(monthly_settlement-aware,见下) |
27
- | `ride-elife` | `get` | Read | `GET /ride/<id>/status` | 查询订单状态;`--watch` 输出 NDJSON 轮询流 |
28
- | `ride-elife` | `cancel` | Write/Y | `POST /ride/<id>/cancel` | 取消订单(无 body,可能产生费用) |
29
- | `ride-elife` | `list-orders` | Read | `GET /ride/orders` | 列出已下单的行程(分页 + 过滤) |
23
+ | `services` | `list` | Read | — (bundled registry) | List available merchant-fulfillment capabilities |
24
+ | `services` | `get <service-id>` | Read | — (bundled registry) | View the full metadata of a single capability (miss → `SERVICE_NOT_FOUND`) |
25
+ | `ride-elife` | `quote` | Read | `POST /ride/quote` | Point-to-point quote (`vehicle_classes[]` + meet-and-greet) |
26
+ | `ride-elife` | `book` | Write/Y | `POST /ride/book` | Book against a quote (monthly_settlement-aware, see below) |
27
+ | `ride-elife` | `get` | Read | `GET /ride/<id>/status` | Query order status; `--watch` emits an NDJSON polling stream |
28
+ | `ride-elife` | `cancel` | Write/Y | `POST /ride/<id>/cancel` | Cancel an order (no body, may incur a fee) |
29
+ | `ride-elife` | `list-orders` | Read | `GET /ride/orders` | List booked trips (pagination + filtering) |
30
30
 
31
- > 写命令(`book` / `cancel`)受幂等键规则约束(见 [幂等键](#幂等键-idempotency-key))。
32
- > 未实现:`ride-elife track`(pending impl,后端无位置流接口)——`get --watch` 已覆盖订单状态轮询。
31
+ > Write commands (`book` / `cancel`) are subject to the idempotency-key rules (see [Idempotency-Key](#idempotency-key)).
32
+ > Not implemented: `ride-elife track` (pending impl, the backend has no location-stream interface) — `get --watch` already covers order-status polling.
33
33
 
34
- ## 技术栈
34
+ ## Tech Stack
35
35
 
36
- - TypeScript(ESM) + [commander@14](https://github.com/tj/commander.js) + [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js)
37
- - 构建:[tsup](https://tsup.egoist.dev/)(esm / node18 / dts,banner `#!/usr/bin/env node`)
38
- - 测试:[vitest](https://vitest.dev/) + [fast-check](https://fast-check.dev/)(PBT)
39
- - 共享底包:`@agenzo/cli-core`(唯一横向依赖;**禁止** import 其它 app)
36
+ - TypeScript (ESM) + [commander@14](https://github.com/tj/commander.js) + [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js)
37
+ - Build: [tsup](https://tsup.egoist.dev/) (esm / node18 / dts, banner `#!/usr/bin/env node`)
38
+ - Testing: [vitest](https://vitest.dev/) + [fast-check](https://fast-check.dev/) (PBT)
39
+ - Shared base package: `@agenzo/cli-core` (the only horizontal dependency; importing other apps is **forbidden**)
40
40
 
41
- ## 快速开始
41
+ ## Quick Start
42
42
 
43
- ### 环境要求
43
+ ### Requirements
44
44
 
45
45
  - Node.js 18+
46
- - monorepo 已 `npm install`
46
+ - `npm install` has been run in the monorepo
47
47
 
48
- ### 构建
48
+ ### Build
49
49
 
50
50
  ```bash
51
- # 在 monorepo 根目录
52
- npm run build -w @agenzo/cli-core # 必须先构建共享底包
53
- npm run build -w @agenzo/merchant-cli # tsup 产出 dist/index.js(= bin agenzo-merchant-cli)
51
+ # in the monorepo root
52
+ npm run build -w @agenzo/cli-core # must build the shared base package first
53
+ npm run build -w @agenzo/merchant-cli # tsup produces dist/index.js (= bin agenzo-merchant-cli)
54
54
  agenzo-merchant-cli --version
55
55
  ```
56
56
 
57
- ### 认证
57
+ ### Authentication
58
58
 
59
- 运行面 CLI,使用 **API Key**(`--api-key`,作为 `X-Api-Key` 头逐命令携带):
59
+ A runtime-plane CLI that uses an **API Key** (`--api-key`, carried per command as the `X-Api-Key` header):
60
60
 
61
- - API Key 由 `agenzo-admin-cli keys create --scope merchant` 签发,需含 `merchant` scope。
62
- - 缺省 `--api-key` 时交互索取(password 输入)。
63
- - 无 Bearer Token / AuthService / keystore——与 `agenzo-token-cli` 一致。
61
+ - The API Key is issued by `agenzo-admin-cli keys create --scope merchant` and must include the `merchant` scope.
62
+ - When `--api-key` is omitted, it is requested interactively (password input).
63
+ - No Bearer Token / AuthService / keystore — consistent with `agenzo-token-cli`.
64
64
 
65
- ### 端到端示例
65
+ ### End-to-end example
66
66
 
67
67
  ```bash
68
- # 1. 发现能力
68
+ # 1. Discover capabilities
69
69
  agenzo-merchant-cli services list
70
70
  agenzo-merchant-cli services get ride-elife
71
71
 
72
- # 2. 报价(产出 quote_id)
72
+ # 2. Quote (produces quote_id)
73
73
  agenzo-merchant-cli ride-elife quote --api-key "$API_KEY" \
74
74
  --pickup-lat 37.7937 --pickup-lng -122.3956 --pickup-name "1 Market St" \
75
75
  --dropoff-lat 37.6213 --dropoff-lng -122.3790 --dropoff-name "SFO Airport" \
76
76
  --pickup-time now
77
77
 
78
- # 3. 下单(写操作:--yes 自动确认 + 必须提供 --idempotency-key)
78
+ # 3. Book (write op: --yes auto-confirms + --idempotency-key must be provided)
79
79
  agenzo-merchant-cli ride-elife book --api-key "$API_KEY" --yes \
80
80
  --quote-id "$QUOTE_ID" --vehicle-class Sedan --price-amount 42.50 \
81
81
  --passenger-name "Alice" --passenger-phone "+14155551234" \
82
82
  --idempotency-key "book-$(date +%s)"
83
83
 
84
- # 4. 查询 / 轮询(--watch 输出逐行 NDJSON)
84
+ # 4. Query / poll (--watch emits line-by-line NDJSON)
85
85
  agenzo-merchant-cli ride-elife get --api-key "$API_KEY" --order-id "$RIDE_ID"
86
86
  agenzo-merchant-cli ride-elife get --api-key "$API_KEY" --order-id "$RIDE_ID" \
87
87
  --watch --watch-interval 5 --watch-timeout 600
88
88
 
89
- # 5. 列单 / 取消
89
+ # 5. List orders / cancel
90
90
  agenzo-merchant-cli ride-elife list-orders --api-key "$API_KEY" --status Pending --page 1 --page-size 20
91
91
  agenzo-merchant-cli ride-elife cancel --api-key "$API_KEY" --yes \
92
92
  --order-id "$RIDE_ID" --idempotency-key "cancel-$(date +%s)"
93
93
  ```
94
94
 
95
- ## 全局 flag
95
+ ## Global flags
96
96
 
97
- | Flag | 默认 | 说明 |
97
+ | Flag | Default | Description |
98
98
  |---|---|---|
99
- | `--format <json\|table>` | **`json`** | 输出格式。merchant-cli 是 agent-first 入口,**默认 `json`**(刻意偏离 cli-core 的 `table` 默认,D2);亦读环境变量 `AGENZO_FORMAT` |
100
- | `--api-key <key>` | —(交互索取) | API Key,作为 `X-Api-Key` 头逐命令携带 |
101
- | `--yes` | `false` | 跳过交互式确认(供 CI / Agent);写命令在 `--yes` 且缺幂等键时为硬错误 |
102
- | `--verbose` | `false` | 详细日志(写 stderr) |
99
+ | `--format <json\|table>` | **`json`** | Output format. merchant-cli is an agent-first entry point, so it **defaults to `json`** (a deliberate deviation from cli-core's `table` default, D2); also reads the `AGENZO_FORMAT` environment variable |
100
+ | `--api-key <key>` | — (requested interactively) | API Key, carried per command as the `X-Api-Key` header |
101
+ | `--yes` | `false` | Skip interactive confirmation (for CI / Agent); for write commands, `--yes` with a missing idempotency key is a hard error |
102
+ | `--verbose` | `false` | Verbose logging (written to stderr) |
103
103
 
104
- ## 输出与错误契约
104
+ ## Output and error contract
105
105
 
106
- - **`--format json`(非 watch 命令)**:stdout 仅含**单个合法 JSON**(业务 payload + `profile`/`endpoint` 信封,endpoint 仅 host 不含 path);所有状态/进度/spinner 行写 stderr,且在 json 模式下静音。
107
- - **`--format table`**:业务输出走 stdout,状态行/spinner 走 stderr。
108
- - **`get --watch`**:stdout 为逐行 NDJSON 流,**不套** profile/endpoint 信封;轮询命中终态集合(`At destination` / `Cancelled` / `Rejected` / `Customer no show` / `Driver no show`,大小写敏感)或超时停止;超时末行为 `{ "watch_status": "timeout", ... }`。
109
- - **错误信封**(cli-core §8):`json` → `{ "error": { "code", "code_num", "message", "request_id"? } }`;`table` → `✗ [<code_num>] <message>`。
106
+ - **`--format json` (non-watch commands)**: stdout contains only a **single valid JSON** (business payload + `profile`/`endpoint` envelope, where endpoint is host-only without a path); all status/progress/spinner lines go to stderr and are silenced in json mode.
107
+ - **`--format table`**: business output goes to stdout, status lines/spinner go to stderr.
108
+ - **`get --watch`**: stdout is a line-by-line NDJSON stream, **not wrapped** in the profile/endpoint envelope; polling stops when it hits the terminal-status set (`At destination` / `Cancelled` / `Rejected` / `Customer no show` / `Driver no show`, case-sensitive) or times out; on timeout the final line is `{ "watch_status": "timeout", ... }`.
109
+ - **Error envelope** (cli-core §8): `json` → `{ "error": { "code", "code_num", "message", "request_id"? } }`; `table` → `✗ [<code_num>] <message>`.
110
110
 
111
- 退出码(由 cli-core `exitCodeFor` 映射):
111
+ Exit codes (mapped by cli-core's `exitCodeFor`):
112
112
 
113
- | 退出码 | 含义 | 代表错误码 |
113
+ | Exit code | Meaning | Representative error codes |
114
114
  |---|---|---|
115
- | `0` | 成功 | — |
116
- | `1` | 业务 / 参数 | `PARAM_INVALID`(2101) · `PARAM_IDEMPOTENCY_KEY_REQUIRED`(2102) · `SERVICE_NOT_FOUND`(4101) · `VEHICLE_UNAVAILABLE`(4201) · `QUOTE_EXPIRED`(4202) · `BOOKING_FAILED`(4203) · `CANCELLATION_NOT_ALLOWED`(4204) · `BILLING_MODE_MISMATCH`(3001) · `ACCOUNT_*`(31xx) · `PAYMENT_ORDER_*`(32xx) |
117
- | `2` | 需升级 | `UPGRADE_REQUIRED`(9008) |
118
- | `3` | 认证 / 无效 key | `KEY_INVALID`(1101) · `KEY_SCOPE_DENIED`(1102) |
119
- | `4` | 网络 / 5xx | `RATE_LIMITED`(5001) · `UPSTREAM_ERROR`(5101) · `INTERNAL_ERROR`(5201) |
120
- | `5` | 用户取消 | `CLIENT_ABORTED`(9007) · SIGINT |
115
+ | `0` | Success | — |
116
+ | `1` | Business / param | `PARAM_INVALID`(2101) · `PARAM_IDEMPOTENCY_KEY_REQUIRED`(2102) · `SERVICE_NOT_FOUND`(4101) · `VEHICLE_UNAVAILABLE`(4201) · `QUOTE_EXPIRED`(4202) · `BOOKING_FAILED`(4203) · `CANCELLATION_NOT_ALLOWED`(4204) · `BILLING_MODE_MISMATCH`(3001) · `ACCOUNT_*`(31xx) · `PAYMENT_ORDER_*`(32xx) |
117
+ | `2` | Upgrade required | `UPGRADE_REQUIRED`(9008) |
118
+ | `3` | Auth / invalid key | `KEY_INVALID`(1101) · `KEY_SCOPE_DENIED`(1102) |
119
+ | `4` | Network / 5xx | `RATE_LIMITED`(5001) · `UPSTREAM_ERROR`(5101) · `INTERNAL_ERROR`(5201) |
120
+ | `5` | User cancel | `CLIENT_ABORTED`(9007) · SIGINT |
121
121
 
122
- > api-key 鉴权映射:HTTP 401 → `KEY_INVALID`、403 → `KEY_SCOPE_DENIED`。后端返回的已知 §8 字符串码(如 `QUOTE_EXPIRED`)优先保真,否则回退 HTTP-status 映射。
122
+ > api-key auth mapping: HTTP 401 → `KEY_INVALID`, 403 → `KEY_SCOPE_DENIED`. A known §8 string code returned by the backend (e.g. `QUOTE_EXPIRED`) takes priority and is preserved; otherwise it falls back to the HTTP-status mapping.
123
123
 
124
- ## 幂等键 (Idempotency-Key)
124
+ ## Idempotency-Key
125
125
 
126
- 写命令 `ride-elife book` / `cancel` 接受 `--idempotency-key`:
126
+ The write commands `ride-elife book` / `cancel` accept `--idempotency-key`:
127
127
 
128
- - 格式:`[A-Za-z0-9_-]{1,128}`。
129
- - **CLI 永不自动生成**:`--yes` 且缺键 → 硬错误 `PARAM_IDEMPOTENCY_KEY_REQUIRED`(exit 1),不发起请求;非 `--yes` 缺键 → 交互索取。
130
- - 作为 `Idempotency-Key` HTTP **头**发送,**绝不进 body**。
131
- - 同一逻辑请求重试用同一键;新请求用新键。
128
+ - Format: `[A-Za-z0-9_-]{1,128}`.
129
+ - **The CLI never auto-generates one**: `--yes` with a missing key → hard error `PARAM_IDEMPOTENCY_KEY_REQUIRED` (exit 1), and no request is sent; a missing key without `--yes` → requested interactively.
130
+ - Sent as the `Idempotency-Key` HTTP **header**, **never in the body**.
131
+ - Reuse the same key to retry the same logical request; use a new key for a new request.
132
132
 
133
- ## book 的计费模型(monthly_settlement-aware)
133
+ ## Billing model for book (monthly_settlement-aware)
134
134
 
135
- `ride-elife book` **不接受** `--payment-method-id` 或任何卡信息——商户域不持有支付句柄。funding 由后端按 Developer 的 `billing_mode` 决定:
135
+ `ride-elife book` **does not accept** `--payment-method-id` or any card information — the merchant domain holds no payment handle. Funding is decided by the backend according to the Developer's `billing_mode`:
136
136
 
137
- - **monthly_settlement**:从月结账户扣减,无支付句柄;响应 `payment_status=ON_ACCOUNT`,含 `billing_entry_id`。
138
- - **pay_per_call**:可选透传 `--payment-order-id`(payment-cli 已扣款的 PAID 订单号);响应回显 `payment_order_id`。
137
+ - **monthly_settlement**: deducted from the monthly-settlement account, no payment handle; the response has `payment_status=ON_ACCOUNT` and includes `billing_entry_id`.
138
+ - **pay_per_call**: optionally pass through `--payment-order-id` (a PAID order number charged separately, out-of-band); the response echoes `payment_order_id`.
139
139
 
140
- 请求 body 至多含可选 `payment_order_id`。
140
+ The request body contains at most an optional `payment_order_id`.
141
141
 
142
- ## profile / host 模型
142
+ ## profile / host model
143
143
 
144
- merchant-cli **没有任何 host / config 命令**,不治理环境。它复用 `@agenzo/cli-core` 的 `ConfigManager` 默认配置(即 `agenzo-admin-cli` 统一治理的环境,默认落盘 `~/.agenzo-admin-cli/`):
144
+ merchant-cli **has no host / config commands** and does not govern environments. It reuses `@agenzo/cli-core`'s `ConfigManager` default configuration (i.e. the environment governed centrally by `agenzo-admin-cli`, persisted by default to `~/.agenzo-admin-cli/`):
145
145
 
146
- - 为 `ApiClient` 提供 `baseUrl`(host + `/api/merchant/v1`)。
147
- - 为 json 输出的 `profile` / `endpoint` 信封提供环境名 + host。
146
+ - Provides the `baseUrl` for `ApiClient` (host + `/api/merchant/v1`).
147
+ - Provides the environment name + host for the `profile` / `endpoint` envelope in json output.
148
148
 
149
- **环境治理(设置 host / 切换环境)归 `agenzo-admin-cli`**(`agenzo-admin-cli config set` / `set-host`)——与 `agenzo-token-cli` 一致。
149
+ **Environment governance (setting the host / switching environments) belongs to `agenzo-admin-cli`** (`agenzo-admin-cli config set` / `set-host`) — consistent with `agenzo-token-cli`.
150
150
 
151
- ## 机器可读的 verb schema
151
+ ## Machine-readable verb schema
152
152
 
153
- 每个 `ride-elife` verb 支持 `--help --format json`,输出该 verb 的机器可读 schema(`cli` / `noun` / `verb` / `description` / `flags` / `response` / `example`,部分含 `error_recovery` / `polling`):
153
+ Every `ride-elife` verb supports `--help --format json`, emitting that verb's machine-readable schema (`cli` / `noun` / `verb` / `description` / `flags` / `response` / `example`, some with `error_recovery` / `polling`):
154
154
 
155
155
  ```bash
156
156
  agenzo-merchant-cli ride-elife quote --help --format json
157
157
  agenzo-merchant-cli ride-elife book --help --format json
158
158
  ```
159
159
 
160
- ## 开发
160
+ ## Development
161
161
 
162
162
  ```bash
163
- npm run build -w @agenzo/merchant-cli # tsup 构建
164
- npm run test -w @agenzo/merchant-cli # vitest(含 fast-check PBT)
163
+ npm run build -w @agenzo/merchant-cli # tsup build
164
+ npm run test -w @agenzo/merchant-cli # vitest (incl. fast-check PBT)
165
165
  ```
166
166
 
167
- 源码结构:
167
+ Source structure:
168
168
 
169
169
  ```text
170
170
  apps/merchant-cli/
171
171
  src/
172
- index.ts # commander 装配 + 顶层 reportError + SIGINT
173
- services/{list,get}.ts # 能力发现(本期:CLI 内置注册表)
174
- services/registry.ts # 内置静态注册表(商户域)
172
+ index.ts # commander assembly + top-level reportError + SIGINT
173
+ services/{list,get}.ts # capability discovery (this iteration: CLI-bundled registry)
174
+ services/registry.ts # bundled static registry (merchant domain)
175
175
  ride-elife/{quote,book,get,cancel,list-orders}.ts
176
- ride-elife/watch.ts # NDJSON 轮询引擎(商户域)
177
- verb-schema.ts # --help --format json 的 verb schema(商户域)
178
- idempotency.ts # 幂等键 resolve / normalize(商户域)
179
- types/api.ts # ride/service 响应类型(仅 merchant 消费,不提升 cli-core)
180
- docs/ # 测试设计文档(见下)
181
- tests/ # vitest 单测 + PBT
176
+ ride-elife/watch.ts # NDJSON polling engine (merchant domain)
177
+ verb-schema.ts # verb schema for --help --format json (merchant domain)
178
+ idempotency.ts # idempotency key resolve / normalize (merchant domain)
179
+ types/api.ts # ride/service response types (consumed by merchant only, not promoted to cli-core)
180
+ docs/ # test design docs (see below)
181
+ tests/ # vitest unit tests + PBT
182
182
  ```
183
183
 
184
- ## 文档索引
184
+ ## Documentation index
185
185
 
186
- - [测试设计文档(命令矩阵 v1)](docs/test-design-command-matrix-v1.md) — 7 条命令 × 需求/Property 覆盖矩阵、L1–L4 分层测试策略、L1 单元 / L3 命令级用例表、L4 手动可执行冒烟用例。
187
- - 架构与规范:`architecture-upgrade/v1/`(cli-standard / cli-design §4+§8 / cli-guide)。
188
- - monorepo 总览:[`agenzo-cli/README.md`](../../README.md)。
186
+ - [Test design doc (command matrix v1)](docs/test-design-command-matrix-v1.md) — a 7-command × requirement/Property coverage matrix, the L1–L4 layered testing strategy, L1 unit / L3 command-level case tables, and L4 manually executable smoke cases.
187
+ - Architecture and standards: `architecture-upgrade/v1/` (cli-standard / cli-design §4+§8 / cli-guide).
188
+ - monorepo overview: [`agenzo-cli/README.md`](../../README.md).
189
189
 
190
- ## 更新日志
190
+ ## Changelog
191
191
 
192
192
  ### 2026-06
193
193
 
194
- - 于 monorepo `agenzo-cli/apps/merchant-cli` 落地 merchant-cli(按 cli-design §4 重写,复用 `@agenzo/cli-core`)。
195
- - 上线 `services`(list / get)+ `ride-elife`(quote / book / get [+`--watch`] / cancel / list-orders)共 7 条命令。
196
- - 默认输出 `json`(agent-first);API Key 鉴权(`X-Api-Key`);写命令强制 `--idempotency-key`。
197
- - 新增测试设计文档 `docs/test-design-command-matrix-v1.md`。
194
+ - Landed merchant-cli in the monorepo `agenzo-cli/apps/merchant-cli` (rewritten per cli-design §4, reusing `@agenzo/cli-core`).
195
+ - Shipped `services` (list / get) + `ride-elife` (quote / book / get [+`--watch`] / cancel / list-orders), 7 commands in total.
196
+ - Output defaults to `json` (agent-first); API Key auth (`X-Api-Key`); write commands require `--idempotency-key`.
197
+ - Added the test design doc `docs/test-design-command-matrix-v1.md`.
package/dist/index.js CHANGED
@@ -270,10 +270,6 @@ var ApiClient = class {
270
270
  const headers = {
271
271
  "User-Agent": `agenzo-admin-cli/${getCurrentVersion()}`
272
272
  };
273
- const traceparent = process.env.TRACEPARENT;
274
- if (traceparent) {
275
- headers["traceparent"] = traceparent;
276
- }
277
273
  if (auth.type === "bearer") {
278
274
  headers["Authorization"] = `Bearer ${auth.token}`;
279
275
  } else if (auth.type === "api-key") {
@@ -564,6 +560,29 @@ var STATUS_ICONS = {
564
560
  warning: "\u26A0",
565
561
  loading: "\u280B"
566
562
  };
563
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
564
+ function createSpinner(message, intervalMs = 60) {
565
+ let frameIdx = 0;
566
+ let currentMessage = message;
567
+ const render2 = () => {
568
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[frameIdx]} ${currentMessage}`);
569
+ frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
570
+ };
571
+ render2();
572
+ const timer = setInterval(render2, intervalMs);
573
+ return {
574
+ update(msg) {
575
+ currentMessage = msg;
576
+ },
577
+ stop(type, finalMessage) {
578
+ clearInterval(timer);
579
+ process.stdout.write("\r\x1B[K");
580
+ if (type && finalMessage) {
581
+ console.log(Formatter.status(type, finalMessage));
582
+ }
583
+ }
584
+ };
585
+ }
567
586
  var Formatter = class _Formatter {
568
587
  /** Get display width of a string (CJK characters count as 2) */
569
588
  static displayWidth(str) {
@@ -673,12 +692,6 @@ function render(result, opts) {
673
692
  process.stdout.write(`${result.text()}
674
693
  `);
675
694
  }
676
- function notify(format, type, message) {
677
- if (format === "json") {
678
- return;
679
- }
680
- console.error(Formatter.status(type, message));
681
- }
682
695
  async function renderWithContext(result, opts, configManager) {
683
696
  if (opts.format !== "json") {
684
697
  render(result, opts);
@@ -696,17 +709,11 @@ async function renderWithContext(result, opts, configManager) {
696
709
  render({ ...result, data: payload }, opts);
697
710
  }
698
711
  var PromptEngine = class {
699
- /** Return flagValue if provided, else env fallback, else prompt interactively */
712
+ /** Return flagValue directly if provided, otherwise prompt interactively */
700
713
  static async resolveInput(flagValue, config) {
701
714
  if (flagValue !== void 0) {
702
715
  return flagValue;
703
716
  }
704
- if (config.envVar) {
705
- const envValue = process.env[config.envVar];
706
- if (envValue !== void 0 && envValue !== "") {
707
- return envValue;
708
- }
709
- }
710
717
  if (config.type === "password") {
711
718
  return password({ message: config.message, mask: "*" });
712
719
  }
@@ -1115,8 +1122,7 @@ function registerQuoteCommand(parent, deps) {
1115
1122
  const format = resolveFormat(opts.format);
1116
1123
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1117
1124
  message: "API Key:",
1118
- type: "password",
1119
- envVar: "AGENZO_API_KEY"
1125
+ type: "password"
1120
1126
  });
1121
1127
  const body = {
1122
1128
  pickup: {
@@ -1145,12 +1151,13 @@ function registerQuoteCommand(parent, deps) {
1145
1151
  if (opts.passengerEmail !== void 0) {
1146
1152
  body.passenger_email = opts.passengerEmail;
1147
1153
  }
1148
- notify(format, "loading", "Fetching quotes...");
1154
+ const spinner = format === "json" ? null : createSpinner("Fetching quotes...");
1149
1155
  const result = await deps.apiClient.post(
1150
1156
  "/ride/quote",
1151
1157
  { type: "api-key", key: apiKey },
1152
1158
  body
1153
1159
  );
1160
+ spinner?.stop();
1154
1161
  if (!result.success) {
1155
1162
  throw CliError.fromApi(result, { auth: "api-key" });
1156
1163
  }
@@ -1244,8 +1251,7 @@ function registerBookCommand(parent, deps) {
1244
1251
  const isYes = Boolean(opts.yes);
1245
1252
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1246
1253
  message: "API Key:",
1247
- type: "password",
1248
- envVar: "AGENZO_API_KEY"
1254
+ type: "password"
1249
1255
  });
1250
1256
  const quoteId = need2(opts.quoteId, "quote-id");
1251
1257
  const body = {
@@ -1318,13 +1324,14 @@ function registerBookCommand(parent, deps) {
1318
1324
  yes: isYes,
1319
1325
  commandPath: "ride-elife book"
1320
1326
  });
1321
- notify(format, "loading", "Booking ride...");
1327
+ const spinner = format === "json" ? null : createSpinner("Booking ride...");
1322
1328
  const result = await deps.apiClient.post(
1323
1329
  "/ride/book",
1324
1330
  { type: "api-key", key: apiKey },
1325
1331
  body,
1326
1332
  { "Idempotency-Key": idempotencyKey }
1327
1333
  );
1334
+ spinner?.stop();
1328
1335
  if (!result.success) {
1329
1336
  throw CliError.fromApi(result, { auth: "api-key" });
1330
1337
  }
@@ -1397,8 +1404,7 @@ function registerRideGetCommand(parent, deps) {
1397
1404
  const format = resolveFormat(opts.format);
1398
1405
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1399
1406
  message: "API Key:",
1400
- type: "password",
1401
- envVar: "AGENZO_API_KEY"
1407
+ type: "password"
1402
1408
  });
1403
1409
  const orderId = need3(opts.orderId, "order-id");
1404
1410
  if (opts.watch) {
@@ -1414,11 +1420,12 @@ function registerRideGetCommand(parent, deps) {
1414
1420
  });
1415
1421
  return;
1416
1422
  }
1417
- notify(format, "loading", "Fetching ride status...");
1423
+ const spinner = format === "json" ? null : createSpinner("Fetching ride status...");
1418
1424
  const result = await deps.apiClient.get(
1419
1425
  `/ride/${encodeURIComponent(orderId)}/status`,
1420
1426
  { type: "api-key", key: apiKey }
1421
1427
  );
1428
+ spinner?.stop();
1422
1429
  if (!result.success) {
1423
1430
  throw CliError.fromApi(result, { auth: "api-key" });
1424
1431
  }
@@ -1467,8 +1474,7 @@ function registerCancelCommand(parent, deps) {
1467
1474
  const isYes = Boolean(opts.yes);
1468
1475
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1469
1476
  message: "API Key:",
1470
- type: "password",
1471
- envVar: "AGENZO_API_KEY"
1477
+ type: "password"
1472
1478
  });
1473
1479
  const orderId = need4(opts.orderId, "order-id");
1474
1480
  if (!isYes) {
@@ -1484,13 +1490,14 @@ function registerCancelCommand(parent, deps) {
1484
1490
  yes: isYes,
1485
1491
  commandPath: "ride-elife cancel"
1486
1492
  });
1487
- notify(format, "loading", "Cancelling ride...");
1493
+ const spinner = format === "json" ? null : createSpinner("Cancelling ride...");
1488
1494
  const result = await deps.apiClient.post(
1489
1495
  `/ride/${encodeURIComponent(orderId)}/cancel`,
1490
1496
  { type: "api-key", key: apiKey },
1491
1497
  void 0,
1492
1498
  { "Idempotency-Key": idempotencyKey }
1493
1499
  );
1500
+ spinner?.stop();
1494
1501
  if (!result.success) {
1495
1502
  throw CliError.fromApi(result, { auth: "api-key" });
1496
1503
  }
@@ -1545,8 +1552,7 @@ function registerListOrdersCommand(parent, deps) {
1545
1552
  const format = resolveFormat(opts.format);
1546
1553
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1547
1554
  message: "API Key:",
1548
- type: "password",
1549
- envVar: "AGENZO_API_KEY"
1555
+ type: "password"
1550
1556
  });
1551
1557
  const params = {
1552
1558
  page: positiveInt(opts.page ?? DEFAULT_PAGE, "page"),
@@ -1554,12 +1560,13 @@ function registerListOrdersCommand(parent, deps) {
1554
1560
  };
1555
1561
  if (opts.status !== void 0) params.status = opts.status;
1556
1562
  if (opts.orderType !== void 0) params.order_type = opts.orderType;
1557
- notify(format, "loading", "Fetching orders...");
1563
+ const spinner = format === "json" ? null : createSpinner("Fetching orders...");
1558
1564
  const result = await deps.apiClient.get(
1559
1565
  "/ride/orders",
1560
1566
  { type: "api-key", key: apiKey },
1561
1567
  params
1562
1568
  );
1569
+ spinner?.stop();
1563
1570
  if (!result.success) {
1564
1571
  throw CliError.fromApi(result, { auth: "api-key" });
1565
1572
  }
@@ -1614,25 +1621,12 @@ function toListItem(s) {
1614
1621
  discovery: s.discovery
1615
1622
  };
1616
1623
  }
1617
- async function resolveServiceList(apiClient, apiKey) {
1618
- if (apiClient && apiKey) {
1619
- const result = await apiClient.get(
1620
- "/services",
1621
- { type: "api-key", key: apiKey }
1622
- );
1623
- if (result.success && Array.isArray(result.data?.services)) {
1624
- return result.data.services;
1625
- }
1626
- }
1627
- return SERVICE_REGISTRY.map(toListItem);
1628
- }
1629
- function registerServicesListCommand(parent, deps) {
1630
- const cmd = parent.command("list").description("List available merchant services (live backend feed; falls back to bundled registry)").option("--api-key <key>", "API Key enabling live backend discovery (or set AGENZO_API_KEY)");
1624
+ function registerServicesListCommand(parent) {
1625
+ const cmd = parent.command("list").description("List available merchant services from the registry");
1631
1626
  cmd.action(async () => {
1632
1627
  const opts = cmd.optsWithGlobals();
1633
1628
  const format = resolveFormat(opts.format);
1634
- const apiKey = opts.apiKey ?? process.env.AGENZO_API_KEY;
1635
- const services = await resolveServiceList(deps?.apiClient, apiKey);
1629
+ const services = SERVICE_REGISTRY.map(toListItem);
1636
1630
  const configManager = new ConfigManager();
1637
1631
  const commandResult = {
1638
1632
  data: { services },
@@ -1676,25 +1670,12 @@ function formatServiceGet(s) {
1676
1670
  Verb descriptions:
1677
1671
  ${verbDetail}`;
1678
1672
  }
1679
- async function resolveService(apiClient, apiKey, serviceId) {
1680
- if (apiClient && apiKey) {
1681
- const result = await apiClient.get(
1682
- `/services/${encodeURIComponent(serviceId)}`,
1683
- { type: "api-key", key: apiKey }
1684
- );
1685
- if (result.success && result.data?.service_id) {
1686
- return result.data;
1687
- }
1688
- }
1689
- return findService(serviceId);
1690
- }
1691
- function registerServiceGetCommand(parent, deps) {
1692
- const cmd = parent.command("get <service-id>").description("Retrieve a single merchant service by id (live backend feed; falls back to bundled registry)").option("--api-key <key>", "API Key enabling live backend discovery (or set AGENZO_API_KEY)");
1673
+ function registerServiceGetCommand(parent) {
1674
+ const cmd = parent.command("get <service-id>").description("Retrieve a single merchant service by id");
1693
1675
  cmd.action(async (serviceId) => {
1694
1676
  const opts = cmd.optsWithGlobals();
1695
1677
  const format = resolveFormat(opts.format);
1696
- const apiKey = opts.apiKey ?? process.env.AGENZO_API_KEY;
1697
- const service = await resolveService(deps?.apiClient, apiKey, serviceId);
1678
+ const service = findService(serviceId);
1698
1679
  if (!service) {
1699
1680
  throw new CliError(
1700
1681
  "SERVICE_NOT_FOUND",
@@ -1738,8 +1719,8 @@ async function main() {
1738
1719
  registerCancelCommand(rideCmd, deps);
1739
1720
  registerListOrdersCommand(rideCmd, deps);
1740
1721
  const servicesCmd = program.command("services").description("Merchant service discovery");
1741
- registerServicesListCommand(servicesCmd, deps);
1742
- registerServiceGetCommand(servicesCmd, deps);
1722
+ registerServicesListCommand(servicesCmd);
1723
+ registerServiceGetCommand(servicesCmd);
1743
1724
  await program.parseAsync(process.argv);
1744
1725
  }
1745
1726
  function resolveActiveFormat() {