@agenzo/merchant-cli 0.2.2 → 0.3.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/dist/index.js CHANGED
@@ -1,839 +1,1567 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command8 } from "commander";
4
+ import { Command } from "commander";
5
5
 
6
- // src/core/errors.ts
7
- var ErrorCodes = {
8
- // -- Ride/elife domain (raised by the backend, recognized here) --
9
- QUOTE_EXPIRED: "QUOTE_EXPIRED",
10
- VEHICLE_UNAVAILABLE: "VEHICLE_UNAVAILABLE",
11
- BOOKING_FAILED: "BOOKING_FAILED",
12
- CANCELLATION_NOT_ALLOWED: "CANCELLATION_NOT_ALLOWED",
13
- // -- Billing / settlement-account (monthly_settlement) --
14
- BILLING_MODE_MISMATCH: "BILLING_MODE_MISMATCH",
15
- ACCOUNT_NOT_FOUND: "ACCOUNT_NOT_FOUND",
16
- ACCOUNT_SUSPENDED: "ACCOUNT_SUSPENDED",
17
- ACCOUNT_INSUFFICIENT_BALANCE: "ACCOUNT_INSUFFICIENT_BALANCE",
18
- // -- Payment order (pay_per_call; reserved until payment-cli ships) --
19
- PAYMENT_ORDER_NOT_FOUND: "PAYMENT_ORDER_NOT_FOUND",
20
- PAYMENT_ORDER_NOT_PAID: "PAYMENT_ORDER_NOT_PAID",
21
- PAYMENT_ORDER_MISMATCH: "PAYMENT_ORDER_MISMATCH",
22
- PAYMENT_ORDER_ALREADY_CONSUMED: "PAYMENT_ORDER_ALREADY_CONSUMED",
23
- // -- Parameter / idempotency --
24
- PARAM_IDEMPOTENCY_KEY_REQUIRED: "PARAM_IDEMPOTENCY_KEY_REQUIRED",
25
- PARAM_IDEMPOTENCY_KEY_CONFLICT: "PARAM_IDEMPOTENCY_KEY_CONFLICT",
26
- // -- Service discovery --
27
- SERVICE_NOT_FOUND: "SERVICE_NOT_FOUND",
28
- // -- CLI-local / transport --
29
- NETWORK_ERROR: "NETWORK_ERROR",
30
- TIMEOUT: "TIMEOUT",
31
- CLI_OUTDATED: "CLI_OUTDATED",
32
- CONFIG_ERROR: "CONFIG_ERROR",
33
- NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
34
- INTERNAL_ERROR: "INTERNAL_ERROR"
6
+ // ../../packages/cli-core/dist/index.js
7
+ import { readFileSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
11
+ import { join as join4 } from "path";
12
+ import { homedir as homedir3 } from "os";
13
+ import { input, password, select } from "@inquirer/prompts";
14
+ var CODE_NUM = {
15
+ AUTH_SESSION_EXPIRED: 1001,
16
+ AUTH_MAGIC_LINK_EXPIRED: 1002,
17
+ AUTH_INVITE_CODE_REQUIRED: 1003,
18
+ AUTH_INVITE_CODE_INVALID: 1004,
19
+ KEY_INVALID: 1101,
20
+ KEY_SCOPE_DENIED: 1102,
21
+ RESOURCE_NOT_FOUND: 2001,
22
+ RESOURCE_CONFLICT: 2002,
23
+ RESOURCE_STATE_INVALID: 2003,
24
+ PARAM_INVALID: 2101,
25
+ PARAM_IDEMPOTENCY_KEY_REQUIRED: 2102,
26
+ PARAM_IDEMPOTENCY_KEY_CONFLICT: 2103,
27
+ BILLING_MODE_MISMATCH: 3001,
28
+ ACCOUNT_NOT_FOUND: 3101,
29
+ ACCOUNT_SUSPENDED: 3102,
30
+ ACCOUNT_INSUFFICIENT_BALANCE: 3103,
31
+ PAYMENT_ORDER_NOT_FOUND: 3201,
32
+ PAYMENT_ORDER_NOT_PAID: 3202,
33
+ PAYMENT_ORDER_MISMATCH: 3203,
34
+ PAYMENT_ORDER_ALREADY_CONSUMED: 3204,
35
+ TOKEN_FEATURE_DISABLED: 4001,
36
+ SERVICE_NOT_FOUND: 4101,
37
+ VEHICLE_UNAVAILABLE: 4201,
38
+ QUOTE_EXPIRED: 4202,
39
+ BOOKING_FAILED: 4203,
40
+ CANCELLATION_NOT_ALLOWED: 4204,
41
+ RATE_LIMITED: 5001,
42
+ UPSTREAM_ERROR: 5101,
43
+ INTERNAL_ERROR: 5201,
44
+ NOT_IMPLEMENTED: 5300,
45
+ CLIENT_NOT_SIGNED_IN: 9001,
46
+ CLIENT_LOGIN_TIMEOUT: 9002,
47
+ CLIENT_ORG_NOT_LOCAL: 9003,
48
+ CLIENT_ORG_WRONG_ENV: 9004,
49
+ CLIENT_NO_PAYMENT_METHOD: 9005,
50
+ CLIENT_CARD_NOT_MATCHED: 9006,
51
+ CLIENT_ABORTED: 9007,
52
+ UPGRADE_REQUIRED: 9008
35
53
  };
36
- var CliError = class extends Error {
37
- constructor(code, message, recovery, exitCode = 1) {
54
+ function codeNum(code) {
55
+ return CODE_NUM[code];
56
+ }
57
+ var CliError = class _CliError extends Error {
58
+ constructor(code, message, statusCode, requestId, upstream) {
38
59
  super(message);
39
60
  this.code = code;
40
- this.recovery = recovery;
41
- this.exitCode = exitCode;
61
+ this.statusCode = statusCode;
62
+ this.requestId = requestId;
63
+ this.upstream = upstream;
42
64
  this.name = "CliError";
43
65
  }
44
66
  code;
45
- recovery;
46
- exitCode;
47
- };
48
-
49
- // src/core/config-manager.ts
50
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
51
- import { homedir } from "os";
52
- import { join } from "path";
53
- var DEFAULT_HOST = "https://agent.everonet.com";
54
- var CONFIG_DIR = join(homedir(), ".agenzo-merchant-cli");
55
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
56
- function read() {
57
- let raw;
58
- try {
59
- raw = readFileSync(CONFIG_PATH, "utf-8");
60
- } catch (error) {
61
- if (error.code === "ENOENT") {
62
- return { host: DEFAULT_HOST };
67
+ statusCode;
68
+ requestId;
69
+ upstream;
70
+ /**
71
+ * Map a backend ApiError to a CLI-facing code (§8.5 transitional mapping).
72
+ * Backend numeric codes / HTTP status are translated to §8.4 string codes;
73
+ * the raw backend message is NOT passed through (§8.1 principle 4) — a
74
+ * stable, fixed message per code is used instead.
75
+ *
76
+ * The optional `opts.auth` selects the auth semantics for the 401 mapping:
77
+ * - `'bearer'` (default, admin-cli Bearer Token): 401 → `AUTH_SESSION_EXPIRED`.
78
+ * - `'api-key'` (token-cli `X-Api-Key`): 401 → `KEY_INVALID`.
79
+ * 403 maps to `KEY_SCOPE_DENIED` under both auth modes. Omitting `opts`
80
+ * preserves the original Bearer behavior exactly (§8.5 / requirement 6.4).
81
+ *
82
+ * Domain string-code routing (D3 / requirement 5.4): when the backend
83
+ * supplies a string `error.code` in the v3 envelope that is a known §8 code
84
+ * (present in {@link CODE_NUM}), it is used verbatim as the CLI-facing code
85
+ * so ride/merchant-specific codes (`QUOTE_EXPIRED`, `VEHICLE_UNAVAILABLE`,
86
+ * `BILLING_MODE_MISMATCH`, `PAYMENT_ORDER_*`, …) survive intact. Otherwise —
87
+ * including when no string code is provided, as is the case for admin/token
88
+ * today — mapping falls back to the HTTP-status table below, leaving the
89
+ * existing Bearer/api-key behavior unchanged.
90
+ */
91
+ static fromApi(error, opts) {
92
+ if (error.code && isKnownErrorCode(error.code)) {
93
+ return new _CliError(
94
+ error.code,
95
+ STABLE_MESSAGE[error.code] ?? "Request failed. Please check your input and retry.",
96
+ error.statusCode,
97
+ error.requestId,
98
+ error.upstream
99
+ );
63
100
  }
64
- throw error;
101
+ const status = error.statusCode;
102
+ let code;
103
+ if (status === 401) code = opts?.auth === "api-key" ? "KEY_INVALID" : "AUTH_SESSION_EXPIRED";
104
+ else if (status === 403) code = "KEY_SCOPE_DENIED";
105
+ else if (status === 404) code = "RESOURCE_NOT_FOUND";
106
+ else if (status === 409) code = "RESOURCE_CONFLICT";
107
+ else if (status === 429) code = "RATE_LIMITED";
108
+ else if (status >= 500) code = "INTERNAL_ERROR";
109
+ else code = "PARAM_INVALID";
110
+ return new _CliError(
111
+ code,
112
+ STABLE_MESSAGE[code] ?? "Request failed. Please check your input and retry.",
113
+ status,
114
+ error.requestId,
115
+ error.upstream
116
+ );
65
117
  }
66
- try {
67
- const parsed = JSON.parse(raw);
68
- return { host: parsed.host ?? DEFAULT_HOST };
69
- } catch {
70
- throw new CliError(
71
- ErrorCodes.CONFIG_ERROR,
72
- `Invalid config file: ${CONFIG_PATH}`,
73
- "Delete the file to reset to defaults."
118
+ };
119
+ var NetworkError = class extends CliError {
120
+ constructor(_url, timeout, _cause) {
121
+ super(
122
+ "UPSTREAM_ERROR",
123
+ timeout ? "The request timed out. The service may be temporarily unavailable \u2014 please retry." : "Connection failed. The service may be temporarily unavailable \u2014 please retry."
74
124
  );
75
125
  }
76
- }
77
- function write(config) {
78
- mkdirSync(CONFIG_DIR, { recursive: true });
79
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
80
- }
81
- function getHost() {
82
- return read().host;
83
- }
84
- function setHost(host) {
85
- write({ host: host.replace(/\/+$/, "") });
86
- }
87
- function resetHost() {
88
- write({ host: DEFAULT_HOST });
89
- }
90
-
91
- // src/core/formatter.ts
92
- var ICONS = {
93
- success: "\u2713",
94
- error: "\u2717",
95
- info: "\u2139",
96
- warning: "\u26A0"
97
126
  };
98
- var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
99
- function createSpinner(message) {
100
- const isTty = Boolean(process.stderr.isTTY);
101
- let current = message;
102
- if (!isTty) {
103
- return {
104
- update(msg) {
105
- current = msg;
106
- },
107
- stop() {
108
- },
109
- succeed(msg) {
110
- if (msg) process.stderr.write(`${Formatter.status("success", msg)}
111
- `);
112
- },
113
- fail(msg) {
114
- process.stderr.write(`${Formatter.status("error", msg ?? current)}
115
- `);
116
- }
117
- };
127
+ var ConfigError = class extends CliError {
128
+ constructor(message, filePath) {
129
+ super("INTERNAL_ERROR", message || "Unknown error");
130
+ this.filePath = filePath;
118
131
  }
119
- let frame = 0;
120
- const render = () => {
121
- process.stderr.write(`\r\x1B[K${SPINNER_FRAMES[frame]} ${current}`);
122
- frame = (frame + 1) % SPINNER_FRAMES.length;
123
- };
124
- render();
125
- const timer = setInterval(render, 80);
126
- const clear = () => {
127
- clearInterval(timer);
128
- process.stderr.write("\r\x1B[K");
129
- };
130
- return {
131
- update(msg) {
132
- current = msg;
133
- },
134
- stop() {
135
- clear();
136
- },
137
- succeed(msg) {
138
- clear();
139
- if (msg) process.stderr.write(`${Formatter.status("success", msg)}
140
- `);
141
- },
142
- fail(msg) {
143
- clear();
144
- process.stderr.write(`${Formatter.status("error", msg ?? current)}
145
- `);
146
- }
147
- };
148
- }
149
- function stringify(value) {
150
- if (value === null || value === void 0) return "";
151
- if (typeof value === "object") return JSON.stringify(value);
152
- return String(value);
153
- }
154
- var Formatter = class _Formatter {
155
- /** Status-prefixed line, e.g. "✓ done". */
156
- static status(type, message) {
157
- return `${ICONS[type]} ${message}`;
132
+ filePath;
133
+ };
134
+ var ValidationError = class extends CliError {
135
+ constructor(message) {
136
+ super("PARAM_INVALID", message || "Unknown error");
158
137
  }
159
- /** Aligned columnar table for list output. */
160
- static table(headers, rows) {
161
- const widths = headers.map(
162
- (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
138
+ };
139
+ var IdempotencyKeyRequiredError = class extends CliError {
140
+ constructor(commandPath) {
141
+ super(
142
+ "PARAM_IDEMPOTENCY_KEY_REQUIRED",
143
+ `\`${commandPath}\` requires --idempotency-key <key>. Supply a unique key so the write can be safely retried.`
163
144
  );
164
- const line = (cells) => cells.map((c, i) => (c ?? "").padEnd(widths[i])).join(" ");
165
- return [
166
- line(headers),
167
- widths.map((w) => "-".repeat(w)).join(" "),
168
- ...rows.map(line)
169
- ].join("\n");
170
145
  }
171
- /** Key-value detail output with left-aligned keys. */
172
- static keyValue(entries) {
173
- const width = Math.max(0, ...entries.map(([k]) => k.length));
174
- return entries.map(([k, v]) => `${k.padEnd(width)} ${v}`).join("\n");
175
- }
176
- /** Best-effort human-readable rendering of arbitrary data. */
177
- static auto(data) {
178
- if (Array.isArray(data)) {
179
- return _Formatter.arrayTable(data);
180
- }
181
- if (data && typeof data === "object") {
182
- const obj = data;
183
- const scalarEntries = [];
184
- const tableSections = [];
185
- for (const [k, v] of Object.entries(obj)) {
186
- if (Array.isArray(v) && v.some((i) => i && typeof i === "object")) {
187
- tableSections.push(`${k}:
188
- ${_Formatter.arrayTable(v)}`);
189
- } else {
190
- scalarEntries.push([k, stringify(v)]);
191
- }
192
- }
193
- const parts = [];
194
- if (scalarEntries.length) parts.push(_Formatter.keyValue(scalarEntries));
195
- parts.push(...tableSections);
196
- return parts.join("\n\n");
197
- }
198
- return stringify(data);
199
- }
200
- /** Render an array of objects as an aligned table; falls back gracefully. */
201
- static arrayTable(items) {
202
- if (items.length === 0) return "(empty)";
203
- const rows = items.filter(
204
- (i) => Boolean(i) && typeof i === "object"
205
- );
206
- if (rows.length === 0) {
207
- return items.map((i) => stringify(i)).join(", ");
208
- }
209
- const headers = Object.keys(rows[0]);
210
- return _Formatter.table(
211
- headers,
212
- rows.map((r) => headers.map((h) => stringify(r[h])))
146
+ };
147
+ var UpgradeRequiredError = class extends CliError {
148
+ constructor(currentVersion, minVersion, upgradeCommand) {
149
+ super(
150
+ "UPGRADE_REQUIRED",
151
+ `agenzo-admin-cli ${currentVersion} is out of date \u2014 the server requires ${minVersion} or newer. To upgrade, run: ${upgradeCommand}`
213
152
  );
214
153
  }
215
154
  };
216
-
217
- // src/core/output.ts
218
- function emit(data, format = "json") {
219
- if (format === "table") {
220
- console.log(Formatter.auto(data));
221
- } else {
222
- console.log(JSON.stringify(data, null, 2));
155
+ var UserCancelError = class extends CliError {
156
+ constructor(message = "Operation cancelled by user") {
157
+ super("CLIENT_ABORTED", message || "Unknown error");
223
158
  }
159
+ };
160
+ function isKnownErrorCode(value) {
161
+ return Object.prototype.hasOwnProperty.call(CODE_NUM, value);
224
162
  }
225
- function emitNdjsonLine(data) {
226
- console.log(JSON.stringify(data));
227
- }
228
- function emitSchema(schema, format = "json") {
229
- if (format === "table") {
230
- console.log(Formatter.keyValue(Object.entries(schema.params)));
231
- } else {
232
- console.log(JSON.stringify(schema, null, 2));
163
+ var STABLE_MESSAGE = {
164
+ AUTH_SESSION_EXPIRED: "Your session has expired. Please run `agenzo-admin-cli auth login` again.",
165
+ AUTH_MAGIC_LINK_EXPIRED: "Verification link is invalid or has expired. Please request a new one.",
166
+ AUTH_INVITE_CODE_REQUIRED: "An invitation code is required to register.",
167
+ AUTH_INVITE_CODE_INVALID: "The invitation code is invalid.",
168
+ KEY_INVALID: "The API key is invalid or has been revoked. Please check your --api-key and retry.",
169
+ KEY_SCOPE_DENIED: "This API key does not have the required scope for this command.",
170
+ RESOURCE_NOT_FOUND: "The resource was not found or does not belong to the current organization.",
171
+ RESOURCE_CONFLICT: "A resource with the same unique value already exists.",
172
+ RESOURCE_STATE_INVALID: "The resource is in a state that does not permit this operation.",
173
+ RATE_LIMITED: "Too many requests. Please back off and retry.",
174
+ UPSTREAM_ERROR: "A third-party service is temporarily unavailable. Please try again later.",
175
+ INTERNAL_ERROR: "Something went wrong on the server. Please retry and note the request_id.",
176
+ PARAM_INVALID: "One or more parameters are invalid. Please check your input and retry.",
177
+ PARAM_IDEMPOTENCY_KEY_CONFLICT: "A request with this idempotency key was already processed with different parameters.",
178
+ TOKEN_FEATURE_DISABLED: "This token feature is currently unavailable.",
179
+ BILLING_MODE_MISMATCH: "The billing mode does not match this operation. Please verify your account billing configuration.",
180
+ ACCOUNT_NOT_FOUND: "The settlement account was not found.",
181
+ ACCOUNT_SUSPENDED: "The settlement account is suspended. Please contact support.",
182
+ ACCOUNT_INSUFFICIENT_BALANCE: "The settlement account has insufficient balance for this operation.",
183
+ PAYMENT_ORDER_NOT_FOUND: "The payment order was not found.",
184
+ PAYMENT_ORDER_NOT_PAID: "The payment order has not been paid. Please complete payment and retry.",
185
+ PAYMENT_ORDER_MISMATCH: "The payment order does not match this order. Please check the --payment-order-id.",
186
+ PAYMENT_ORDER_ALREADY_CONSUMED: "The payment order has already been consumed by another booking.",
187
+ SERVICE_NOT_FOUND: "The requested service was not found.",
188
+ VEHICLE_UNAVAILABLE: "No vehicle is available for the requested trip. Please try a different time or class.",
189
+ QUOTE_EXPIRED: "The quote has expired. Please request a new quote and retry.",
190
+ BOOKING_FAILED: "The booking could not be completed. Please retry.",
191
+ CANCELLATION_NOT_ALLOWED: "This order cannot be cancelled in its current state.",
192
+ NOT_IMPLEMENTED: "This operation is not implemented yet."
193
+ };
194
+ function toErrorEnvelope(error) {
195
+ if (error instanceof CliError) {
196
+ return {
197
+ error: {
198
+ code: error.code,
199
+ code_num: codeNum(error.code),
200
+ message: error.message || "Unknown error",
201
+ ...error.requestId ? { request_id: error.requestId } : {},
202
+ ...error.upstream ? { upstream: error.upstream } : {}
203
+ }
204
+ };
233
205
  }
206
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unexpected error";
207
+ return {
208
+ error: {
209
+ code: "INTERNAL_ERROR",
210
+ code_num: codeNum("INTERNAL_ERROR"),
211
+ message: message || "Unknown error"
212
+ }
213
+ };
234
214
  }
235
-
236
- // src/core/version.ts
237
- import { readFileSync as readFileSync2 } from "fs";
238
- import { dirname, join as join2 } from "path";
239
- import { fileURLToPath } from "url";
240
- var UPGRADE_COMMAND = "npm install -g agenzo-merchant-cli@latest";
241
- var FALLBACK_VERSION = "0.1.0";
242
- var cached = null;
215
+ var UPGRADE_COMMAND = "npm install -g agenzo-admin-cli@latest";
216
+ var FALLBACK_VERSION = "0.1.1";
217
+ var cachedVersion = null;
243
218
  function getCurrentVersion() {
244
- if (cached !== null) return cached;
219
+ if (cachedVersion !== null) return cachedVersion;
245
220
  const here = dirname(fileURLToPath(import.meta.url));
246
221
  const candidates = [
247
- join2(here, "..", "package.json"),
248
- join2(here, "..", "..", "package.json")
222
+ join(here, "..", "package.json"),
223
+ // dist/ → project root
224
+ join(here, "..", "..", "package.json")
225
+ // src/utils/ → project root
249
226
  ];
250
227
  for (const p of candidates) {
251
228
  try {
252
- const pkg = JSON.parse(readFileSync2(p, "utf-8"));
229
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
253
230
  if (typeof pkg.version === "string") {
254
- cached = pkg.version;
255
- return cached;
231
+ cachedVersion = pkg.version;
232
+ return cachedVersion;
256
233
  }
257
234
  } catch {
258
235
  }
259
236
  }
260
- cached = FALLBACK_VERSION;
261
- return cached;
237
+ cachedVersion = FALLBACK_VERSION;
238
+ console.warn(
239
+ `[agenzo-admin-cli] package.json not found; using fallback version ${FALLBACK_VERSION}`
240
+ );
241
+ return cachedVersion;
262
242
  }
263
- function compareVersion(a, b) {
264
- const parse = (v) => v.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => parseInt(n, 10) || 0);
265
- const pa = parse(a);
266
- const pb = parse(b);
267
- for (let i = 0; i < 3; i++) {
268
- const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
269
- if (diff !== 0) return diff;
270
- }
271
- return 0;
243
+ function compareVersions(a, b) {
244
+ const parse = (v) => {
245
+ const clean = v.trim().replace(/^v/, "").split(/[-+]/)[0];
246
+ const parts = clean.split(".");
247
+ return [
248
+ parseInt(parts[0] ?? "0", 10) || 0,
249
+ parseInt(parts[1] ?? "0", 10) || 0,
250
+ parseInt(parts[2] ?? "0", 10) || 0
251
+ ];
252
+ };
253
+ const [a1, a2, a3] = parse(a);
254
+ const [b1, b2, b3] = parse(b);
255
+ if (a1 !== b1) return a1 - b1;
256
+ if (a2 !== b2) return a2 - b2;
257
+ return a3 - b3;
272
258
  }
273
- function enforceMinVersion(header) {
274
- const min = header?.trim();
275
- if (!min) return;
276
- const current = getCurrentVersion();
277
- if (compareVersion(current, min) < 0) {
278
- throw new CliError(
279
- ErrorCodes.CLI_OUTDATED,
280
- `CLI ${current} is below the required minimum ${min}.`,
281
- `Upgrade: ${UPGRADE_COMMAND}`,
282
- 2
283
- );
284
- }
259
+ function isBelow(current, minimum) {
260
+ return compareVersions(current, minimum) < 0;
285
261
  }
286
-
287
- // src/ride/quote.ts
288
- import { Command } from "commander";
289
-
290
- // src/core/api-client.ts
291
- var RIDE_PREFIX = "/api/v3/agent-pay";
292
262
  var ApiClient = class {
293
- host;
294
- apiKey;
263
+ baseUrl;
295
264
  timeout;
296
- constructor(options) {
297
- this.apiKey = options.apiKey;
298
- this.host = (options.host ?? getHost()).replace(/\/+$/, "");
299
- this.timeout = options.timeout ?? 3e4;
300
- }
301
- get(path, options = {}) {
302
- return this.request("GET", path, options);
303
- }
304
- post(path, options = {}) {
305
- return this.request("POST", path, options);
306
- }
307
- async request(method, path, options) {
308
- let url = `${this.host}${RIDE_PREFIX}${path}`;
309
- if (options.query) {
310
- const params = new URLSearchParams();
311
- for (const [key, value] of Object.entries(options.query)) {
312
- if (value !== void 0) params.set(key, String(value));
313
- }
314
- const qs = params.toString();
315
- if (qs) url += `?${qs}`;
316
- }
265
+ constructor(config) {
266
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
267
+ this.timeout = config.timeout ?? 6e4;
268
+ }
269
+ buildHeaders(auth) {
317
270
  const headers = {
318
- "X-Api-Key": this.apiKey,
319
- "User-Agent": `agenzo-merchant-cli/${getCurrentVersion()}`
271
+ "User-Agent": `agenzo-admin-cli/${getCurrentVersion()}`
320
272
  };
321
- if (options.body) headers["Content-Type"] = "application/json";
322
- if (options.idempotencyKey) headers["Idempotency-Key"] = options.idempotencyKey;
273
+ if (auth.type === "bearer") {
274
+ headers["Authorization"] = `Bearer ${auth.token}`;
275
+ } else if (auth.type === "api-key") {
276
+ headers["X-Api-Key"] = auth.key;
277
+ }
278
+ return headers;
279
+ }
280
+ async get(path, auth, params) {
281
+ let url = `${this.baseUrl}${path}`;
282
+ if (params && Object.keys(params).length > 0) {
283
+ const searchParams = new URLSearchParams(params);
284
+ url += `?${searchParams.toString()}`;
285
+ }
286
+ return this.request(url, {
287
+ method: "GET",
288
+ headers: this.buildHeaders(auth)
289
+ });
290
+ }
291
+ async post(path, auth, body, extraHeaders) {
292
+ const url = `${this.baseUrl}${path}`;
293
+ const headers = this.buildHeaders(auth);
294
+ headers["Content-Type"] = "application/json";
295
+ if (extraHeaders) {
296
+ Object.assign(headers, extraHeaders);
297
+ }
298
+ return this.request(url, {
299
+ method: "POST",
300
+ headers,
301
+ body: body ? JSON.stringify(body) : void 0
302
+ });
303
+ }
304
+ async request(url, init) {
323
305
  const controller = new AbortController();
324
- const timer = setTimeout(() => controller.abort(), this.timeout);
325
- let response;
306
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
326
307
  try {
327
- response = await fetch(url, {
328
- method,
329
- headers,
330
- body: options.body ? JSON.stringify(options.body) : void 0,
308
+ const response = await fetch(url, {
309
+ ...init,
331
310
  signal: controller.signal
332
311
  });
312
+ this.enforceMinVersion(response.headers.get("x-cli-min-version"));
313
+ const contentType = response.headers.get("content-type") ?? "";
314
+ let responseBody;
315
+ if (contentType.includes("application/json")) {
316
+ responseBody = await response.json();
317
+ } else {
318
+ const text = await response.text();
319
+ if (!response.ok) {
320
+ const statusMsg = this.friendlyStatusMessage(response.status);
321
+ return {
322
+ success: false,
323
+ errorCode: 0,
324
+ errorMessage: statusMsg,
325
+ statusCode: response.status
326
+ };
327
+ }
328
+ try {
329
+ responseBody = JSON.parse(text);
330
+ } catch {
331
+ return {
332
+ success: false,
333
+ errorCode: 0,
334
+ errorMessage: `Unexpected response from server (${response.status})`,
335
+ statusCode: response.status
336
+ };
337
+ }
338
+ }
339
+ const code = responseBody.code;
340
+ const message = responseBody.message;
341
+ const data = responseBody.data;
342
+ if (response.ok && (!code || code === "0000")) {
343
+ const payload = data ?? responseBody;
344
+ return { success: true, data: payload, message };
345
+ }
346
+ const errorCode = code ? parseInt(code, 10) : 0;
347
+ let errorMsg = message ?? response.statusText;
348
+ if (!errorMsg || errorMsg === response.statusText) {
349
+ const detail = responseBody.detail;
350
+ if (Array.isArray(detail)) {
351
+ errorMsg = detail.map((d) => {
352
+ const loc = d.loc?.slice(1).join(".") ?? "";
353
+ return loc ? `${loc}: ${d.msg}` : String(d.msg);
354
+ }).join("; ");
355
+ } else if (typeof detail === "string") {
356
+ errorMsg = detail;
357
+ }
358
+ }
359
+ const errorCodeStr = responseBody.error_code;
360
+ const rawData = responseBody.data;
361
+ const upstream = rawData?.upstream && typeof rawData.upstream === "object" ? rawData.upstream : void 0;
362
+ return {
363
+ success: false,
364
+ errorCode: isNaN(errorCode) ? 0 : errorCode,
365
+ errorMessage: errorMsg,
366
+ statusCode: response.status,
367
+ ...errorCodeStr ? { code: errorCodeStr } : typeof code === "string" && code ? { code } : {},
368
+ requestId: responseBody.request_id ?? responseBody.requestId,
369
+ ...upstream ? { upstream } : {}
370
+ };
333
371
  } catch (error) {
334
- if (error instanceof Error && error.name === "AbortError") {
335
- throw new CliError(
336
- ErrorCodes.TIMEOUT,
337
- `Request timed out after ${this.timeout}ms: ${url}`,
338
- "Retry, or check your network connection."
339
- );
372
+ if (error instanceof UpgradeRequiredError) {
373
+ throw error;
340
374
  }
341
- throw new CliError(
342
- ErrorCodes.NETWORK_ERROR,
343
- `Network request failed: ${url}`,
344
- "Check your network connection and the configured API host."
345
- );
375
+ if (error instanceof DOMException && error.name === "AbortError") {
376
+ throw new NetworkError(url, this.timeout);
377
+ }
378
+ throw new NetworkError(url, void 0, error instanceof Error ? error : void 0);
346
379
  } finally {
347
- clearTimeout(timer);
380
+ clearTimeout(timeoutId);
381
+ }
382
+ }
383
+ /**
384
+ * Validate that the running CLI meets the server-advertised minimum.
385
+ * Missing or empty header → skip (backward compat with servers that don't
386
+ * advertise yet, and clients hitting legacy proxies).
387
+ */
388
+ enforceMinVersion(headerValue) {
389
+ const minVersion = headerValue?.trim();
390
+ if (!minVersion) return;
391
+ const current = getCurrentVersion();
392
+ if (isBelow(current, minVersion)) {
393
+ throw new UpgradeRequiredError(current, minVersion, UPGRADE_COMMAND);
348
394
  }
349
- enforceMinVersion(response.headers.get("x-cli-min-version"));
350
- const text = await response.text();
351
- let envelope;
395
+ }
396
+ friendlyStatusMessage(status) {
397
+ const messages = {
398
+ 400: "Invalid request. Please check your input.",
399
+ 401: "Authentication failed. Please check your API key or login again.",
400
+ 403: "Access denied. You do not have permission for this operation.",
401
+ 404: "Resource not found. Please check the ID.",
402
+ 409: "Conflict. This resource may already exist.",
403
+ 422: "Invalid input. Please check the request parameters.",
404
+ 429: "Too many requests. Please wait and try again.",
405
+ 500: "Something went wrong on the server. Please try again later.",
406
+ 502: "Service is temporarily unavailable. Please try again in a moment.",
407
+ 503: "Service is temporarily unavailable. Please try again in a moment.",
408
+ 504: "The request took too long. Please try again."
409
+ };
410
+ return messages[status] ?? `Something went wrong (${status}). Please try again later.`;
411
+ }
412
+ };
413
+ var DEFAULT_API_PATH = "/api/admin/v1";
414
+ var DEFAULT_CONFIG = {
415
+ active_org: null,
416
+ active_developer_id: null,
417
+ api_host: "https://agent.everonet.com",
418
+ api_path: DEFAULT_API_PATH
419
+ };
420
+ var BUILTIN_PROFILES = {
421
+ production: "https://agent.everonet.com",
422
+ testing: "https://agent-test.everonet.com"
423
+ };
424
+ function resolveApiHost(host) {
425
+ if (BUILTIN_PROFILES[host]) {
426
+ return BUILTIN_PROFILES[host];
427
+ }
428
+ let url;
429
+ try {
430
+ url = new URL(host);
431
+ } catch {
432
+ throw new ValidationError(`Unknown profile or invalid URL: ${host}`);
433
+ }
434
+ if (url.protocol === "https:") {
435
+ return host;
436
+ }
437
+ if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
438
+ return host;
439
+ }
440
+ throw new ValidationError(
441
+ `Insecure API host is not allowed: ${host}. Use HTTPS, except for localhost or 127.0.0.1.`
442
+ );
443
+ }
444
+ var ConfigManager = class {
445
+ basePath;
446
+ configPath;
447
+ /**
448
+ * Product-line API prefix this CLI targets (e.g. /api/admin/v1,
449
+ * /api/token/v1). Injected per binary so the four CLIs share one api_host
450
+ * but route to different product lines. Overrides any persisted api_path.
451
+ */
452
+ apiPath;
453
+ constructor(basePath, apiPath) {
454
+ this.basePath = basePath ?? join4(homedir3(), ".agenzo-admin-cli");
455
+ this.configPath = join4(this.basePath, "config.json");
456
+ this.apiPath = apiPath ?? DEFAULT_API_PATH;
457
+ }
458
+ /** The resolved product-line API prefix for this CLI (per-binary constant). */
459
+ getApiPath() {
460
+ return this.apiPath;
461
+ }
462
+ async ensureDirectories() {
463
+ await mkdir3(this.basePath, { recursive: true, mode: 448 });
464
+ await chmod3(this.basePath, 448);
465
+ const credDir = join4(this.basePath, "credentials");
466
+ await mkdir3(credDir, { recursive: true, mode: 448 });
467
+ await chmod3(credDir, 448);
468
+ }
469
+ async load() {
352
470
  try {
353
- envelope = JSON.parse(text);
354
- } catch {
355
- throw new CliError(
356
- ErrorCodes.NETWORK_ERROR,
357
- `Unexpected non-JSON response (HTTP ${response.status}) from ${url}`
358
- );
471
+ const content = await readFile3(this.configPath, "utf-8");
472
+ try {
473
+ const raw = JSON.parse(content);
474
+ if (raw.api_base_url && !raw.api_host) {
475
+ const url = String(raw.api_base_url);
476
+ const pathIndex = url.indexOf("/api/");
477
+ raw.api_host = pathIndex > 0 ? url.slice(0, pathIndex) : url;
478
+ raw.api_path = pathIndex > 0 ? url.slice(pathIndex) : DEFAULT_CONFIG.api_path;
479
+ delete raw.api_base_url;
480
+ }
481
+ return {
482
+ active_org: raw.active_org ?? null,
483
+ active_developer_id: raw.active_developer_id ?? null,
484
+ api_host: raw.api_host ?? DEFAULT_CONFIG.api_host,
485
+ api_path: raw.api_path ?? DEFAULT_CONFIG.api_path
486
+ };
487
+ } catch {
488
+ throw new ConfigError(
489
+ `Invalid config file: ${this.configPath}`,
490
+ this.configPath
491
+ );
492
+ }
493
+ } catch (error) {
494
+ if (error instanceof ConfigError) throw error;
495
+ if (error.code === "ENOENT") {
496
+ return { ...DEFAULT_CONFIG };
497
+ }
498
+ throw error;
359
499
  }
360
- if (envelope.code !== void 0 && envelope.code !== "0000") {
361
- const code = envelope.code;
362
- const message = envelope.message ?? `Request failed (HTTP ${response.status})`;
363
- throw new CliError(code, message);
500
+ }
501
+ async save(config) {
502
+ await this.ensureDirectories();
503
+ await writeFile3(this.configPath, JSON.stringify(config, null, 2), {
504
+ encoding: "utf-8",
505
+ mode: 384
506
+ });
507
+ await chmod3(this.configPath, 384);
508
+ }
509
+ async getActiveOrg() {
510
+ const config = await this.load();
511
+ return config.active_org;
512
+ }
513
+ async setActiveOrg(orgId) {
514
+ const config = await this.load();
515
+ config.active_org = orgId;
516
+ await this.save(config);
517
+ }
518
+ async getApiBaseUrl() {
519
+ const config = await this.load();
520
+ const host = resolveApiHost(config.api_host).replace(/\/+$/, "");
521
+ const path = this.apiPath.startsWith("/") ? this.apiPath : `/${this.apiPath}`;
522
+ return `${host}${path}`;
523
+ }
524
+ async setApiHost(host) {
525
+ const config = await this.load();
526
+ config.api_host = resolveApiHost(host);
527
+ await this.save(config);
528
+ }
529
+ async getApiHost() {
530
+ const config = await this.load();
531
+ return resolveApiHost(config.api_host);
532
+ }
533
+ };
534
+ function exitCodeFor(error) {
535
+ if (!(error instanceof CliError)) {
536
+ return 1;
537
+ }
538
+ const code = error.code;
539
+ if (code === "UPGRADE_REQUIRED") {
540
+ return 2;
541
+ }
542
+ if (code === "CLIENT_ABORTED") {
543
+ return 5;
544
+ }
545
+ if (code.startsWith("AUTH_") || code.startsWith("KEY_")) {
546
+ return 3;
547
+ }
548
+ if (code === "UPSTREAM_ERROR" || code === "INTERNAL_ERROR" || code === "RATE_LIMITED") {
549
+ return 4;
550
+ }
551
+ return 1;
552
+ }
553
+ var STATUS_ICONS = {
554
+ success: "\u2713",
555
+ error: "\u2717",
556
+ info: "\u2139",
557
+ warning: "\u26A0",
558
+ loading: "\u280B"
559
+ };
560
+ var Formatter = class _Formatter {
561
+ /** Get display width of a string (CJK characters count as 2) */
562
+ static displayWidth(str) {
563
+ let width = 0;
564
+ for (const char of str) {
565
+ const code = char.codePointAt(0);
566
+ if (code >= 19968 && code <= 40959 || // CJK Unified
567
+ code >= 12288 && code <= 12351 || // CJK Punctuation
568
+ code >= 13312 && code <= 19903 || // CJK Extension A
569
+ code >= 65280 && code <= 65519 || // Fullwidth Forms
570
+ code >= 63744 && code <= 64255 || // CJK Compatibility
571
+ code >= 131072 && code <= 173791) {
572
+ width += 2;
573
+ } else {
574
+ width += 1;
575
+ }
364
576
  }
365
- return envelope.data;
577
+ return width;
578
+ }
579
+ /** Pad string to target display width (right-pad with spaces) */
580
+ static padEndDisplay(str, targetWidth) {
581
+ const currentWidth = _Formatter.displayWidth(str);
582
+ const padding = Math.max(0, targetWidth - currentWidth);
583
+ return str + " ".repeat(padding);
584
+ }
585
+ /** Pad string to target display width (left-pad with spaces) */
586
+ static padStartDisplay(str, targetWidth) {
587
+ const currentWidth = _Formatter.displayWidth(str);
588
+ const padding = Math.max(0, targetWidth - currentWidth);
589
+ return " ".repeat(padding) + str;
590
+ }
591
+ /** Table output for list commands — columns aligned by max width */
592
+ static table(headers, rows) {
593
+ const colWidths = headers.map((h, i) => {
594
+ const cellWidths = rows.map((r) => _Formatter.displayWidth(r[i] ?? ""));
595
+ return Math.max(_Formatter.displayWidth(h), ...cellWidths);
596
+ });
597
+ const sep = " ";
598
+ const headerLine = headers.map((h, i) => _Formatter.padEndDisplay(h, colWidths[i])).join(sep);
599
+ const divider = colWidths.map((w) => "-".repeat(w)).join(sep);
600
+ const dataLines = rows.map(
601
+ (row) => row.map((cell, i) => _Formatter.padEndDisplay(cell ?? "", colWidths[i])).join(sep)
602
+ );
603
+ return [headerLine, divider, ...dataLines].join("\n");
604
+ }
605
+ /** Key-value output for detail commands — keys left-aligned */
606
+ static keyValue(entries) {
607
+ const maxKeyWidth = Math.max(...entries.map(([k]) => _Formatter.displayWidth(k)));
608
+ return entries.map(([key, value]) => `${_Formatter.padEndDisplay(key, maxKeyWidth)} ${value}`).join("\n");
609
+ }
610
+ /** Status-prefixed message */
611
+ static status(type, message) {
612
+ return `${STATUS_ICONS[type]} ${message}`;
613
+ }
614
+ /** Mask sensitive info, keeping only the prefix */
615
+ static maskKey(key, prefixLength = 8) {
616
+ if (key.length <= prefixLength) {
617
+ return key;
618
+ }
619
+ return key.slice(0, prefixLength) + "*".repeat(key.length - prefixLength);
620
+ }
621
+ /**
622
+ * Format ISO 8601 timestamp to local timezone display.
623
+ * Automatically uses the user's system timezone.
624
+ * e.g. "2026-05-07T03:24:53+00:00" → "2026-05-07 11:24:53" (in CST)
625
+ * "2026-05-07T03:24:53+00:00" → "2026-05-06 20:24:53" (in PDT)
626
+ * Returns the original string if parsing fails or input is empty.
627
+ */
628
+ static formatTime(iso) {
629
+ if (!iso) return "";
630
+ const date = new Date(iso);
631
+ if (isNaN(date.getTime())) return iso;
632
+ const pad = (n) => String(n).padStart(2, "0");
633
+ const y = date.getFullYear();
634
+ const m = pad(date.getMonth() + 1);
635
+ const d = pad(date.getDate());
636
+ const h = pad(date.getHours());
637
+ const min = pad(date.getMinutes());
638
+ const s = pad(date.getSeconds());
639
+ const offsetMin = -date.getTimezoneOffset();
640
+ const sign = offsetMin >= 0 ? "+" : "-";
641
+ const absOffset = Math.abs(offsetMin);
642
+ const tzH = pad(Math.floor(absOffset / 60));
643
+ const tzM = pad(absOffset % 60);
644
+ return `${y}-${m}-${d} ${h}:${min}:${s} (UTC${sign}${tzH}:${tzM})`;
366
645
  }
367
646
  };
368
-
369
- // src/core/prompt-engine.ts
370
- import { confirm, input, password, select } from "@inquirer/prompts";
647
+ var DEFAULT_FORMAT = "table";
648
+ function isOutputFormat(value) {
649
+ return value === "json" || value === "table";
650
+ }
651
+ function resolveFormat(flag, env = process.env.AGENZO_FORMAT) {
652
+ if (flag !== void 0) {
653
+ return isOutputFormat(flag) ? flag : DEFAULT_FORMAT;
654
+ }
655
+ if (env !== void 0 && isOutputFormat(env)) {
656
+ return env;
657
+ }
658
+ return DEFAULT_FORMAT;
659
+ }
660
+ function render(result, opts) {
661
+ if (opts.format === "json") {
662
+ process.stdout.write(`${JSON.stringify(result.data, null, 2)}
663
+ `);
664
+ return;
665
+ }
666
+ process.stdout.write(`${result.text()}
667
+ `);
668
+ }
669
+ function notify(format, type, message) {
670
+ if (format === "json") {
671
+ return;
672
+ }
673
+ console.error(Formatter.status(type, message));
674
+ }
675
+ async function renderWithContext(result, opts, configManager) {
676
+ if (opts.format !== "json") {
677
+ render(result, opts);
678
+ return;
679
+ }
680
+ const config = await configManager.load();
681
+ const host = config.api_host.replace(/\/+$/, "");
682
+ const profile = Object.entries(BUILTIN_PROFILES).find(([, v]) => v === host)?.[0] ?? "custom";
683
+ const dataObj = result.data && typeof result.data === "object" ? result.data : { value: result.data };
684
+ const payload = {
685
+ profile,
686
+ endpoint: host,
687
+ ...dataObj
688
+ };
689
+ render({ ...result, data: payload }, opts);
690
+ }
371
691
  var PromptEngine = class {
372
- static async resolveInput(flagValue, options) {
373
- if (flagValue !== void 0) return flagValue;
374
- if (options.type === "password") {
375
- return password({ message: options.message, mask: "*" });
692
+ /** Return flagValue directly if provided, otherwise prompt interactively */
693
+ static async resolveInput(flagValue, config) {
694
+ if (flagValue !== void 0) {
695
+ return flagValue;
376
696
  }
377
- if (options.type === "select" && options.choices) {
378
- return select({ message: options.message, choices: options.choices });
697
+ if (config.type === "password") {
698
+ return password({ message: config.message, mask: "*" });
379
699
  }
380
- return input({ message: options.message, validate: options.validate });
700
+ if (config.type === "select" && config.choices) {
701
+ return select({
702
+ message: config.message,
703
+ choices: config.choices
704
+ });
705
+ }
706
+ return input({
707
+ message: config.message,
708
+ validate: config.validate
709
+ });
381
710
  }
382
- /** Confirmation prompt; auto-approves when `--yes` is set. */
383
- static async confirm(message, yes) {
384
- if (yes) return true;
385
- return confirm({ message });
711
+ };
712
+
713
+ // src/ride-elife/watch.ts
714
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
715
+ "At destination",
716
+ "Cancelled",
717
+ "Rejected",
718
+ "Customer no show",
719
+ "Driver no show"
720
+ ]);
721
+ var DEFAULT_WATCH_INTERVAL_SECONDS = 5;
722
+ var DEFAULT_WATCH_TIMEOUT_SECONDS = 600;
723
+ function isTerminalStatus(status) {
724
+ return status !== void 0 && status !== null && TERMINAL_STATUSES.has(status);
725
+ }
726
+ function statusOf(data) {
727
+ const status = data.status;
728
+ return typeof status === "string" ? status : void 0;
729
+ }
730
+ async function runWatch(deps, opts) {
731
+ const deadline = deps.now() + opts.timeoutMs;
732
+ for (; ; ) {
733
+ const data = await deps.fetchStatus();
734
+ deps.writeLine(data);
735
+ const status = statusOf(data);
736
+ if (isTerminalStatus(status)) {
737
+ return;
738
+ }
739
+ if (deps.now() + opts.intervalMs >= deadline) {
740
+ const timeoutLine = {
741
+ watch_status: "timeout",
742
+ message: `Polling stopped after ${opts.timeoutMs / 1e3}s without reaching a terminal status.`,
743
+ last_status: status ?? null
744
+ };
745
+ deps.writeLine(timeoutLine);
746
+ return;
747
+ }
748
+ await deps.sleep(opts.intervalMs);
386
749
  }
750
+ }
751
+ function resolveSeconds(value, fallback) {
752
+ if (value === void 0) return fallback;
753
+ const n = Number(value);
754
+ return Number.isFinite(n) && n > 0 ? n : fallback;
755
+ }
756
+ var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
757
+ var ndjsonWriteLine = (record) => {
758
+ process.stdout.write(`${JSON.stringify(record)}
759
+ `);
387
760
  };
761
+ async function watchRideStatus(apiClient, apiKey, orderId, opts) {
762
+ const path = `/ride/${encodeURIComponent(orderId)}/status`;
763
+ await runWatch(
764
+ {
765
+ fetchStatus: async () => {
766
+ const result = await apiClient.get(path, {
767
+ type: "api-key",
768
+ key: apiKey
769
+ });
770
+ if (!result.success) {
771
+ throw CliError.fromApi(result, { auth: "api-key" });
772
+ }
773
+ return result.data;
774
+ },
775
+ writeLine: ndjsonWriteLine,
776
+ sleep: realSleep,
777
+ now: () => Date.now()
778
+ },
779
+ {
780
+ intervalMs: opts.intervalSeconds * 1e3,
781
+ timeoutMs: opts.timeoutSeconds * 1e3
782
+ }
783
+ );
784
+ }
388
785
 
389
- // src/utils/cli-help.ts
390
- function explicitFormat() {
391
- const argv = process.argv;
786
+ // src/verb-schema.ts
787
+ var CLI_NAME = "agenzo-merchant-cli";
788
+ var NOUN = "ride-elife";
789
+ function wantsJsonSchema(argv = process.argv) {
392
790
  for (let i = 0; i < argv.length; i++) {
393
791
  const a = argv[i];
394
- if (a === "--format=json" || a === "--format" && argv[i + 1] === "json") return "json";
395
- if (a === "--format=table" || a === "--format" && argv[i + 1] === "table") return "table";
792
+ if (a === "--format=json") return true;
793
+ if (a === "--format" && argv[i + 1] === "json") return true;
396
794
  }
397
- return void 0;
795
+ return false;
796
+ }
797
+ function emitSchema(schema) {
798
+ console.log(JSON.stringify(schema, null, 2));
398
799
  }
399
800
  function attachSchemaHelp(cmd, schema) {
400
801
  const baseHelp = cmd.helpInformation.bind(cmd);
401
- cmd.helpInformation = (ctx) => {
402
- const format = explicitFormat();
403
- if (!format) return baseHelp(ctx);
404
- emitSchema(schema, format);
802
+ cmd.helpInformation = (context) => {
803
+ if (!wantsJsonSchema()) return baseHelp(context);
804
+ emitSchema(schema);
405
805
  return "";
406
806
  };
407
807
  return cmd;
408
808
  }
409
-
410
- // src/ride/quote.ts
411
809
  var quoteSchema = {
412
- description: "Request fare quotes for a ride between two points.",
413
- params: {
414
- "pickup-lat": "number \u2014 pickup latitude",
415
- "pickup-lng": "number \u2014 pickup longitude",
416
- "pickup-name": "string \u2014 pickup location name",
417
- "dropoff-lat": "number \u2014 dropoff latitude",
418
- "dropoff-lng": "number \u2014 dropoff longitude",
419
- "dropoff-name": "string \u2014 dropoff location name",
420
- "pickup-time": 'number|string \u2014 epoch seconds, or "now"',
421
- "passenger-name": "string \u2014 optional, lead passenger full name",
422
- "passenger-phone": "string \u2014 optional, passenger phone in E.164 format",
423
- "passenger-count": "number \u2014 optional passenger count",
424
- "luggage-count": "number \u2014 optional luggage count",
425
- "passenger-email": "string \u2014 optional passenger email",
426
- "children-count": "number \u2014 optional children count"
810
+ cli: CLI_NAME,
811
+ noun: NOUN,
812
+ verb: "quote",
813
+ description: "Request fare quotes for a ride between two points",
814
+ flags: {
815
+ "pickup-lat": { type: "float", required: true, description: "Pickup latitude", constraints: "-90 to 90" },
816
+ "pickup-lng": { type: "float", required: true, description: "Pickup longitude", constraints: "-180 to 180" },
817
+ "pickup-name": { type: "string", required: true, description: "Pickup location name" },
818
+ "dropoff-lat": { type: "float", required: true, description: "Dropoff latitude", constraints: "-90 to 90" },
819
+ "dropoff-lng": { type: "float", required: true, description: "Dropoff longitude", constraints: "-180 to 180" },
820
+ "dropoff-name": { type: "string", required: true, description: "Dropoff location name" },
821
+ "pickup-time": {
822
+ type: "int|string",
823
+ required: true,
824
+ description: "Epoch seconds or the literal 'now'"
825
+ },
826
+ "passenger-name": { type: "string", required: false, description: "Lead passenger full name" },
827
+ "passenger-phone": { type: "string", required: false, description: "Passenger phone in E.164 format (e.g. +14155551234)" },
828
+ "passenger-count": { type: "int", required: false, description: "Number of passengers" },
829
+ "luggage-count": { type: "int", required: false, description: "Number of luggage items" },
830
+ "passenger-email": { type: "string", required: false, description: "Passenger email address" },
831
+ "children-count": { type: "int", required: false, description: "Number of children" }
427
832
  },
428
833
  response: {
429
- vehicle_classes: "VehicleClass[] \u2014 each: vehicle_class, price{amount,currency,quote_id}, passenger_capacity, luggage_capacity (amount is a decimal in the currency standard unit, e.g. 42.50 = $42.50)",
430
- meet_and_greet: "object|null \u2014 { available: boolean, price: { amount, currency } } when applicable",
431
- is_airport_transfer: "boolean",
432
- airport_direction: "string|null \u2014 pickup/dropoff direction for airport transfers"
834
+ vehicle_classes: {
835
+ type: "array",
836
+ description: "Available vehicle options with pricing",
837
+ items: {
838
+ vehicle_class: { type: "string", description: "Sedan / SUV / MPV-5 / MPV-7 / Van / Luxury / Train" },
839
+ "price.amount": { type: "float", description: "Price in currency units (e.g. 42.50), NOT cents" },
840
+ "price.currency": { type: "string", description: "ISO 4217 (e.g. USD)" },
841
+ "price.quote_id": { type: "string", description: "Use this in `ride-elife book --quote-id`" },
842
+ passenger_capacity: { type: "int", description: "Max passengers" },
843
+ luggage_capacity: { type: "int", description: "Max luggage items" }
844
+ }
845
+ },
846
+ meet_and_greet: { type: "object|null", description: "Meet & greet availability and price" },
847
+ is_airport_transfer: { type: "bool", description: "Whether this is an airport transfer" },
848
+ airport_direction: { type: "string|null", description: "pickup / dropoff / null" }
849
+ },
850
+ example: {
851
+ command: 'agenzo-merchant-cli ride-elife quote --pickup-lat 37.7937 --pickup-lng -122.3956 --pickup-name "1 Market St" --dropoff-lat 37.6213 --dropoff-lng -122.3790 --dropoff-name "SFO Airport" --pickup-time now',
852
+ output_summary: "Returns vehicle_classes[] with a quote_id per option. Use quote_id and vehicle_class for booking."
853
+ },
854
+ error_recovery: {
855
+ VEHICLE_UNAVAILABLE: "No vehicles for this route/time. Try a different pickup-time or location. Do NOT blindly retry.",
856
+ PARAM_INVALID: "Fix the offending flag (coordinates must be numbers; required pickup/dropoff/pickup-time present), then retry."
433
857
  }
434
858
  };
859
+ var bookSchema = {
860
+ cli: CLI_NAME,
861
+ noun: NOUN,
862
+ verb: "book",
863
+ description: "Book a ride using a quote_id returned by a previous quote call",
864
+ flags: {
865
+ "quote-id": { type: "string", required: true, description: "Quote id from quote response (vehicle_classes[].price.quote_id)" },
866
+ "vehicle-class": { type: "string", required: true, description: "Vehicle class: Sedan / SUV / MPV-5 / MPV-7 / Van / Luxury / Train" },
867
+ "price-amount": { type: "float", required: true, description: "Fare in decimal currency units (NOT cents), from the quote" },
868
+ "price-currency": { type: "string", required: false, default: "USD", description: "ISO 4217 currency code" },
869
+ "payment-order-id": {
870
+ type: "string",
871
+ required: "conditional",
872
+ description: "Required when billing_mode=pay_per_call (a PAID payment order). Forbidden when billing_mode=monthly_settlement"
873
+ },
874
+ "passenger-name": { type: "string", required: true, description: "Passenger full name" },
875
+ "passenger-phone": { type: "string", required: true, description: "Passenger phone in E.164 format (e.g. +14155551234)" },
876
+ "passenger-email": { type: "string", required: true, description: "Passenger email (required by eLife for booking)" },
877
+ "luggage-count": { type: "int", required: false, description: "Number of luggage items" },
878
+ "special-requests": { type: "string", required: false, description: "Free-text special requests or notes" },
879
+ "pickup-lat": { type: "float", required: false, description: "Pickup latitude (must match the quote)", constraints: "-90 to 90" },
880
+ "pickup-lng": { type: "float", required: false, description: "Pickup longitude (must match the quote)", constraints: "-180 to 180" },
881
+ "pickup-name": { type: "string", required: false, description: "Pickup location name (must match the quote)" },
882
+ "dropoff-lat": { type: "float", required: false, description: "Dropoff latitude (must match the quote)", constraints: "-90 to 90" },
883
+ "dropoff-lng": { type: "float", required: false, description: "Dropoff longitude (must match the quote)", constraints: "-180 to 180" },
884
+ "dropoff-name": { type: "string", required: false, description: "Dropoff location name (must match the quote)" },
885
+ "pickup-time": { type: "int|string", required: false, description: "Epoch seconds or 'now' (must match the quote)" },
886
+ "meet-and-greet": { type: "bool", required: false, default: false, description: "Enable meet & greet service" },
887
+ "meet-and-greet-price": { type: "float", required: false, description: "Meet & greet surcharge from the quote (meet_and_greet.price.amount)" },
888
+ "welcome-sign": { type: "string", required: false, description: "Welcome sign text (meet & greet)" },
889
+ "child-seat-count": { type: "int", required: false, description: "Number of child seats needed", constraints: "0 to 5" },
890
+ "infant-seat-count": { type: "int", required: false, description: "Number of infant seats needed", constraints: "0 to 5" },
891
+ "toddler-seat-count": { type: "int", required: false, description: "Number of toddler seats needed", constraints: "0 to 5" },
892
+ "arrival-flight-no": { type: "string", required: false, description: "Arrival flight number (airport pickup)" },
893
+ "arrival-airline": { type: "string", required: false, description: "Arrival airline name" },
894
+ "departure-flight-no": { type: "string", required: false, description: "Departure flight number (airport dropoff)" },
895
+ "departure-airline": { type: "string", required: false, description: "Departure airline name" },
896
+ "idempotency-key": {
897
+ type: "string",
898
+ required: true,
899
+ description: "Unique key (1-128 chars [A-Za-z0-9_-]) forwarded verbatim as the Idempotency-Key header; never auto-generated"
900
+ }
901
+ },
902
+ response: {
903
+ ride_id: { type: "string|int", description: "Ride identifier \u2014 use for `ride-elife get` / `ride-elife cancel`" },
904
+ order_id: { type: "string|int", description: "Internal order id for reconciliation" },
905
+ status: { type: "string", description: "Ride status (case-sensitive server casing)" },
906
+ is_scheduled: { type: "bool", description: "true = scheduled/airport ride; false = realtime" },
907
+ order_type: { type: "string", description: "'realtime' or 'airport'" },
908
+ price: { type: "object", description: "{ amount, currency, quote_id }" },
909
+ payment_status: { type: "string", description: "PAID (pay_per_call) or ON_ACCOUNT (monthly_settlement)" },
910
+ billing_entry_id: { type: "string|absent", description: "Only in monthly_settlement mode" },
911
+ payment_order_id: { type: "string|absent", description: "Only in pay_per_call mode (echoes the request)" }
912
+ },
913
+ example: {
914
+ command: 'agenzo-merchant-cli ride-elife book --quote-id qte_01HZXD --vehicle-class Sedan --price-amount 42.50 --passenger-name "Alice" --passenger-phone +14155551234 --idempotency-key book-123',
915
+ output_summary: "Returns ride_id. Use ride_id in `ride-elife get` to poll status."
916
+ },
917
+ error_recovery: {
918
+ QUOTE_EXPIRED: "Re-invoke 'quote' for a fresh quote_id, then retry 'book' with the SAME --idempotency-key.",
919
+ BILLING_MODE_MISMATCH: "Check Developer.billing_mode. pay_per_call requires --payment-order-id; monthly_settlement forbids it.",
920
+ PAYMENT_ORDER_NOT_PAID: "Wait for the payment order status=PAID, then retry with the SAME --idempotency-key.",
921
+ PAYMENT_ORDER_ALREADY_CONSUMED: "Create a new payment order, then retry with the new payment-order-id and a NEW --idempotency-key.",
922
+ ACCOUNT_INSUFFICIENT_BALANCE: "Top up the settlement account (offline) or pick a cheaper vehicle_class, then retry.",
923
+ BOOKING_FAILED: "elife rejected after settlement. The message contains a ref: for manual reconciliation. Do NOT auto-retry.",
924
+ PARAM_IDEMPOTENCY_KEY_REQUIRED: "Supply --idempotency-key (1-128 chars [A-Za-z0-9_-]); the CLI never generates one under --yes."
925
+ }
926
+ };
927
+ var rideGetSchema = {
928
+ cli: CLI_NAME,
929
+ noun: NOUN,
930
+ verb: "get",
931
+ description: "Retrieve a ride order status by id; with --watch, poll until a terminal status",
932
+ flags: {
933
+ "order-id": { type: "string", required: true, description: "The numeric ride_id (e.g. 4112961) \u2014 use the `ride_id` field from book/list-orders responses, NOT the rio_... order_id" },
934
+ watch: { type: "bool", required: false, default: false, description: "Poll until a terminal status, emitting one NDJSON line per update" },
935
+ "watch-interval": { type: "int", required: false, default: DEFAULT_WATCH_INTERVAL_SECONDS, description: "Seconds between polls when --watch is set" },
936
+ "watch-timeout": { type: "int", required: false, default: 600, description: "Max seconds to poll before giving up" }
937
+ },
938
+ response: {
939
+ ride_id: { type: "string|int", description: "Ride identifier" },
940
+ status: { type: "string", description: "Pending / Accepted / On my way / Waiting / On board / At destination / Rejected / Cancelled / Customer no show / Driver no show" },
941
+ source: { type: "string", description: "'local_cache' when served from the local fallback; 'mock' in sandbox" },
942
+ is_scheduled: { type: "bool", description: "true = scheduled/airport ride" },
943
+ from_location: { type: "object", description: "Pickup { lat, lng, name, address } (v3 snake_case, NOT elife from/to)" },
944
+ to_location: { type: "object", description: "Dropoff { lat, lng, name, address }" },
945
+ pickup_time: { type: "int|string", description: "Pickup time" },
946
+ vehicle_class: { type: "string|null", description: "Vehicle class" },
947
+ price: { type: "object", description: "{ amount, currency, quote_id }" },
948
+ final_amount: { type: "float|null", description: "Final settled fare (realtime, after final-fare settlement)" },
949
+ final_settlement_status: { type: "string", description: "pending / settled / no_adjustment / settlement_pending / not_applicable" },
950
+ driver: { type: "object|null", description: "{ name, phone_number } \u2014 available after a driver is assigned" },
951
+ vehicle: { type: "object|null", description: "{ make, model, color, license_plate, image_url } \u2014 available after a driver is assigned" },
952
+ created_at: { type: "string", description: "Order creation time" }
953
+ },
954
+ example: {
955
+ command: "agenzo-merchant-cli ride-elife get --order-id EL-20260527-1023456",
956
+ output_summary: "Returns current status and driver/vehicle info. Terminal statuses: At destination, Cancelled, Rejected, Customer no show, Driver no show."
957
+ },
958
+ polling: {
959
+ recommended_interval_seconds: DEFAULT_WATCH_INTERVAL_SECONDS,
960
+ terminal_statuses: [...TERMINAL_STATUSES],
961
+ in_progress_statuses: ["Pending", "Accepted", "On my way", "Waiting", "On board"],
962
+ field_availability: {
963
+ driver: "Populated from status 'Accepted' onwards (after a driver is dispatched). Null in 'Pending'.",
964
+ vehicle: "Populated from status 'Accepted' onwards. Null in 'Pending'."
965
+ }
966
+ },
967
+ error_recovery: {
968
+ VEHICLE_UNAVAILABLE: "Order not found. Verify --order-id is the ride_id from book. Do NOT retry.",
969
+ BOOKING_FAILED: "Transient elife error. Retry once after 5s."
970
+ }
971
+ };
972
+ var cancelSchema = {
973
+ cli: CLI_NAME,
974
+ noun: NOUN,
975
+ verb: "cancel",
976
+ description: "Cancel a ride order by id (a cancellation fee may apply depending on ride status)",
977
+ flags: {
978
+ "order-id": { type: "string", required: true, description: "The numeric ride_id (e.g. 4112961) to cancel \u2014 use the `ride_id` field from book/list-orders responses, NOT the rio_... order_id" },
979
+ "idempotency-key": {
980
+ type: "string",
981
+ required: true,
982
+ description: "Unique key (1-128 chars [A-Za-z0-9_-]) forwarded verbatim as the Idempotency-Key header; never auto-generated"
983
+ }
984
+ },
985
+ response: {
986
+ ride_id: { type: "string|int", description: "Cancelled ride id" },
987
+ ride_stat: { type: "string", description: "Status after cancellation (typically 'Cancelled')" },
988
+ cancellation: {
989
+ type: "object|null",
990
+ description: "Cancellation details",
991
+ properties: {
992
+ cancellation_fee: { type: "float", description: "Fee charged for cancellation" },
993
+ reversal_amount: { type: "float", description: "Amount reversed/refunded" },
994
+ currency: { type: "string", description: "ISO 4217 currency code" }
995
+ }
996
+ },
997
+ refund_amount: { type: "float", description: "Amount credited back to the settlement balance (paid \u2212 cancellation_fee)" }
998
+ },
999
+ example: {
1000
+ command: "agenzo-merchant-cli ride-elife cancel --order-id EL-20260527-1023456 --idempotency-key cancel-456",
1001
+ output_summary: "Returns cancellation fee and refund amount."
1002
+ },
1003
+ error_recovery: {
1004
+ CANCELLATION_NOT_ALLOWED: "The ride is in a non-cancellable state. Do NOT retry; query status with `ride-elife get`.",
1005
+ PARAM_IDEMPOTENCY_KEY_REQUIRED: "Supply --idempotency-key (1-128 chars [A-Za-z0-9_-]); the CLI never generates one under --yes."
1006
+ }
1007
+ };
1008
+ var listOrdersSchema = {
1009
+ cli: CLI_NAME,
1010
+ noun: NOUN,
1011
+ verb: "list-orders",
1012
+ description: "List previously placed ride orders with optional filters and pagination",
1013
+ flags: {
1014
+ status: { type: "string", required: false, description: "Filter by ride status (case-sensitive server casing)" },
1015
+ "order-type": { type: "string", required: false, description: "Filter by order type (e.g. realtime / airport)" },
1016
+ page: { type: "int", required: false, default: 1, description: "Page number", constraints: ">= 1" },
1017
+ "page-size": { type: "int", required: false, default: 20, description: "Items per page", constraints: ">= 1" }
1018
+ },
1019
+ response: {
1020
+ orders: {
1021
+ type: "array",
1022
+ description: "List of ride orders (slim list-item shape)",
1023
+ items: {
1024
+ order_id: { type: "string", description: "Internal order id" },
1025
+ ride_id: { type: "string", description: "Ride id (same as ride_id from book)" },
1026
+ status: { type: "string", description: "Current ride status" },
1027
+ vehicle_class: { type: "string", description: "Vehicle class" },
1028
+ is_scheduled: { type: "bool", description: "true = scheduled/airport, false = realtime" },
1029
+ scheduled_at: { type: "string", description: "ISO 8601 datetime; empty for realtime orders" },
1030
+ price_amount: { type: "float|null", description: "Price in decimal currency units (NOT cents)" },
1031
+ final_amount: { type: "float|null", description: "Final settled amount" },
1032
+ price_currency: { type: "string", description: "ISO 4217 currency code" },
1033
+ payment_status: { type: "string", description: "Payment status" },
1034
+ final_settlement_status: { type: "string", description: "pending / settled / no_adjustment / settlement_pending / not_applicable" },
1035
+ cancellation_fee: { type: "float|null", description: "Cancellation fee for cancelled orders; null otherwise" },
1036
+ provider: { type: "string", description: "Service provider (elife)" },
1037
+ created_at: { type: "string|null", description: "Order creation time (ISO 8601)" },
1038
+ updated_at: { type: "string|null", description: "Last update time (ISO 8601)" }
1039
+ }
1040
+ },
1041
+ total: { type: "int", description: "Total matching orders" },
1042
+ page: { type: "int", description: "Current page" },
1043
+ page_size: { type: "int", description: "Items per page" }
1044
+ },
1045
+ example: {
1046
+ command: "agenzo-merchant-cli ride-elife list-orders --status Pending --page 1 --page-size 10",
1047
+ output_summary: "Returns a paginated list of ride orders with status and amount info."
1048
+ },
1049
+ error_recovery: {
1050
+ INTERNAL_ERROR: "Transient backend error. Retry once after a short delay.",
1051
+ PARAM_INVALID: "Ensure --page / --page-size are positive integers, then retry."
1052
+ }
1053
+ };
1054
+
1055
+ // src/ride-elife/quote.ts
435
1056
  function need(value, flag) {
436
- if (value === void 0) throw new CliError("PARAM_REQUIRED", `Missing required --${flag}.`);
1057
+ if (value === void 0) {
1058
+ throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
1059
+ }
437
1060
  return value;
438
1061
  }
439
1062
  function num(value, flag) {
440
1063
  const n = Number(need(value, flag));
441
- if (!Number.isFinite(n)) throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
1064
+ if (!Number.isFinite(n)) {
1065
+ throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
1066
+ }
442
1067
  return n;
443
1068
  }
444
- function buildQuoteCommand() {
445
- const cmd = new Command("quote").description(quoteSchema.description).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").action(async (_opts, command) => {
446
- const o = command.optsWithGlobals();
447
- const format = o.format === "table" ? "table" : "json";
448
- const apiKey = await PromptEngine.resolveInput(o.apiKey, {
449
- message: "API key:",
1069
+ function formatQuote(data) {
1070
+ const classes = data.vehicle_classes ?? [];
1071
+ if (classes.length === 0) {
1072
+ return Formatter.status("info", "No vehicle classes available for this route");
1073
+ }
1074
+ const headers = ["Vehicle Class", "Price", "Currency", "Quote ID", "Pax", "Luggage"];
1075
+ const rows = classes.map((vc) => [
1076
+ String(vc.vehicle_class ?? "-"),
1077
+ String(vc.price?.amount ?? "-"),
1078
+ String(vc.price?.currency ?? "-"),
1079
+ String(vc.price?.quote_id ?? "-"),
1080
+ String(vc.passenger_capacity ?? "-"),
1081
+ String(vc.luggage_capacity ?? "-")
1082
+ ]);
1083
+ const lines = [
1084
+ ["Airport transfer", String(data.is_airport_transfer ?? false)]
1085
+ ];
1086
+ if (data.airport_direction) {
1087
+ lines.push(["Airport direction", String(data.airport_direction)]);
1088
+ }
1089
+ if (data.meet_and_greet) {
1090
+ const mg = data.meet_and_greet;
1091
+ const mgPrice = mg.price ? ` (${mg.price.amount} ${mg.price.currency})` : "";
1092
+ lines.push(["Meet & greet", `${mg.available ? "available" : "unavailable"}${mgPrice}`]);
1093
+ }
1094
+ return `${Formatter.table(headers, rows)}
1095
+ ${Formatter.keyValue(lines)}`;
1096
+ }
1097
+ function registerQuoteCommand(parent, deps) {
1098
+ 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");
1099
+ attachSchemaHelp(cmd, quoteSchema);
1100
+ cmd.action(async () => {
1101
+ const opts = cmd.optsWithGlobals();
1102
+ const format = resolveFormat(opts.format);
1103
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1104
+ message: "API Key:",
450
1105
  type: "password"
451
1106
  });
452
1107
  const body = {
453
1108
  pickup: {
454
- lat: num(o.pickupLat, "pickup-lat"),
455
- lng: num(o.pickupLng, "pickup-lng"),
456
- name: need(o.pickupName, "pickup-name")
1109
+ lat: num(opts.pickupLat, "pickup-lat"),
1110
+ lng: num(opts.pickupLng, "pickup-lng"),
1111
+ name: need(opts.pickupName, "pickup-name")
457
1112
  },
458
1113
  dropoff: {
459
- lat: num(o.dropoffLat, "dropoff-lat"),
460
- lng: num(o.dropoffLng, "dropoff-lng"),
461
- name: need(o.dropoffName, "dropoff-name")
1114
+ lat: num(opts.dropoffLat, "dropoff-lat"),
1115
+ lng: num(opts.dropoffLng, "dropoff-lng"),
1116
+ name: need(opts.dropoffName, "dropoff-name")
462
1117
  },
463
- pickup_time: o.pickupTime === "now" ? "now" : num(o.pickupTime, "pickup-time")
1118
+ pickup_time: opts.pickupTime === "now" ? "now" : num(opts.pickupTime, "pickup-time")
464
1119
  };
465
- if (o.passengerName) body.passenger_name = o.passengerName;
466
- if (o.passengerPhone) body.passenger_phone = o.passengerPhone;
467
- if (o.passengerCount !== void 0) body.passenger_count = num(o.passengerCount, "passenger-count");
468
- if (o.luggageCount !== void 0) body.luggage_count = num(o.luggageCount, "luggage-count");
469
- if (o.childrenCount !== void 0) body.children_count = num(o.childrenCount, "children-count");
470
- if (o.passengerEmail !== void 0) body.passenger_email = o.passengerEmail;
471
- const client = new ApiClient({ apiKey });
472
- const spinner = createSpinner("Fetching quotes...");
473
- try {
474
- const data = await client.post("/ride/quote", { body });
475
- spinner.stop();
476
- emit(data, format);
477
- } catch (err) {
478
- spinner.fail("Quote request failed");
479
- throw err;
1120
+ if (opts.passengerName) body.passenger_name = opts.passengerName;
1121
+ if (opts.passengerPhone) body.passenger_phone = opts.passengerPhone;
1122
+ if (opts.passengerCount !== void 0) {
1123
+ body.passenger_count = num(opts.passengerCount, "passenger-count");
1124
+ }
1125
+ if (opts.luggageCount !== void 0) {
1126
+ body.luggage_count = num(opts.luggageCount, "luggage-count");
480
1127
  }
1128
+ if (opts.childrenCount !== void 0) {
1129
+ body.children_count = num(opts.childrenCount, "children-count");
1130
+ }
1131
+ if (opts.passengerEmail !== void 0) {
1132
+ body.passenger_email = opts.passengerEmail;
1133
+ }
1134
+ notify(format, "loading", "Fetching quotes...");
1135
+ const result = await deps.apiClient.post(
1136
+ "/ride/quote",
1137
+ { type: "api-key", key: apiKey },
1138
+ body
1139
+ );
1140
+ if (!result.success) {
1141
+ throw CliError.fromApi(result, { auth: "api-key" });
1142
+ }
1143
+ const data = result.data;
1144
+ const configManager = new ConfigManager();
1145
+ const commandResult = {
1146
+ data,
1147
+ text: () => formatQuote(data)
1148
+ };
1149
+ await renderWithContext(commandResult, { format }, configManager);
481
1150
  });
482
- return attachSchemaHelp(cmd, quoteSchema);
483
1151
  }
484
1152
 
485
- // src/ride/book.ts
486
- import { Command as Command2 } from "commander";
1153
+ // src/ride-elife/book.ts
1154
+ import { confirm } from "@inquirer/prompts";
487
1155
 
488
- // src/utils/idempotency.ts
489
- import { input as input2 } from "@inquirer/prompts";
490
- var KEY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
491
- var RULE = "Use 1-128 characters from [A-Za-z0-9_-].";
1156
+ // src/idempotency.ts
1157
+ var IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
1158
+ var IDEMPOTENCY_KEY_RULE = "Use 1-128 characters from [A-Za-z0-9_-].";
492
1159
  function normalizeIdempotencyKey(key) {
493
1160
  const trimmed = key.trim();
494
- if (!KEY_PATTERN.test(trimmed)) {
1161
+ if (!IDEMPOTENCY_KEY_PATTERN.test(trimmed)) {
495
1162
  throw new CliError(
496
- ErrorCodes.PARAM_IDEMPOTENCY_KEY_REQUIRED,
497
- `Invalid idempotency key: "${key}".`,
498
- RULE
1163
+ "PARAM_INVALID",
1164
+ `Invalid idempotency key: "${key}". ${IDEMPOTENCY_KEY_RULE}`
499
1165
  );
500
1166
  }
501
1167
  return trimmed;
502
1168
  }
503
- async function resolveIdempotencyKey(flagValue, options) {
504
- if (flagValue !== void 0) return normalizeIdempotencyKey(flagValue);
505
- if (options.yes) {
506
- throw new CliError(
507
- ErrorCodes.PARAM_IDEMPOTENCY_KEY_REQUIRED,
508
- "An idempotency key is required for write operations.",
509
- "Pass --idempotency-key <key>; the CLI never auto-generates one."
510
- );
1169
+ async function resolveIdempotencyKey(flagValue, opts) {
1170
+ if (flagValue !== void 0) {
1171
+ return normalizeIdempotencyKey(flagValue);
1172
+ }
1173
+ if (opts.yes) {
1174
+ throw new IdempotencyKeyRequiredError(opts.commandPath);
511
1175
  }
512
- const entered = await input2({
513
- message: "Idempotency key:",
514
- validate: (v) => KEY_PATTERN.test(v.trim()) || RULE
1176
+ const entered = await PromptEngine.resolveInput(void 0, {
1177
+ message: "Idempotency key (unique per write, for safe retry):",
1178
+ validate: (v) => IDEMPOTENCY_KEY_PATTERN.test(v.trim()) || IDEMPOTENCY_KEY_RULE
515
1179
  });
516
1180
  return normalizeIdempotencyKey(entered);
517
1181
  }
518
1182
 
519
- // src/ride/book.ts
520
- var bookSchema = {
521
- description: "Book a ride against a previously returned quote.",
522
- params: {
523
- "quote-id": "string \u2014 required, quote id from `ride-elife quote`",
524
- "vehicle-class": "string \u2014 required, chosen vehicle class",
525
- "price-amount": "number \u2014 required, fare in decimal currency units (not cents)",
526
- "price-currency": "string \u2014 currency code (default USD)",
527
- "payment-order-id": "string \u2014 conditional. Required for pay_per_call billing (a PAID order from payment-cli); omit for monthly_settlement (deducted from the settlement account)",
528
- "passenger-name": "string \u2014 required, passenger full name",
529
- "passenger-phone": "string \u2014 required, passenger phone",
530
- "passenger-email": "string \u2014 optional passenger email",
531
- "luggage-count": "number \u2014 optional luggage count",
532
- "special-requests": "string \u2014 optional free-text requests",
533
- "pickup-lat": "number \u2014 required, pickup latitude",
534
- "pickup-lng": "number \u2014 required, pickup longitude",
535
- "pickup-name": "string \u2014 required, pickup location name",
536
- "dropoff-lat": "number \u2014 required, dropoff latitude",
537
- "dropoff-lng": "number \u2014 required, dropoff longitude",
538
- "dropoff-name": "string \u2014 required, dropoff location name",
539
- "pickup-time": 'number|string \u2014 required, epoch seconds or "now" (must match quote)',
540
- "meet-and-greet": "boolean \u2014 optional meet & greet service",
541
- "meet-and-greet-price": "number \u2014 optional meet & greet surcharge",
542
- "welcome-sign": "string \u2014 optional welcome sign text",
543
- "child-seat-count": "number \u2014 optional child seats (0-5)",
544
- "infant-seat-count": "number \u2014 optional infant seats (0-5)",
545
- "toddler-seat-count": "number \u2014 optional toddler seats (0-5)",
546
- "arrival-flight-no": "string \u2014 optional arrival flight number",
547
- "arrival-airline": "string \u2014 optional arrival airline",
548
- "departure-flight-no": "string \u2014 optional departure flight number",
549
- "departure-airline": "string \u2014 optional departure airline",
550
- "idempotency-key": "string \u2014 required, sent as the Idempotency-Key header (never in body)"
551
- },
552
- response: {
553
- ride_id: "string \u2014 booked ride id, used by `ride-elife get` / `ride-elife cancel`",
554
- order_id: "string \u2014 internal ride order id (rio_...)",
555
- status: "string \u2014 initial ride status (e.g. INIT / Pending)",
556
- is_scheduled: "boolean \u2014 true = scheduled/airport ride, false = realtime",
557
- order_type: "string \u2014 realtime / airport (derived from pickup-time)",
558
- price: "{ amount, currency, quote_id } \u2014 confirmed fare",
559
- payment_status: "string \u2014 ON_ACCOUNT (monthly_settlement) or PAID (pay_per_call)",
560
- billing_entry_id: "string \u2014 settlement ledger entry id (monthly_settlement only)"
1183
+ // src/ride-elife/book.ts
1184
+ function need2(value, flag) {
1185
+ if (value === void 0) {
1186
+ throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
561
1187
  }
562
- };
563
- function buildBookCommand() {
564
- return new Command2("book").description(bookSchema.description).helpOption(false).option("--help", "Show help (add --format json for the machine-readable schema)").option("--format <format>", "Output format: json|table").option("--api-key <key>", "API key for runtime requests").option("--yes", "Skip confirmation prompts").option("--quote-id <id>", "Quote id from `ride-elife quote`").option("--vehicle-class <class>", "Chosen vehicle class").option("--price-amount <amount>", "Fare in decimal currency units (not cents)").option("--price-currency <currency>", "Currency code (default USD)").option("--payment-order-id <id>", "Paid payment order id (pay_per_call mode only)").option("--passenger-name <name>", "Passenger full name").option("--passenger-phone <phone>", "Passenger phone").option("--passenger-email <email>", "Passenger email").option("--luggage-count <n>", "Luggage count").option("--special-requests <text>", "Free-text special requests").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("--meet-and-greet", "Enable meet & greet service").option("--meet-and-greet-price <amount>", "Meet & greet surcharge").option("--welcome-sign <text>", "Welcome sign text").option("--child-seat-count <n>", "Number of child seats needed").option("--infant-seat-count <n>", "Number of infant seats needed").option("--toddler-seat-count <n>", "Number of toddler seats needed").option("--arrival-flight-no <no>", "Arrival flight number").option("--arrival-airline <airline>", "Arrival airline").option("--departure-flight-no <no>", "Departure flight number").option("--departure-airline <airline>", "Departure airline").option("--idempotency-key <key>", "Caller-supplied idempotency key").action(async (_options, command) => {
565
- const merged = command.optsWithGlobals();
566
- const format = merged.format === "table" ? "table" : "json";
567
- if (merged.help) {
568
- const wantsSchema = process.argv.some((a) => a === "--format" || a.startsWith("--format="));
569
- if (wantsSchema) emitSchema(bookSchema, format);
570
- else command.outputHelp();
571
- return;
572
- }
573
- const yes = merged.yes === true;
574
- const apiKey = await PromptEngine.resolveInput(merged.apiKey, {
575
- message: "API key:",
1188
+ return value;
1189
+ }
1190
+ function num2(value, flag) {
1191
+ const n = Number(need2(value, flag));
1192
+ if (!Number.isFinite(n)) {
1193
+ throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
1194
+ }
1195
+ return n;
1196
+ }
1197
+ function seatCount(value, flag) {
1198
+ const n = num2(value, flag);
1199
+ if (!Number.isInteger(n) || n < 0 || n > 5) {
1200
+ throw new CliError("PARAM_INVALID", `--${flag} must be an integer between 0 and 5.`);
1201
+ }
1202
+ return n;
1203
+ }
1204
+ function formatBook(data) {
1205
+ const lines = [
1206
+ ["Ride ID", String(data.ride_id ?? "-")],
1207
+ ["Order ID", String(data.order_id ?? "-")],
1208
+ ["Status", String(data.status ?? "-")],
1209
+ ["Scheduled", String(data.is_scheduled ?? false)],
1210
+ ["Order type", String(data.order_type ?? "-")]
1211
+ ];
1212
+ if (data.price) {
1213
+ lines.push(["Price", `${data.price.amount} ${data.price.currency}`]);
1214
+ if (data.price.quote_id) lines.push(["Quote ID", String(data.price.quote_id)]);
1215
+ }
1216
+ lines.push(["Payment status", String(data.payment_status ?? "-")]);
1217
+ if (data.billing_entry_id) lines.push(["Billing entry", String(data.billing_entry_id)]);
1218
+ if (data.payment_order_id) lines.push(["Payment order", String(data.payment_order_id)]);
1219
+ return Formatter.keyValue(lines);
1220
+ }
1221
+ function registerBookCommand(parent, deps) {
1222
+ const cmd = parent.command("book").description("Book a ride using a quote_id returned by quote").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--quote-id <id>", "Quote id from `ride-elife quote`").option("--vehicle-class <class>", "Chosen vehicle class").option("--price-amount <amount>", "Fare in decimal currency units (not cents)").option("--price-currency <currency>", "Currency code (default USD)").option("--payment-order-id <id>", "Paid payment order id (pay_per_call mode only)").option("--passenger-name <name>", "Passenger full name").option("--passenger-phone <phone>", "Passenger phone").option("--passenger-email <email>", "Passenger email").option("--luggage-count <n>", "Luggage count").option("--special-requests <text>", "Free-text special requests").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("--meet-and-greet", "Enable meet & greet service").option("--meet-and-greet-price <amount>", "Meet & greet surcharge").option("--welcome-sign <text>", "Welcome sign text").option("--child-seat-count <n>", "Number of child seats needed (0-5)").option("--infant-seat-count <n>", "Number of infant seats needed (0-5)").option("--toddler-seat-count <n>", "Number of toddler seats needed (0-5)").option("--arrival-flight-no <no>", "Arrival flight number").option("--arrival-airline <airline>", "Arrival airline").option("--departure-flight-no <no>", "Departure flight number").option("--departure-airline <airline>", "Departure airline").option(
1223
+ "--idempotency-key <key>",
1224
+ "Idempotency key forwarded verbatim as the Idempotency-Key header"
1225
+ );
1226
+ attachSchemaHelp(cmd, bookSchema);
1227
+ cmd.action(async () => {
1228
+ const opts = cmd.optsWithGlobals();
1229
+ const format = resolveFormat(opts.format);
1230
+ const isYes = Boolean(opts.yes);
1231
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1232
+ message: "API Key:",
576
1233
  type: "password"
577
1234
  });
578
- const quoteId = await PromptEngine.resolveInput(merged.quoteId, { message: "Quote id:" });
579
- const vehicleClass = await PromptEngine.resolveInput(merged.vehicleClass, {
580
- message: "Vehicle class:"
581
- });
582
- const priceAmount = await PromptEngine.resolveInput(merged.priceAmount, {
583
- message: "Price amount (decimal):"
584
- });
585
- const passengerName = await PromptEngine.resolveInput(merged.passengerName, {
586
- message: "Passenger name:"
587
- });
588
- const passengerPhone = await PromptEngine.resolveInput(merged.passengerPhone, {
589
- message: "Passenger phone:"
590
- });
1235
+ const quoteId = need2(opts.quoteId, "quote-id");
591
1236
  const body = {
592
1237
  quote_id: quoteId,
593
- vehicle_class: vehicleClass,
594
- price_amount: Number(priceAmount),
595
- price_currency: merged.priceCurrency ?? "USD",
596
- passenger_name: passengerName,
597
- passenger_phone: passengerPhone
1238
+ vehicle_class: need2(opts.vehicleClass, "vehicle-class"),
1239
+ price_amount: num2(opts.priceAmount, "price-amount"),
1240
+ price_currency: opts.priceCurrency ?? "USD",
1241
+ passenger_name: need2(opts.passengerName, "passenger-name"),
1242
+ passenger_phone: need2(opts.passengerPhone, "passenger-phone")
598
1243
  };
599
- if (merged.paymentOrderId) body.payment_order_id = merged.paymentOrderId;
600
- if (merged.passengerEmail) body.passenger_email = merged.passengerEmail;
601
- if (merged.luggageCount !== void 0) body.luggage_count = Number(merged.luggageCount);
602
- if (merged.specialRequests) body.special_requests = merged.specialRequests;
603
- if (merged.pickupLat || merged.pickupLng || merged.pickupName) {
604
- body.pickup = { lat: Number(merged.pickupLat), lng: Number(merged.pickupLng), name: merged.pickupName };
605
- }
606
- if (merged.dropoffLat || merged.dropoffLng || merged.dropoffName) {
607
- body.dropoff = { lat: Number(merged.dropoffLat), lng: Number(merged.dropoffLng), name: merged.dropoffName };
608
- }
609
- if (merged.pickupTime) {
610
- body.pickup_time = merged.pickupTime === "now" ? "now" : Number(merged.pickupTime);
611
- }
612
- if (merged.meetAndGreet) body.meet_and_greet = true;
613
- if (merged.meetAndGreetPrice !== void 0) body.meet_and_greet_price = Number(merged.meetAndGreetPrice);
614
- if (merged.welcomeSign) body.welcome_sign = merged.welcomeSign;
615
- if (merged.childSeatCount !== void 0) body.child_seat_count = Number(merged.childSeatCount);
616
- if (merged.infantSeatCount !== void 0) body.infant_seat_count = Number(merged.infantSeatCount);
617
- if (merged.toddlerSeatCount !== void 0) body.toddler_seat_count = Number(merged.toddlerSeatCount);
618
- if (merged.arrivalFlightNo || merged.arrivalAirline) {
619
- body.arrival_flight = { flight_no: merged.arrivalFlightNo, airline: merged.arrivalAirline };
620
- }
621
- if (merged.departureFlightNo || merged.departureAirline) {
622
- body.departure_flight = { flight_no: merged.departureFlightNo, airline: merged.departureAirline };
623
- }
624
- if (!await PromptEngine.confirm(`Book ride with quote ${quoteId}?`, yes)) {
625
- console.log(Formatter.status("info", "Booking aborted."));
626
- return;
1244
+ if (opts.paymentOrderId) body.payment_order_id = opts.paymentOrderId;
1245
+ if (opts.passengerEmail) body.passenger_email = opts.passengerEmail;
1246
+ if (opts.luggageCount !== void 0) {
1247
+ body.luggage_count = num2(opts.luggageCount, "luggage-count");
627
1248
  }
628
- const idempotencyKey = await resolveIdempotencyKey(merged.idempotencyKey, { yes });
629
- const client = new ApiClient({ apiKey });
630
- const spinner = createSpinner("Booking ride...");
631
- try {
632
- const data = await client.post("/ride/book", { body, idempotencyKey });
633
- spinner.stop();
634
- emit(data, format);
635
- } catch (err) {
636
- spinner.fail("Booking failed");
637
- throw err;
1249
+ if (opts.specialRequests) body.special_requests = opts.specialRequests;
1250
+ if (opts.pickupLat || opts.pickupLng || opts.pickupName) {
1251
+ body.pickup = {
1252
+ lat: num2(opts.pickupLat, "pickup-lat"),
1253
+ lng: num2(opts.pickupLng, "pickup-lng"),
1254
+ name: need2(opts.pickupName, "pickup-name")
1255
+ };
1256
+ }
1257
+ if (opts.dropoffLat || opts.dropoffLng || opts.dropoffName) {
1258
+ body.dropoff = {
1259
+ lat: num2(opts.dropoffLat, "dropoff-lat"),
1260
+ lng: num2(opts.dropoffLng, "dropoff-lng"),
1261
+ name: need2(opts.dropoffName, "dropoff-name")
1262
+ };
1263
+ }
1264
+ if (opts.pickupTime) {
1265
+ body.pickup_time = opts.pickupTime === "now" ? "now" : num2(opts.pickupTime, "pickup-time");
1266
+ }
1267
+ if (opts.meetAndGreet) body.meet_and_greet = true;
1268
+ if (opts.meetAndGreetPrice !== void 0) {
1269
+ body.meet_and_greet_price = num2(opts.meetAndGreetPrice, "meet-and-greet-price");
638
1270
  }
1271
+ if (opts.welcomeSign) body.welcome_sign = opts.welcomeSign;
1272
+ if (opts.childSeatCount !== void 0) {
1273
+ body.child_seat_count = seatCount(opts.childSeatCount, "child-seat-count");
1274
+ }
1275
+ if (opts.infantSeatCount !== void 0) {
1276
+ body.infant_seat_count = seatCount(opts.infantSeatCount, "infant-seat-count");
1277
+ }
1278
+ if (opts.toddlerSeatCount !== void 0) {
1279
+ body.toddler_seat_count = seatCount(opts.toddlerSeatCount, "toddler-seat-count");
1280
+ }
1281
+ if (opts.arrivalFlightNo || opts.arrivalAirline) {
1282
+ body.arrival_flight = {
1283
+ flight_no: opts.arrivalFlightNo,
1284
+ airline: opts.arrivalAirline
1285
+ };
1286
+ }
1287
+ if (opts.departureFlightNo || opts.departureAirline) {
1288
+ body.departure_flight = {
1289
+ flight_no: opts.departureFlightNo,
1290
+ airline: opts.departureAirline
1291
+ };
1292
+ }
1293
+ if (!isYes) {
1294
+ const confirmed = await confirm({
1295
+ message: `Book ride with quote ${quoteId}?`,
1296
+ default: true
1297
+ });
1298
+ if (!confirmed) {
1299
+ throw new CliError("CLIENT_ABORTED", "Booking cancelled by user.");
1300
+ }
1301
+ }
1302
+ const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
1303
+ yes: isYes,
1304
+ commandPath: "ride-elife book"
1305
+ });
1306
+ notify(format, "loading", "Booking ride...");
1307
+ const result = await deps.apiClient.post(
1308
+ "/ride/book",
1309
+ { type: "api-key", key: apiKey },
1310
+ body,
1311
+ { "Idempotency-Key": idempotencyKey }
1312
+ );
1313
+ if (!result.success) {
1314
+ throw CliError.fromApi(result, { auth: "api-key" });
1315
+ }
1316
+ const data = result.data;
1317
+ const configManager = new ConfigManager();
1318
+ const commandResult = {
1319
+ data,
1320
+ text: () => formatBook(data)
1321
+ };
1322
+ await renderWithContext(commandResult, { format }, configManager);
639
1323
  });
640
1324
  }
641
1325
 
642
- // src/ride/get.ts
643
- import { Command as Command3 } from "commander";
644
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
645
- "At destination",
646
- "Cancelled",
647
- "Rejected",
648
- "Customer no show",
649
- "Driver no show"
650
- ]);
651
- var DEFAULT_WATCH_INTERVAL_SECONDS = 5;
652
- var DEFAULT_WATCH_TIMEOUT_SECONDS = 600;
653
- var rideGetSchema = {
654
- description: "Retrieve a single ride order status by id (repeatable for polling).",
655
- params: {
656
- "order-id": "string \u2014 ride order id (the ride_id returned by `ride-elife book`)",
657
- watch: "boolean \u2014 poll until a terminal status, emitting one NDJSON line per update (default off)",
658
- "watch-interval": `number \u2014 seconds between polls when --watch is set (default ${DEFAULT_WATCH_INTERVAL_SECONDS})`,
659
- "watch-timeout": `number \u2014 max seconds to poll before giving up (default ${DEFAULT_WATCH_TIMEOUT_SECONDS})`
660
- },
661
- response: {
662
- status: "OrderStatus \u2014 case-sensitive; terminal states: At destination | Cancelled | Rejected | Customer no show | Driver no show",
663
- from_location: "GeoPoint \u2014 pickup",
664
- to_location: "GeoPoint \u2014 dropoff",
665
- pickup_time: "number|string",
666
- vehicle_class: "string",
667
- price: "Price{amount,currency} \u2014 amount is a decimal in the currency standard unit",
668
- final_amount: "number \u2014 final settled fare (realtime, after final-fare settlement); local-cache path only",
669
- final_settlement_status: "string \u2014 pending | settled | no_adjustment | settlement_pending | not_applicable (local-cache path)",
670
- passenger: "Passenger",
671
- driver: "Driver \u2014 present after assignment",
672
- vehicle: "Vehicle \u2014 present after assignment"
1326
+ // src/ride-elife/get.ts
1327
+ function need3(value, flag) {
1328
+ if (value === void 0) {
1329
+ throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
673
1330
  }
674
- };
675
- function num2(value, fallback) {
676
- if (value === void 0) return fallback;
677
- const n = Number(value);
678
- return Number.isFinite(n) && n > 0 ? n : fallback;
1331
+ return value;
679
1332
  }
680
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
681
- function statusOf(data) {
682
- return data.status;
683
- }
684
- function buildRideGetCommand() {
685
- const cmd = new Command3("get").description(rideGetSchema.description).option("--order-id <id>", "Ride order id (the ride_id returned by `ride-elife book`)").option("--watch", "Poll until a terminal status, emitting one NDJSON line per update").option("--watch-interval <seconds>", "Seconds between polls when --watch is set").option("--watch-timeout <seconds>", "Max seconds to poll before giving up").action(async (_opts, command) => {
686
- const o = command.optsWithGlobals();
687
- const format = o.format === "table" ? "table" : "json";
688
- const apiKey = await PromptEngine.resolveInput(o.apiKey, {
689
- message: "API key:",
1333
+ function formatGetOrder(data) {
1334
+ const lines = [
1335
+ ["Ride ID", String(data.ride_id ?? "-")],
1336
+ ["Status", String(data.status ?? "-")]
1337
+ ];
1338
+ if (data.source) lines.push(["Source", String(data.source)]);
1339
+ if (data.is_scheduled !== void 0) lines.push(["Scheduled", String(data.is_scheduled)]);
1340
+ if (data.from_location) {
1341
+ lines.push(["From", String(data.from_location.name ?? "-")]);
1342
+ }
1343
+ if (data.to_location) {
1344
+ lines.push(["To", String(data.to_location.name ?? "-")]);
1345
+ }
1346
+ if (data.pickup_time !== void 0) lines.push(["Pickup time", String(data.pickup_time)]);
1347
+ if (data.vehicle_class) lines.push(["Vehicle class", String(data.vehicle_class)]);
1348
+ if (data.price) {
1349
+ lines.push(["Price", `${data.price.amount} ${data.price.currency}`]);
1350
+ }
1351
+ if (data.final_amount !== void 0 && data.final_amount !== null) {
1352
+ lines.push(["Final amount", String(data.final_amount)]);
1353
+ }
1354
+ if (data.final_settlement_status) {
1355
+ lines.push(["Settlement", String(data.final_settlement_status)]);
1356
+ }
1357
+ if (data.driver) {
1358
+ const driver = data.driver;
1359
+ const phone = driver.phone_number ? ` (${driver.phone_number})` : "";
1360
+ lines.push(["Driver", `${driver.name ?? "-"}${phone}`]);
1361
+ }
1362
+ if (data.vehicle) {
1363
+ const v = data.vehicle;
1364
+ const desc = [v.color, v.make, v.model].filter(Boolean).join(" ") || "-";
1365
+ const plate = v.license_plate ? ` [${v.license_plate}]` : "";
1366
+ lines.push(["Vehicle", `${desc}${plate}`]);
1367
+ }
1368
+ if (data.created_at) lines.push(["Created at", String(data.created_at)]);
1369
+ return Formatter.keyValue(lines);
1370
+ }
1371
+ function registerRideGetCommand(parent, deps) {
1372
+ const cmd = parent.command("get").description("Retrieve a ride order status by id (repeatable for polling)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Ride order id (the ride_id returned by `ride-elife book`)").option("--watch", "Poll until a terminal status, emitting one NDJSON line per update").option(
1373
+ "--watch-interval <seconds>",
1374
+ `Seconds between polls when --watch is set (default ${DEFAULT_WATCH_INTERVAL_SECONDS})`
1375
+ ).option(
1376
+ "--watch-timeout <seconds>",
1377
+ `Max seconds to poll before giving up (default ${DEFAULT_WATCH_TIMEOUT_SECONDS})`
1378
+ );
1379
+ attachSchemaHelp(cmd, rideGetSchema);
1380
+ cmd.action(async () => {
1381
+ const opts = cmd.optsWithGlobals();
1382
+ const format = resolveFormat(opts.format);
1383
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1384
+ message: "API Key:",
690
1385
  type: "password"
691
1386
  });
692
- const orderId = await PromptEngine.resolveInput(o.orderId, { message: "Order id:" });
693
- const client = new ApiClient({ apiKey });
694
- const path = `/ride/${encodeURIComponent(orderId)}/status`;
695
- if (o.watch) {
696
- await watchUntilTerminal(client, path, {
697
- intervalMs: num2(o.watchInterval, DEFAULT_WATCH_INTERVAL_SECONDS) * 1e3,
698
- timeoutMs: num2(o.watchTimeout, DEFAULT_WATCH_TIMEOUT_SECONDS) * 1e3
1387
+ const orderId = need3(opts.orderId, "order-id");
1388
+ if (opts.watch) {
1389
+ await watchRideStatus(deps.apiClient, apiKey, orderId, {
1390
+ intervalSeconds: resolveSeconds(
1391
+ opts.watchInterval,
1392
+ DEFAULT_WATCH_INTERVAL_SECONDS
1393
+ ),
1394
+ timeoutSeconds: resolveSeconds(
1395
+ opts.watchTimeout,
1396
+ DEFAULT_WATCH_TIMEOUT_SECONDS
1397
+ )
699
1398
  });
700
1399
  return;
701
1400
  }
702
- const spinner = createSpinner("Fetching ride status...");
703
- try {
704
- const data = await client.get(path);
705
- spinner.stop();
706
- emit(data, format);
707
- } catch (err) {
708
- spinner.fail("Failed to fetch ride status");
709
- throw err;
1401
+ notify(format, "loading", "Fetching ride status...");
1402
+ const result = await deps.apiClient.get(
1403
+ `/ride/${encodeURIComponent(orderId)}/status`,
1404
+ { type: "api-key", key: apiKey }
1405
+ );
1406
+ if (!result.success) {
1407
+ throw CliError.fromApi(result, { auth: "api-key" });
710
1408
  }
1409
+ const data = result.data;
1410
+ const configManager = new ConfigManager();
1411
+ const commandResult = {
1412
+ data,
1413
+ text: () => formatGetOrder(data)
1414
+ };
1415
+ await renderWithContext(commandResult, { format }, configManager);
711
1416
  });
712
- return attachSchemaHelp(cmd, rideGetSchema);
713
1417
  }
714
- async function watchUntilTerminal(client, path, opts) {
715
- const deadline = Date.now() + opts.timeoutMs;
716
- for (; ; ) {
717
- const data = await client.get(path);
718
- emitNdjsonLine(data);
719
- const status = statusOf(data);
720
- if (status !== void 0 && TERMINAL_STATUSES.has(status)) return;
721
- if (Date.now() + opts.intervalMs >= deadline) {
722
- emitNdjsonLine({
723
- watch_status: "timeout",
724
- message: `Polling stopped after ${opts.timeoutMs / 1e3}s without reaching a terminal status.`,
725
- last_status: status ?? null
726
- });
727
- return;
728
- }
729
- await sleep(opts.intervalMs);
1418
+
1419
+ // src/ride-elife/cancel.ts
1420
+ import { confirm as confirm2 } from "@inquirer/prompts";
1421
+ function need4(value, flag) {
1422
+ if (value === void 0) {
1423
+ throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
730
1424
  }
1425
+ return value;
731
1426
  }
732
-
733
- // src/ride/cancel.ts
734
- import { Command as Command4 } from "commander";
735
- var cancelSchema = {
736
- description: "Cancel a ride order; may incur a cancellation fee.",
737
- params: {
738
- "order-id": "string \u2014 required, order id (a.k.a. ride id) to cancel",
739
- "idempotency-key": "string \u2014 required, sent as the Idempotency-Key header (never in body)"
740
- },
741
- response: {
742
- ride_id: "string \u2014 cancelled ride id",
743
- ride_stat: "string \u2014 resulting ride status",
744
- cancellation: "{ cancellation_fee, reversal_amount, currency }",
745
- refund_amount: "number \u2014 amount credited back to the settlement balance (paid \u2212 cancellation_fee)"
1427
+ function formatCancel(data) {
1428
+ const lines = [
1429
+ ["Ride ID", String(data.ride_id ?? "-")],
1430
+ ["Status", String(data.ride_stat ?? "-")]
1431
+ ];
1432
+ if (data.cancellation) {
1433
+ const c = data.cancellation;
1434
+ lines.push(["Cancellation fee", `${c.cancellation_fee} ${c.currency}`]);
1435
+ lines.push(["Reversal amount", `${c.reversal_amount} ${c.currency}`]);
746
1436
  }
747
- };
748
- function buildCancelCommand() {
749
- return new Command4("cancel").description(cancelSchema.description).helpOption(false).option("--help", "Show help (add --format json for the machine-readable schema)").option("--format <format>", "Output format: json|table").option("--api-key <key>", "API key for runtime requests").option("--yes", "Skip confirmation prompts").option("--order-id <id>", "Order id (a.k.a. ride id) to cancel").option("--idempotency-key <key>", "Caller-supplied idempotency key").action(async (_options, command) => {
750
- const merged = command.optsWithGlobals();
751
- const format = merged.format === "table" ? "table" : "json";
752
- if (merged.help) {
753
- const wantsSchema = process.argv.some((a) => a === "--format" || a.startsWith("--format="));
754
- if (wantsSchema) emitSchema(cancelSchema, format);
755
- else command.outputHelp();
756
- return;
757
- }
758
- const yes = merged.yes === true;
759
- const apiKey = await PromptEngine.resolveInput(merged.apiKey, {
760
- message: "API key:",
1437
+ if (data.refund_amount !== void 0 && data.refund_amount !== null) {
1438
+ lines.push(["Refund amount", String(data.refund_amount)]);
1439
+ }
1440
+ return Formatter.keyValue(lines);
1441
+ }
1442
+ function registerCancelCommand(parent, deps) {
1443
+ const cmd = parent.command("cancel").description("Cancel a ride order by id (may incur a cancellation fee)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Ride order id to cancel (the ride_id returned by book)").option(
1444
+ "--idempotency-key <key>",
1445
+ "Idempotency key forwarded verbatim as the Idempotency-Key header"
1446
+ );
1447
+ attachSchemaHelp(cmd, cancelSchema);
1448
+ cmd.action(async () => {
1449
+ const opts = cmd.optsWithGlobals();
1450
+ const format = resolveFormat(opts.format);
1451
+ const isYes = Boolean(opts.yes);
1452
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1453
+ message: "API Key:",
761
1454
  type: "password"
762
1455
  });
763
- const orderId = await PromptEngine.resolveInput(merged.orderId, { message: "Order id:" });
764
- if (!await PromptEngine.confirm(`Cancel order ${orderId}? This may incur a fee.`, yes)) {
765
- console.log(Formatter.status("info", "Cancellation aborted."));
766
- return;
1456
+ const orderId = need4(opts.orderId, "order-id");
1457
+ if (!isYes) {
1458
+ const confirmed = await confirm2({
1459
+ message: `Cancel ride ${orderId}? This may incur a fee.`,
1460
+ default: false
1461
+ });
1462
+ if (!confirmed) {
1463
+ throw new CliError("CLIENT_ABORTED", "Cancellation aborted by user.");
1464
+ }
767
1465
  }
768
- const idempotencyKey = await resolveIdempotencyKey(merged.idempotencyKey, { yes });
769
- const client = new ApiClient({ apiKey });
770
- const spinner = createSpinner("Cancelling ride...");
771
- try {
772
- const data = await client.post(`/ride/${encodeURIComponent(orderId)}/cancel`, { idempotencyKey });
773
- spinner.stop();
774
- emit(data, format);
775
- } catch (err) {
776
- spinner.fail("Cancellation failed");
777
- throw err;
1466
+ const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
1467
+ yes: isYes,
1468
+ commandPath: "ride-elife cancel"
1469
+ });
1470
+ notify(format, "loading", "Cancelling ride...");
1471
+ const result = await deps.apiClient.post(
1472
+ `/ride/${encodeURIComponent(orderId)}/cancel`,
1473
+ { type: "api-key", key: apiKey },
1474
+ void 0,
1475
+ { "Idempotency-Key": idempotencyKey }
1476
+ );
1477
+ if (!result.success) {
1478
+ throw CliError.fromApi(result, { auth: "api-key" });
778
1479
  }
1480
+ const data = result.data;
1481
+ const configManager = new ConfigManager();
1482
+ const commandResult = {
1483
+ data,
1484
+ text: () => formatCancel(data)
1485
+ };
1486
+ await renderWithContext(commandResult, { format }, configManager);
779
1487
  });
780
1488
  }
781
1489
 
782
- // src/ride/list-orders.ts
783
- import { Command as Command5 } from "commander";
784
- var listOrdersSchema = {
785
- description: "List ride orders with pagination and optional filters.",
786
- params: {
787
- page: "number \u2014 page number (default 1)",
788
- "page-size": "number \u2014 page size (default 20)",
789
- status: "OrderStatus \u2014 optional status filter",
790
- "order-type": "string \u2014 optional order type filter"
791
- },
792
- response: {
793
- orders: "RideOrderListItem[] \u2014 each: order_id, ride_id, status, vehicle_class, is_scheduled, scheduled_at, price_amount, final_amount, price_currency, payment_status, final_settlement_status (pending|settled|no_adjustment|settlement_pending|not_applicable), cancellation_fee, provider, created_at, updated_at",
794
- total: "number",
795
- page: "number",
796
- page_size: "number"
1490
+ // src/ride-elife/list-orders.ts
1491
+ var DEFAULT_PAGE = "1";
1492
+ var DEFAULT_PAGE_SIZE = "20";
1493
+ function positiveInt(value, flag) {
1494
+ const n = Number(value);
1495
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
1496
+ throw new CliError("PARAM_INVALID", `--${flag} must be a positive integer.`);
797
1497
  }
798
- };
799
- function buildListOrdersCommand() {
800
- const cmd = new Command5("list-orders").description(listOrdersSchema.description).option("--page <page>", "Page number", "1").option("--page-size <size>", "Page size", "20").option("--status <status>", "Filter by order status").option("--order-type <type>", "Filter by order type").action(async (_opts, command) => {
801
- const o = command.optsWithGlobals();
802
- const format = o.format === "table" ? "table" : "json";
803
- const apiKey = await PromptEngine.resolveInput(o.apiKey, {
804
- message: "API key:",
1498
+ return String(n);
1499
+ }
1500
+ function formatOrders(data) {
1501
+ const orders = data.orders ?? [];
1502
+ if (orders.length === 0) {
1503
+ return Formatter.status("info", "No ride orders found");
1504
+ }
1505
+ const headers = ["Order ID", "Ride ID", "Status", "Vehicle Class", "Price", "Currency", "Payment"];
1506
+ const rows = orders.map((o) => [
1507
+ String(o.order_id ?? "-"),
1508
+ String(o.ride_id ?? "-"),
1509
+ String(o.status ?? "-"),
1510
+ String(o.vehicle_class ?? "-"),
1511
+ String(o.price_amount ?? "-"),
1512
+ String(o.price_currency ?? "-"),
1513
+ String(o.payment_status ?? "-")
1514
+ ]);
1515
+ const summary = [
1516
+ ["Total", String(data.total ?? "-")],
1517
+ ["Page", String(data.page ?? "-")],
1518
+ ["Page size", String(data.page_size ?? "-")]
1519
+ ];
1520
+ return `${Formatter.table(headers, rows)}
1521
+ ${Formatter.keyValue(summary)}`;
1522
+ }
1523
+ function registerListOrdersCommand(parent, deps) {
1524
+ const cmd = parent.command("list-orders").description("List previously placed ride orders").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--page <page>", "Page number", DEFAULT_PAGE).option("--page-size <size>", "Page size", DEFAULT_PAGE_SIZE).option("--status <status>", "Filter by order status").option("--order-type <type>", "Filter by order type");
1525
+ attachSchemaHelp(cmd, listOrdersSchema);
1526
+ cmd.action(async () => {
1527
+ const opts = cmd.optsWithGlobals();
1528
+ const format = resolveFormat(opts.format);
1529
+ const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
1530
+ message: "API Key:",
805
1531
  type: "password"
806
1532
  });
807
- const client = new ApiClient({ apiKey });
808
- const spinner = createSpinner("Fetching orders...");
809
- try {
810
- const data = await client.get("/ride/orders", {
811
- query: {
812
- page: Number(o.page),
813
- page_size: Number(o.pageSize),
814
- status: o.status,
815
- order_type: o.orderType
816
- }
817
- });
818
- spinner.stop();
819
- emit(data, format);
820
- } catch (err) {
821
- spinner.fail("Failed to fetch orders");
822
- throw err;
1533
+ const params = {
1534
+ page: positiveInt(opts.page ?? DEFAULT_PAGE, "page"),
1535
+ page_size: positiveInt(opts.pageSize ?? DEFAULT_PAGE_SIZE, "page-size")
1536
+ };
1537
+ if (opts.status !== void 0) params.status = opts.status;
1538
+ if (opts.orderType !== void 0) params.order_type = opts.orderType;
1539
+ notify(format, "loading", "Fetching orders...");
1540
+ const result = await deps.apiClient.get(
1541
+ "/ride/orders",
1542
+ { type: "api-key", key: apiKey },
1543
+ params
1544
+ );
1545
+ if (!result.success) {
1546
+ throw CliError.fromApi(result, { auth: "api-key" });
823
1547
  }
1548
+ const data = result.data;
1549
+ const configManager = new ConfigManager();
1550
+ const commandResult = {
1551
+ data,
1552
+ text: () => formatOrders(data)
1553
+ };
1554
+ await renderWithContext(commandResult, { format }, configManager);
824
1555
  });
825
- return attachSchemaHelp(cmd, listOrdersSchema);
826
1556
  }
827
1557
 
828
- // src/services/list.ts
829
- import { Command as Command6 } from "commander";
830
-
831
1558
  // src/services/registry.ts
832
1559
  var SERVICE_REGISTRY = [
833
1560
  {
834
1561
  service_id: "ride-elife",
835
1562
  name: "Ride hailing (eLife)",
836
1563
  description: "On-demand ride ordering: quote a fare, book it, poll status, and cancel.",
1564
+ category: "ride",
837
1565
  version: "1.0.0",
838
1566
  provider: "elife",
839
1567
  cli_noun: "ride-elife",
@@ -841,8 +1569,8 @@ var SERVICE_REGISTRY = [
841
1569
  verb_descriptions: {
842
1570
  quote: "Request fare quotes for a ride between two points.",
843
1571
  book: "Book a ride using a quote_id returned by quote.",
844
- get: "Retrieve a ride order by id (poll for status changes).",
845
- cancel: "Cancel a ride order by id.",
1572
+ get: "Retrieve a ride order by id (poll for status changes with --watch).",
1573
+ cancel: "Cancel a ride order by id (may incur a fee).",
846
1574
  "list-orders": "List previously placed ride orders."
847
1575
  },
848
1576
  workflow: ["quote", "book", "get (poll for status)", "cancel (optional)"],
@@ -855,117 +1583,142 @@ function findService(serviceId) {
855
1583
  }
856
1584
 
857
1585
  // src/services/list.ts
858
- var servicesListSchema = {
859
- description: "List available merchant services from the registry.",
860
- params: {},
861
- response: {
862
- services: "Service[] \u2014 each with service_id, name, provider, cli_noun, version, verbs"
863
- }
864
- };
865
- function resolveFormat(cmd) {
866
- return cmd.optsWithGlobals().format === "table" ? "table" : "json";
867
- }
868
- function buildServicesListCommand() {
869
- return new Command6("list").description(servicesListSchema.description).helpOption(false).option("-h, --help", "Show help; with --format json prints the schema").action((opts, cmd) => {
870
- const format = resolveFormat(cmd);
871
- if (opts.help) {
872
- emitSchema(servicesListSchema, format);
873
- return;
874
- }
875
- const services = SERVICE_REGISTRY.map((s) => ({
876
- service_id: s.service_id,
877
- name: s.name,
878
- provider: s.provider,
879
- cli_noun: s.cli_noun,
880
- version: s.version,
881
- verbs: s.verbs.join(", ")
882
- }));
883
- emit(services, format);
1586
+ function toListItem(s) {
1587
+ return {
1588
+ service_id: s.service_id,
1589
+ name: s.name,
1590
+ category: s.category,
1591
+ provider: s.provider,
1592
+ cli_noun: s.cli_noun,
1593
+ version: s.version,
1594
+ verbs: s.verbs,
1595
+ since: s.since,
1596
+ discovery: s.discovery
1597
+ };
1598
+ }
1599
+ function registerServicesListCommand(parent) {
1600
+ const cmd = parent.command("list").description("List available merchant services from the registry");
1601
+ cmd.action(async () => {
1602
+ const opts = cmd.optsWithGlobals();
1603
+ const format = resolveFormat(opts.format);
1604
+ const services = SERVICE_REGISTRY.map(toListItem);
1605
+ const configManager = new ConfigManager();
1606
+ const commandResult = {
1607
+ data: { services },
1608
+ text: () => {
1609
+ if (services.length === 0) {
1610
+ return Formatter.status("info", "No services found");
1611
+ }
1612
+ const headers = ["Service ID", "Name", "Category", "Provider", "Version", "Verbs"];
1613
+ const rows = services.map((s) => [
1614
+ s.service_id,
1615
+ s.name,
1616
+ s.category,
1617
+ s.provider,
1618
+ s.version,
1619
+ s.verbs.join(", ")
1620
+ ]);
1621
+ return Formatter.table(headers, rows);
1622
+ }
1623
+ };
1624
+ await renderWithContext(commandResult, { format }, configManager);
884
1625
  });
885
1626
  }
886
1627
 
887
1628
  // src/services/get.ts
888
- import { Command as Command7 } from "commander";
889
- var serviceGetSchema = {
890
- description: "Retrieve a single merchant service by id, with verb and workflow detail.",
891
- params: {
892
- "service-id": "string \u2014 service id to retrieve"
893
- },
894
- response: {
895
- service_id: "string",
896
- name: "string",
897
- description: "string",
898
- version: "string",
899
- provider: "string",
900
- cli_noun: "string",
901
- verbs: "string[]",
902
- verb_descriptions: "Record<verb, description>",
903
- workflow: "string[] \u2014 ordered steps quote\u2192book\u2192get(poll)\u2192[cancel]",
904
- since: "string \u2014 ISO date",
905
- discovery: "{ help_command: string }"
906
- }
907
- };
908
- function resolveFormat2(cmd) {
909
- return cmd.optsWithGlobals().format === "table" ? "table" : "json";
910
- }
911
- function buildServiceGetCommand() {
912
- return new Command7("get").description(serviceGetSchema.description).argument("[service-id]", "Service id to retrieve").helpOption(false).option("-h, --help", "Show help; with --format json prints the schema").action((serviceId, opts, cmd) => {
913
- const format = resolveFormat2(cmd);
914
- if (opts.help) {
915
- emitSchema(serviceGetSchema, format);
916
- return;
917
- }
918
- const service = serviceId ? findService(serviceId) : void 0;
1629
+ function formatServiceGet(s) {
1630
+ const lines = [
1631
+ ["Service ID", s.service_id],
1632
+ ["Name", s.name],
1633
+ ["Description", s.description],
1634
+ ["Category", s.category],
1635
+ ["Version", s.version],
1636
+ ["Provider", s.provider],
1637
+ ["CLI Noun", s.cli_noun],
1638
+ ["Verbs", s.verbs.join(", ")],
1639
+ ["Workflow", s.workflow.join(" \u2192 ")],
1640
+ ["Since", s.since],
1641
+ ["Help", s.discovery.help_command]
1642
+ ];
1643
+ const verbDetail = s.verbs.map((v) => ` ${v}: ${s.verb_descriptions[v] ?? ""}`).join("\n");
1644
+ return `${Formatter.keyValue(lines)}
1645
+ Verb descriptions:
1646
+ ${verbDetail}`;
1647
+ }
1648
+ function registerServiceGetCommand(parent) {
1649
+ const cmd = parent.command("get <service-id>").description("Retrieve a single merchant service by id");
1650
+ cmd.action(async (serviceId) => {
1651
+ const opts = cmd.optsWithGlobals();
1652
+ const format = resolveFormat(opts.format);
1653
+ const service = findService(serviceId);
919
1654
  if (!service) {
920
1655
  throw new CliError(
921
- ErrorCodes.SERVICE_NOT_FOUND,
922
- `Service '${serviceId ?? ""}' not found in the registry (SERVICE_NOT_FOUND).`,
923
- 'Run "services list" to see available services.'
1656
+ "SERVICE_NOT_FOUND",
1657
+ `Service '${serviceId}' was not found in the registry. Run "services list" to see available services.`
924
1658
  );
925
1659
  }
926
- emit(service, format);
1660
+ const configManager = new ConfigManager();
1661
+ const commandResult = {
1662
+ data: service,
1663
+ text: () => formatServiceGet(service)
1664
+ };
1665
+ await renderWithContext(commandResult, { format }, configManager);
927
1666
  });
928
1667
  }
929
1668
 
930
1669
  // src/index.ts
931
- function resolveFormat3(cmd) {
932
- return cmd.optsWithGlobals().format === "table" ? "table" : "json";
933
- }
1670
+ var programRef;
934
1671
  async function main() {
935
- const program = new Command8();
936
- program.name("agenzo-merchant-cli").description("CLI for ordering rides and querying merchant services").version(getCurrentVersion()).option("--format <format>", "Output format: json|table", "json").option("--yes", "Skip confirmation prompts (for automation/AI Agents)", false).option("--api-key <key>", "API key for runtime requests");
937
- const config = program.command("config").description("Local API host configuration");
938
- config.command("set-host <url>").description("Set the API host").action((url) => {
939
- setHost(url);
940
- console.log(Formatter.status("success", `API host set to ${getHost()}`));
1672
+ const configManager = new ConfigManager(void 0, "/api/merchant/v1");
1673
+ await configManager.ensureDirectories();
1674
+ const apiBaseUrl = await configManager.getApiBaseUrl();
1675
+ const apiClient = new ApiClient({ baseUrl: apiBaseUrl });
1676
+ const deps = { apiClient };
1677
+ const program = new Command();
1678
+ programRef = program;
1679
+ program.name("agenzo-merchant-cli").version(getCurrentVersion()).description(
1680
+ "Agenzo merchant fulfillment plane: service discovery (services) and ride ordering (ride-elife)"
1681
+ ).option("--verbose", "Show verbose logs").option("--yes", "Skip confirmation prompts (for automation/AI Agents)").option(
1682
+ "--format <format>",
1683
+ "Output format: json | table (default: json; or set AGENZO_FORMAT)",
1684
+ "json"
1685
+ ).option("--api-key <key>", "API Key for authentication (X-Api-Key)");
1686
+ program.hook("preAction", (thisCommand) => {
1687
+ const flag = thisCommand.opts().format;
1688
+ process.env.AGENZO_FORMAT = resolveFormat(flag);
941
1689
  });
942
- config.command("reset-host").description("Reset the API host to the default").action(() => {
943
- resetHost();
944
- console.log(Formatter.status("success", `API host reset to ${getHost()}`));
945
- });
946
- config.command("show").description("Show the current configuration").action((_options, cmd) => {
947
- emit({ host: getHost() }, resolveFormat3(cmd));
948
- });
949
- const rideElife = program.command("ride-elife").description("Ride ordering (eLife): quote, book, get, cancel, list-orders");
950
- rideElife.addCommand(buildQuoteCommand());
951
- rideElife.addCommand(buildBookCommand());
952
- rideElife.addCommand(buildRideGetCommand());
953
- rideElife.addCommand(buildCancelCommand());
954
- rideElife.addCommand(buildListOrdersCommand());
955
- const services = program.command("services").description("Service registry: list, get");
956
- services.addCommand(buildServicesListCommand());
957
- services.addCommand(buildServiceGetCommand());
1690
+ const rideCmd = program.command("ride-elife").description("Ride ordering (eLife)");
1691
+ registerQuoteCommand(rideCmd, deps);
1692
+ registerBookCommand(rideCmd, deps);
1693
+ registerRideGetCommand(rideCmd, deps);
1694
+ registerCancelCommand(rideCmd, deps);
1695
+ registerListOrdersCommand(rideCmd, deps);
1696
+ const servicesCmd = program.command("services").description("Merchant service discovery");
1697
+ registerServicesListCommand(servicesCmd);
1698
+ registerServiceGetCommand(servicesCmd);
958
1699
  await program.parseAsync(process.argv);
959
1700
  }
960
- main().catch((error) => {
961
- if (error instanceof CliError) {
962
- console.error(Formatter.status("error", error.message));
963
- if (error.recovery) console.error(Formatter.status("info", error.recovery));
964
- process.exit(error.exitCode);
1701
+ function resolveActiveFormat() {
1702
+ const flag = programRef?.opts().format;
1703
+ return resolveFormat(flag);
1704
+ }
1705
+ function reportError(error) {
1706
+ const envelope = toErrorEnvelope(error);
1707
+ const format = resolveActiveFormat();
1708
+ if (format === "json") {
1709
+ console.error(JSON.stringify(envelope));
1710
+ } else {
1711
+ console.error(
1712
+ Formatter.status("error", `[${envelope.error.code_num}] ${envelope.error.message}`)
1713
+ );
1714
+ if (!(error instanceof CliError) && process.argv.includes("--verbose")) {
1715
+ console.error(error);
1716
+ }
965
1717
  }
966
- console.error(
967
- Formatter.status("error", error instanceof Error ? error.message : "An unexpected error occurred")
968
- );
969
- process.exit(1);
1718
+ process.exit(exitCodeFor(error));
1719
+ }
1720
+ process.on("SIGINT", () => {
1721
+ reportError(new UserCancelError());
970
1722
  });
1723
+ main().catch(reportError);
971
1724
  //# sourceMappingURL=index.js.map