@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/README.md +144 -176
- package/dist/index.js +1531 -778
- package/dist/index.js.map +1 -1
- package/package.json +5 -37
- package/SKILL.md +0 -195
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
|
|
4
|
+
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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.
|
|
41
|
-
this.
|
|
61
|
+
this.statusCode = statusCode;
|
|
62
|
+
this.requestId = requestId;
|
|
63
|
+
this.upstream = upstream;
|
|
42
64
|
this.name = "CliError";
|
|
43
65
|
}
|
|
44
66
|
code;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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 (
|
|
219
|
+
if (cachedVersion !== null) return cachedVersion;
|
|
245
220
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
246
221
|
const candidates = [
|
|
247
|
-
|
|
248
|
-
|
|
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(
|
|
229
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
253
230
|
if (typeof pkg.version === "string") {
|
|
254
|
-
|
|
255
|
-
return
|
|
231
|
+
cachedVersion = pkg.version;
|
|
232
|
+
return cachedVersion;
|
|
256
233
|
}
|
|
257
234
|
} catch {
|
|
258
235
|
}
|
|
259
236
|
}
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
264
|
-
const parse = (v) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
274
|
-
|
|
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
|
-
|
|
294
|
-
apiKey;
|
|
263
|
+
baseUrl;
|
|
295
264
|
timeout;
|
|
296
|
-
constructor(
|
|
297
|
-
this.
|
|
298
|
-
this.
|
|
299
|
-
|
|
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
|
-
"
|
|
319
|
-
"User-Agent": `agenzo-merchant-cli/${getCurrentVersion()}`
|
|
271
|
+
"User-Agent": `agenzo-admin-cli/${getCurrentVersion()}`
|
|
320
272
|
};
|
|
321
|
-
if (
|
|
322
|
-
|
|
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
|
|
325
|
-
let response;
|
|
306
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
326
307
|
try {
|
|
327
|
-
response = await fetch(url, {
|
|
328
|
-
|
|
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
|
|
335
|
-
throw
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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(
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
return
|
|
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 (
|
|
378
|
-
return
|
|
697
|
+
if (config.type === "password") {
|
|
698
|
+
return password({ message: config.message, mask: "*" });
|
|
379
699
|
}
|
|
380
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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/
|
|
390
|
-
|
|
391
|
-
|
|
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"
|
|
395
|
-
if (a === "--format
|
|
792
|
+
if (a === "--format=json") return true;
|
|
793
|
+
if (a === "--format" && argv[i + 1] === "json") return true;
|
|
396
794
|
}
|
|
397
|
-
return
|
|
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 = (
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
"
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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)
|
|
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))
|
|
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
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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(
|
|
455
|
-
lng: num(
|
|
456
|
-
name: need(
|
|
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(
|
|
460
|
-
lng: num(
|
|
461
|
-
name: need(
|
|
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:
|
|
1118
|
+
pickup_time: opts.pickupTime === "now" ? "now" : num(opts.pickupTime, "pickup-time")
|
|
464
1119
|
};
|
|
465
|
-
if (
|
|
466
|
-
if (
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
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 {
|
|
1153
|
+
// src/ride-elife/book.ts
|
|
1154
|
+
import { confirm } from "@inquirer/prompts";
|
|
487
1155
|
|
|
488
|
-
// src/
|
|
489
|
-
|
|
490
|
-
var
|
|
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 (!
|
|
1161
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(trimmed)) {
|
|
495
1162
|
throw new CliError(
|
|
496
|
-
|
|
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,
|
|
504
|
-
if (flagValue !== void 0)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
513
|
-
message: "Idempotency key:",
|
|
514
|
-
validate: (v) =>
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 =
|
|
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:
|
|
595
|
-
price_currency:
|
|
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 (
|
|
600
|
-
if (
|
|
601
|
-
if (
|
|
602
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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 =
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
"
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
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 =
|
|
764
|
-
if (!
|
|
765
|
-
|
|
766
|
-
|
|
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(
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
784
|
-
var
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
"
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
"
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
922
|
-
`Service '${serviceId
|
|
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
|
-
|
|
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
|
-
|
|
932
|
-
return cmd.optsWithGlobals().format === "table" ? "table" : "json";
|
|
933
|
-
}
|
|
1670
|
+
var programRef;
|
|
934
1671
|
async function main() {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|