@agenzo/merchant-cli 0.4.1 → 0.4.3

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,152 @@
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
+ [![npm](https://img.shields.io/npm/v/@agenzo/merchant-cli.svg)](https://www.npmjs.com/package/@agenzo/merchant-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE) ![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)
5
4
 
6
- ---
5
+ > **Merchant-fulfillment CLI** for the Agenzo platform — binary `agenzo-merchant-cli`.
6
+ > Agents fulfill commerce services here. Each capability is a noun and the set grows over time — use `services` to discover what's available; today's capability is ride-hailing (`ride-elife`).
7
7
 
8
- ## 项目简介
8
+ **[Overview](#overview)** · **[Features](#features)** · **[Quick Start](#quick-start)** · **[Global flags](#global-flags)** · **[Errors](#output-and-error-contract)** · **[Idempotency-Key](#idempotency-key)**
9
9
 
10
- `agenzo-merchant-cli` 是 Agent 接入 Agenzo 商户履约能力的命令行入口。本期上线两个 noun group:
10
+ ---
11
11
 
12
- - **`services`**(能力发现):让 Agent 发现可用的商户履约能力(该调哪个 noun/verb、调用流程)。本期数据源为 **CLI 内置注册表**(后端 `/services` discovery endpoint 属 pending impl)。
13
- - **`ride-elife`**(叫车,唯一在线 provider):报价 → 下单 → 查询/轮询 → 取消 → 列单的完整闭环。
12
+ ## Overview
14
13
 
15
- 所有通用基础设施(HTTP 客户端、输出渲染、错误体系、退出码、版本协商、配置)均复用 `@agenzo/cli-core`,app 内只保留命令处理与商户域专属逻辑(ride body 组装、NDJSON watch、verb-schema help、services 注册表、幂等键策略)。
14
+ `agenzo-merchant-cli` is the command-line entry point for Agents to fulfill commerce services on the Agenzo platform. Each fulfillment capability is exposed as a **noun**, and the set grows over time:
16
15
 
17
- > 本 CLI 与 `agenzo-admin-cli` / `agenzo-token-cli` 结构、命名、构建配置保持一致(4-CLI consistency)。
16
+ - **`services`** (capability discovery): list what's currently available and how to call it — run this first to learn the current capability set.
17
+ - **`ride-elife`** (ride-hailing): the capability available today — the full loop of quote → book → query/poll → cancel → list-orders.
18
18
 
19
- ## 功能特性
19
+ More fulfillment capabilities are added as new nouns over time; `services list` always reflects the current set.
20
20
 
21
- | Noun | Verb | 类型 | HTTP | 说明 |
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` | 列出已下单的行程(分页 + 过滤) |
21
+ ## Features
30
22
 
31
- > 写命令(`book` / `cancel`)受幂等键规则约束(见 [幂等键](#幂等键-idempotency-key))。
32
- > 未实现:`ride-elife track`(pending impl,后端无位置流接口)——`get --watch` 已覆盖订单状态轮询。
33
-
34
- ## 技术栈
23
+ | Noun | Verb | Type | HTTP | Description |
24
+ |---|---|---|---|---|
25
+ | `services` | `list` | Read | — (local) | List available merchant-fulfillment capabilities |
26
+ | `services` | `get <service-id>` | Read | — (local) | View the full metadata of a single capability (miss → `SERVICE_NOT_FOUND`) |
27
+ | `ride-elife` | `quote` | Read | `POST /ride/quote` | Point-to-point quote (`vehicle_classes[]` + meet-and-greet) |
28
+ | `ride-elife` | `book` | Write/Y | `POST /ride/book` | Book against a quote (monthly_settlement-aware, see below) |
29
+ | `ride-elife` | `get` | Read | `GET /ride/<id>/status` | Query order status; `--watch` emits an NDJSON polling stream |
30
+ | `ride-elife` | `cancel` | Write/Y | `POST /ride/<id>/cancel` | Cancel an order (no body, may incur a fee) |
31
+ | `ride-elife` | `list-orders` | Read | `GET /ride/orders` | List booked trips (pagination + filtering) |
35
32
 
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)
33
+ > Write commands (`book` / `cancel`) are subject to the idempotency-key rules (see [Idempotency-Key](#idempotency-key)).
34
+ > `ride-elife track` is not available — use `get --watch` for order-status polling.
40
35
 
41
- ## 快速开始
36
+ ## Quick Start
42
37
 
43
- ### 环境要求
38
+ ### Requirements
44
39
 
45
- - Node.js 18+
46
- - monorepo 已 `npm install`
40
+ - Node.js 22+
47
41
 
48
- ### 构建
42
+ ### Installation
49
43
 
50
44
  ```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)
45
+ npm install -g @agenzo/merchant-cli
54
46
  agenzo-merchant-cli --version
55
47
  ```
56
48
 
57
- ### 认证
49
+ Upgrade later with `npm install -g @agenzo/merchant-cli@latest`.
58
50
 
59
- 运行面 CLI,使用 **API Key**(`--api-key`,作为 `X-Api-Key` 头逐命令携带):
51
+ ### Authentication
60
52
 
61
- - API Key 由 `agenzo-admin-cli keys create --scope merchant` 签发,需含 `merchant` scope。
62
- - 缺省 `--api-key` 时交互索取(password 输入)。
63
- - 无 Bearer Token / AuthService / keystore——与 `agenzo-token-cli` 一致。
53
+ A runtime-plane CLI that uses an **API Key** (`--api-key`, carried per command as the `X-Api-Key` header):
64
54
 
65
- ### 端到端示例
55
+ - The API Key is issued by `agenzo-admin-cli keys create --scope merchant` and must include the `merchant` scope.
56
+ - When `--api-key` is omitted, it is requested interactively (password input).
57
+ - No Bearer Token or local keystore — every command carries the API Key.
58
+
59
+ ### End-to-end example
66
60
 
67
61
  ```bash
68
- # 1. 发现能力
62
+ # 1. Discover capabilities
69
63
  agenzo-merchant-cli services list
70
64
  agenzo-merchant-cli services get ride-elife
71
65
 
72
- # 2. 报价(产出 quote_id)
66
+ # 2. Quote (produces quote_id)
73
67
  agenzo-merchant-cli ride-elife quote --api-key "$API_KEY" \
74
68
  --pickup-lat 37.7937 --pickup-lng -122.3956 --pickup-name "1 Market St" \
75
69
  --dropoff-lat 37.6213 --dropoff-lng -122.3790 --dropoff-name "SFO Airport" \
76
70
  --pickup-time now
77
71
 
78
- # 3. 下单(写操作:--yes 自动确认 + 必须提供 --idempotency-key)
72
+ # 3. Book (write op: --yes auto-confirms + --idempotency-key must be provided)
79
73
  agenzo-merchant-cli ride-elife book --api-key "$API_KEY" --yes \
80
74
  --quote-id "$QUOTE_ID" --vehicle-class Sedan --price-amount 42.50 \
81
75
  --passenger-name "Alice" --passenger-phone "+14155551234" \
82
76
  --idempotency-key "book-$(date +%s)"
83
77
 
84
- # 4. 查询 / 轮询(--watch 输出逐行 NDJSON)
78
+ # 4. Query / poll (--watch emits line-by-line NDJSON)
85
79
  agenzo-merchant-cli ride-elife get --api-key "$API_KEY" --order-id "$RIDE_ID"
86
80
  agenzo-merchant-cli ride-elife get --api-key "$API_KEY" --order-id "$RIDE_ID" \
87
81
  --watch --watch-interval 5 --watch-timeout 600
88
82
 
89
- # 5. 列单 / 取消
83
+ # 5. List orders / cancel
90
84
  agenzo-merchant-cli ride-elife list-orders --api-key "$API_KEY" --status Pending --page 1 --page-size 20
91
85
  agenzo-merchant-cli ride-elife cancel --api-key "$API_KEY" --yes \
92
86
  --order-id "$RIDE_ID" --idempotency-key "cancel-$(date +%s)"
93
87
  ```
94
88
 
95
- ## 全局 flag
89
+ ## Global flags
96
90
 
97
- | Flag | 默认 | 说明 |
91
+ | Flag | Default | Description |
98
92
  |---|---|---|
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) |
93
+ | `--format <json\|table>` | **`json`** | Output format. Defaults to `json`; pass `table` for tabular output. Also reads the `AGENZO_FORMAT` environment variable |
94
+ | `--api-key <key>` | — (requested interactively) | API Key, carried per command as the `X-Api-Key` header |
95
+ | `--yes` | `false` | Skip interactive confirmation (non-interactive automation); for write commands, `--yes` with a missing idempotency key is a hard error |
96
+ | `--verbose` | `false` | Verbose logging (written to stderr) |
103
97
 
104
- ## 输出与错误契约
98
+ ## Output and error contract
105
99
 
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>`。
100
+ - **`--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.
101
+ - **`--format table`**: business output goes to stdout, status lines/spinner go to stderr.
102
+ - **`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", ... }`.
103
+ - **Error envelope**: `json` → `{ "error": { "code", "code_num", "message", "request_id"? } }`; `table` → `✗ [<code_num>] <message>`. When a request fails, retry with the same `--idempotency-key`; if it persists, contact support with the `request_id` from the error output.
110
104
 
111
- 退出码(由 cli-core `exitCodeFor` 映射):
105
+ Exit codes:
112
106
 
113
- | 退出码 | 含义 | 代表错误码 |
107
+ | Exit code | Meaning | Representative error codes |
114
108
  |---|---|---|
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 |
109
+ | `0` | Success | — |
110
+ | `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) |
111
+ | `2` | Upgrade required | `UPGRADE_REQUIRED`(9008) |
112
+ | `3` | Auth / invalid key | `KEY_INVALID`(1101) · `KEY_SCOPE_DENIED`(1102) |
113
+ | `4` | Network / 5xx | `RATE_LIMITED`(5001) · `UPSTREAM_ERROR`(5101) · `INTERNAL_ERROR`(5201) |
114
+ | `5` | User cancel | `CLIENT_ABORTED`(9007) · SIGINT |
121
115
 
122
- > api-key 鉴权映射:HTTP 401 → `KEY_INVALID`、403 → `KEY_SCOPE_DENIED`。后端返回的已知 §8 字符串码(如 `QUOTE_EXPIRED`)优先保真,否则回退 HTTP-status 映射。
116
+ > api-key auth mapping: HTTP 401 → `KEY_INVALID`, 403 → `KEY_SCOPE_DENIED`. A known string error code (e.g. `QUOTE_EXPIRED`) takes priority and is preserved; otherwise it falls back to the HTTP-status mapping.
123
117
 
124
- ## 幂等键 (Idempotency-Key)
118
+ ## Idempotency-Key
125
119
 
126
- 写命令 `ride-elife book` / `cancel` 接受 `--idempotency-key`:
120
+ The write commands `ride-elife book` / `cancel` accept `--idempotency-key`:
127
121
 
128
- - 格式:`[A-Za-z0-9_-]{1,128}`。
129
- - **CLI 永不自动生成**:`--yes` 且缺键 → 硬错误 `PARAM_IDEMPOTENCY_KEY_REQUIRED`(exit 1),不发起请求;非 `--yes` 缺键 → 交互索取。
130
- - 作为 `Idempotency-Key` HTTP **头**发送,**绝不进 body**。
131
- - 同一逻辑请求重试用同一键;新请求用新键。
122
+ - Format: `[A-Za-z0-9_-]{1,128}`.
123
+ - **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.
124
+ - Sent as the `Idempotency-Key` HTTP **header**, **never in the body**.
125
+ - Reuse the same key to retry the same logical request; use a new key for a new request.
132
126
 
133
- ## book 的计费模型(monthly_settlement-aware)
127
+ ## Billing model for book (monthly_settlement-aware)
134
128
 
135
- `ride-elife book` **不接受** `--payment-method-id` 或任何卡信息——商户域不持有支付句柄。funding 由后端按 Developer 的 `billing_mode` 决定:
129
+ `ride-elife book` **does not accept** `--payment-method-id` or any card information — the merchant domain holds no payment handle. Funding is decided according to the Developer's `billing_mode`:
136
130
 
137
- - **monthly_settlement**:从月结账户扣减,无支付句柄;响应 `payment_status=ON_ACCOUNT`,含 `billing_entry_id`。
138
- - **pay_per_call**:可选透传 `--payment-order-id`(payment-cli 已扣款的 PAID 订单号);响应回显 `payment_order_id`。
131
+ - **monthly_settlement**: deducted from the monthly-settlement account, no payment handle; the response has `payment_status=ON_ACCOUNT` and includes `billing_entry_id`.
132
+ - **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
133
 
140
- 请求 body 至多含可选 `payment_order_id`。
134
+ The request body contains at most an optional `payment_order_id`.
141
135
 
142
- ## profile / host 模型
136
+ ## profile / host model
143
137
 
144
- merchant-cli **没有任何 host / config 命令**,不治理环境。它复用 `@agenzo/cli-core` 的 `ConfigManager` 默认配置(即 `agenzo-admin-cli` 统一治理的环境,默认落盘 `~/.agenzo-admin-cli/`):
138
+ merchant-cli **has no host / config commands** and does not govern environments. It reuses the environment configured by `agenzo-admin-cli` (persisted under `~/.agenzo-admin-cli/`):
145
139
 
146
- - 为 `ApiClient` 提供 `baseUrl`(host + `/api/merchant/v1`)。
147
- - 为 json 输出的 `profile` / `endpoint` 信封提供环境名 + host。
140
+ - Determines the API host that requests are sent to.
141
+ - Provides the environment name + host for the `profile` / `endpoint` envelope in json output.
148
142
 
149
- **环境治理(设置 host / 切换环境)归 `agenzo-admin-cli`**(`agenzo-admin-cli config set` / `set-host`)——与 `agenzo-token-cli` 一致。
143
+ **Setting the host / switching environments belongs to `agenzo-admin-cli`** (`agenzo-admin-cli config set-host`).
150
144
 
151
- ## 机器可读的 verb schema
145
+ ## Machine-readable verb schema
152
146
 
153
- 每个 `ride-elife` verb 支持 `--help --format json`,输出该 verb 的机器可读 schema(`cli` / `noun` / `verb` / `description` / `flags` / `response` / `example`,部分含 `error_recovery` / `polling`):
147
+ 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
148
 
155
149
  ```bash
156
150
  agenzo-merchant-cli ride-elife quote --help --format json
157
151
  agenzo-merchant-cli ride-elife book --help --format json
158
152
  ```
159
-
160
- ## 开发
161
-
162
- ```bash
163
- npm run build -w @agenzo/merchant-cli # tsup 构建
164
- npm run test -w @agenzo/merchant-cli # vitest(含 fast-check PBT)
165
- ```
166
-
167
- 源码结构:
168
-
169
- ```text
170
- apps/merchant-cli/
171
- src/
172
- index.ts # commander 装配 + 顶层 reportError + SIGINT
173
- services/{list,get}.ts # 能力发现(本期:CLI 内置注册表)
174
- services/registry.ts # 内置静态注册表(商户域)
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
182
- ```
183
-
184
- ## 文档索引
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)。
189
-
190
- ## 更新日志
191
-
192
- ### 2026-06
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`。
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
  }
@@ -1108,15 +1115,14 @@ function formatQuote(data) {
1108
1115
  ${Formatter.keyValue(lines)}`;
1109
1116
  }
1110
1117
  function registerQuoteCommand(parent, deps) {
1111
- const cmd = parent.command("quote").description("Request fare quotes for a ride between two points").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--pickup-lat <lat>", "Pickup latitude").option("--pickup-lng <lng>", "Pickup longitude").option("--pickup-name <name>", "Pickup location name").option("--dropoff-lat <lat>", "Dropoff latitude").option("--dropoff-lng <lng>", "Dropoff longitude").option("--dropoff-name <name>", "Dropoff location name").option("--pickup-time <time>", 'Pickup time: epoch seconds, or "now"').option("--passenger-name <name>", "Lead passenger full name").option("--passenger-phone <phone>", "Passenger phone (E.164)").option("--passenger-count <n>", "Passenger count").option("--luggage-count <n>", "Luggage count").option("--passenger-email <email>", "Passenger email").option("--children-count <n>", "Children count");
1118
+ const cmd = parent.command("quote").description("Request fare quotes for a ride between two points").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--pickup-lat <lat>", "Pickup latitude").option("--pickup-lng <lng>", "Pickup longitude").option("--pickup-name <name>", "Pickup location name").option("--dropoff-lat <lat>", "Dropoff latitude").option("--dropoff-lng <lng>", "Dropoff longitude").option("--dropoff-name <name>", "Dropoff location name").option("--pickup-time <time>", 'Pickup time: epoch seconds, or "now"').option("--passenger-name <name>", "Lead passenger full name").option("--passenger-phone <phone>", "Passenger phone (E.164)").option("--passenger-count <n>", "Passenger count").option("--luggage-count <n>", "Luggage count").option("--passenger-email <email>", "Passenger email").option("--children-count <n>", "Children count").option("--infant-count <n>", "Infant count (passenger breakdown)").option("--toddler-count <n>", "Toddler count (passenger breakdown)");
1112
1119
  attachSchemaHelp(cmd, quoteSchema);
1113
1120
  cmd.action(async () => {
1114
1121
  const opts = cmd.optsWithGlobals();
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: {
@@ -1142,15 +1148,22 @@ function registerQuoteCommand(parent, deps) {
1142
1148
  if (opts.childrenCount !== void 0) {
1143
1149
  body.children_count = num(opts.childrenCount, "children-count");
1144
1150
  }
1151
+ if (opts.infantCount !== void 0) {
1152
+ body.infant_count = num(opts.infantCount, "infant-count");
1153
+ }
1154
+ if (opts.toddlerCount !== void 0) {
1155
+ body.toddler_count = num(opts.toddlerCount, "toddler-count");
1156
+ }
1145
1157
  if (opts.passengerEmail !== void 0) {
1146
1158
  body.passenger_email = opts.passengerEmail;
1147
1159
  }
1148
- notify(format, "loading", "Fetching quotes...");
1160
+ const spinner = format === "json" ? null : createSpinner("Fetching quotes...");
1149
1161
  const result = await deps.apiClient.post(
1150
1162
  "/ride/quote",
1151
1163
  { type: "api-key", key: apiKey },
1152
1164
  body
1153
1165
  );
1166
+ spinner?.stop();
1154
1167
  if (!result.success) {
1155
1168
  throw CliError.fromApi(result, { auth: "api-key" });
1156
1169
  }
@@ -1244,8 +1257,7 @@ function registerBookCommand(parent, deps) {
1244
1257
  const isYes = Boolean(opts.yes);
1245
1258
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1246
1259
  message: "API Key:",
1247
- type: "password",
1248
- envVar: "AGENZO_API_KEY"
1260
+ type: "password"
1249
1261
  });
1250
1262
  const quoteId = need2(opts.quoteId, "quote-id");
1251
1263
  const body = {
@@ -1318,13 +1330,14 @@ function registerBookCommand(parent, deps) {
1318
1330
  yes: isYes,
1319
1331
  commandPath: "ride-elife book"
1320
1332
  });
1321
- notify(format, "loading", "Booking ride...");
1333
+ const spinner = format === "json" ? null : createSpinner("Booking ride...");
1322
1334
  const result = await deps.apiClient.post(
1323
1335
  "/ride/book",
1324
1336
  { type: "api-key", key: apiKey },
1325
1337
  body,
1326
1338
  { "Idempotency-Key": idempotencyKey }
1327
1339
  );
1340
+ spinner?.stop();
1328
1341
  if (!result.success) {
1329
1342
  throw CliError.fromApi(result, { auth: "api-key" });
1330
1343
  }
@@ -1397,8 +1410,7 @@ function registerRideGetCommand(parent, deps) {
1397
1410
  const format = resolveFormat(opts.format);
1398
1411
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1399
1412
  message: "API Key:",
1400
- type: "password",
1401
- envVar: "AGENZO_API_KEY"
1413
+ type: "password"
1402
1414
  });
1403
1415
  const orderId = need3(opts.orderId, "order-id");
1404
1416
  if (opts.watch) {
@@ -1414,11 +1426,12 @@ function registerRideGetCommand(parent, deps) {
1414
1426
  });
1415
1427
  return;
1416
1428
  }
1417
- notify(format, "loading", "Fetching ride status...");
1429
+ const spinner = format === "json" ? null : createSpinner("Fetching ride status...");
1418
1430
  const result = await deps.apiClient.get(
1419
1431
  `/ride/${encodeURIComponent(orderId)}/status`,
1420
1432
  { type: "api-key", key: apiKey }
1421
1433
  );
1434
+ spinner?.stop();
1422
1435
  if (!result.success) {
1423
1436
  throw CliError.fromApi(result, { auth: "api-key" });
1424
1437
  }
@@ -1467,8 +1480,7 @@ function registerCancelCommand(parent, deps) {
1467
1480
  const isYes = Boolean(opts.yes);
1468
1481
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1469
1482
  message: "API Key:",
1470
- type: "password",
1471
- envVar: "AGENZO_API_KEY"
1483
+ type: "password"
1472
1484
  });
1473
1485
  const orderId = need4(opts.orderId, "order-id");
1474
1486
  if (!isYes) {
@@ -1484,13 +1496,14 @@ function registerCancelCommand(parent, deps) {
1484
1496
  yes: isYes,
1485
1497
  commandPath: "ride-elife cancel"
1486
1498
  });
1487
- notify(format, "loading", "Cancelling ride...");
1499
+ const spinner = format === "json" ? null : createSpinner("Cancelling ride...");
1488
1500
  const result = await deps.apiClient.post(
1489
1501
  `/ride/${encodeURIComponent(orderId)}/cancel`,
1490
1502
  { type: "api-key", key: apiKey },
1491
1503
  void 0,
1492
1504
  { "Idempotency-Key": idempotencyKey }
1493
1505
  );
1506
+ spinner?.stop();
1494
1507
  if (!result.success) {
1495
1508
  throw CliError.fromApi(result, { auth: "api-key" });
1496
1509
  }
@@ -1545,8 +1558,7 @@ function registerListOrdersCommand(parent, deps) {
1545
1558
  const format = resolveFormat(opts.format);
1546
1559
  const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1547
1560
  message: "API Key:",
1548
- type: "password",
1549
- envVar: "AGENZO_API_KEY"
1561
+ type: "password"
1550
1562
  });
1551
1563
  const params = {
1552
1564
  page: positiveInt(opts.page ?? DEFAULT_PAGE, "page"),
@@ -1554,12 +1566,13 @@ function registerListOrdersCommand(parent, deps) {
1554
1566
  };
1555
1567
  if (opts.status !== void 0) params.status = opts.status;
1556
1568
  if (opts.orderType !== void 0) params.order_type = opts.orderType;
1557
- notify(format, "loading", "Fetching orders...");
1569
+ const spinner = format === "json" ? null : createSpinner("Fetching orders...");
1558
1570
  const result = await deps.apiClient.get(
1559
1571
  "/ride/orders",
1560
1572
  { type: "api-key", key: apiKey },
1561
1573
  params
1562
1574
  );
1575
+ spinner?.stop();
1563
1576
  if (!result.success) {
1564
1577
  throw CliError.fromApi(result, { auth: "api-key" });
1565
1578
  }
@@ -1614,25 +1627,12 @@ function toListItem(s) {
1614
1627
  discovery: s.discovery
1615
1628
  };
1616
1629
  }
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)");
1630
+ function registerServicesListCommand(parent) {
1631
+ const cmd = parent.command("list").description("List available merchant services from the registry");
1631
1632
  cmd.action(async () => {
1632
1633
  const opts = cmd.optsWithGlobals();
1633
1634
  const format = resolveFormat(opts.format);
1634
- const apiKey = opts.apiKey ?? process.env.AGENZO_API_KEY;
1635
- const services = await resolveServiceList(deps?.apiClient, apiKey);
1635
+ const services = SERVICE_REGISTRY.map(toListItem);
1636
1636
  const configManager = new ConfigManager();
1637
1637
  const commandResult = {
1638
1638
  data: { services },
@@ -1676,25 +1676,12 @@ function formatServiceGet(s) {
1676
1676
  Verb descriptions:
1677
1677
  ${verbDetail}`;
1678
1678
  }
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)");
1679
+ function registerServiceGetCommand(parent) {
1680
+ const cmd = parent.command("get <service-id>").description("Retrieve a single merchant service by id");
1693
1681
  cmd.action(async (serviceId) => {
1694
1682
  const opts = cmd.optsWithGlobals();
1695
1683
  const format = resolveFormat(opts.format);
1696
- const apiKey = opts.apiKey ?? process.env.AGENZO_API_KEY;
1697
- const service = await resolveService(deps?.apiClient, apiKey, serviceId);
1684
+ const service = findService(serviceId);
1698
1685
  if (!service) {
1699
1686
  throw new CliError(
1700
1687
  "SERVICE_NOT_FOUND",
@@ -1738,8 +1725,8 @@ async function main() {
1738
1725
  registerCancelCommand(rideCmd, deps);
1739
1726
  registerListOrdersCommand(rideCmd, deps);
1740
1727
  const servicesCmd = program.command("services").description("Merchant service discovery");
1741
- registerServicesListCommand(servicesCmd, deps);
1742
- registerServiceGetCommand(servicesCmd, deps);
1728
+ registerServicesListCommand(servicesCmd);
1729
+ registerServiceGetCommand(servicesCmd);
1743
1730
  await program.parseAsync(process.argv);
1744
1731
  }
1745
1732
  function resolveActiveFormat() {