fortrip 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -74
- package/dist/chunk-OBK2KTLC.js +16333 -0
- package/dist/cli.js +253 -570
- package/dist/mcp-bridge-VEJ33LYD.js +191 -0
- package/package.json +18 -14
- package/AGENT_GUIDE.md +0 -133
- package/guides/planning.md +0 -54
- package/skills/fortrip-hotels/SKILL.md +0 -20
package/dist/cli.js
CHANGED
|
@@ -1,122 +1,193 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
API,
|
|
4
|
+
COMMANDS,
|
|
5
|
+
CliError,
|
|
6
|
+
EXIT,
|
|
7
|
+
GLOBAL_FLAGS,
|
|
8
|
+
VERSION,
|
|
9
|
+
authorizedRequest,
|
|
10
|
+
login,
|
|
11
|
+
logout,
|
|
12
|
+
notices,
|
|
13
|
+
renderGuide,
|
|
14
|
+
resolveApiUrl,
|
|
15
|
+
unwrap,
|
|
16
|
+
usageError,
|
|
17
|
+
whoami
|
|
18
|
+
} from "./chunk-OBK2KTLC.js";
|
|
2
19
|
|
|
3
|
-
// src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
// src/commands.ts
|
|
21
|
+
import { readFileSync } from "fs";
|
|
22
|
+
import { parseArgs } from "util";
|
|
23
|
+
|
|
24
|
+
// src/help.ts
|
|
25
|
+
var WIDTH = 78;
|
|
26
|
+
var OPTIONAL_POSITIONALS = /* @__PURE__ */ new Set(["topic"]);
|
|
27
|
+
function wrap(text, indent) {
|
|
28
|
+
const pad = " ".repeat(indent);
|
|
29
|
+
const lines = [];
|
|
30
|
+
let line = "";
|
|
31
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
32
|
+
if (line && indent + line.length + 1 + word.length > WIDTH) {
|
|
33
|
+
lines.push(pad + line);
|
|
34
|
+
line = word;
|
|
35
|
+
} else {
|
|
36
|
+
line = line ? `${line} ${word}` : word;
|
|
37
|
+
}
|
|
16
38
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
function usageError(message) {
|
|
21
|
-
return new CliError(message, EXIT.usage);
|
|
39
|
+
if (line) lines.push(pad + line);
|
|
40
|
+
return lines;
|
|
22
41
|
}
|
|
42
|
+
function invocation(spec) {
|
|
43
|
+
const positionals = (spec.positionals ?? []).map(
|
|
44
|
+
(name) => OPTIONAL_POSITIONALS.has(name) ? `[${name}]` : `<${name}>`
|
|
45
|
+
);
|
|
46
|
+
return ["fortrip", spec.name, ...positionals].join(" ");
|
|
47
|
+
}
|
|
48
|
+
function usageLine(spec) {
|
|
49
|
+
return `${invocation(spec)} [options]`;
|
|
50
|
+
}
|
|
51
|
+
function names(option, aligned) {
|
|
52
|
+
const short = option.short ? `-${option.short}, ` : aligned ? " " : "";
|
|
53
|
+
const placeholder = option.placeholder ? ` ${option.placeholder}` : "";
|
|
54
|
+
const repeats = "multiple" in option && option.multiple ? "..." : "";
|
|
55
|
+
return `${short}--${option.flag}${placeholder}${repeats}`;
|
|
56
|
+
}
|
|
57
|
+
function optionList(options) {
|
|
58
|
+
const aligned = options.some((option) => option.short);
|
|
59
|
+
return options.map(
|
|
60
|
+
(option) => [` ${names(option, aligned)}`, ...wrap(option.description, 8)].join("\n")
|
|
61
|
+
).join("\n");
|
|
62
|
+
}
|
|
63
|
+
var HELP_FLAG = GLOBAL_FLAGS.filter((option) => option.flag === "help");
|
|
64
|
+
function commandHelp(spec) {
|
|
65
|
+
const options = [...spec.flags, ...spec.options ?? [], ...HELP_FLAG];
|
|
66
|
+
return `Usage: ${usageLine(spec)}
|
|
23
67
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
68
|
+
${wrap(spec.summary, 0).join("\n")}
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
${optionList(options)}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
function commandList() {
|
|
75
|
+
const labels = COMMANDS.map((spec) => invocation(spec).slice("fortrip ".length));
|
|
76
|
+
const column = 2 + Math.max(...labels.map((label) => label.length)) + 2;
|
|
77
|
+
return COMMANDS.map((spec, n) => {
|
|
78
|
+
const [first = "", ...rest] = wrap(spec.summary, column);
|
|
79
|
+
const label = ` ${labels[n]}`.padEnd(column);
|
|
80
|
+
return [label + first.slice(column), ...rest].join("\n");
|
|
81
|
+
}).join("\n");
|
|
82
|
+
}
|
|
83
|
+
function overallHelp() {
|
|
84
|
+
return `fortrip ${VERSION} \u2014 search, price and book Fortrip hotels
|
|
85
|
+
|
|
86
|
+
Usage: fortrip <command> [options]
|
|
87
|
+
fortrip <command> --help
|
|
88
|
+
|
|
89
|
+
Commands:
|
|
90
|
+
${commandList()}
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
${optionList(GLOBAL_FLAGS)}
|
|
94
|
+
|
|
95
|
+
Output is JSON on stdout; errors are JSON on stderr.
|
|
96
|
+
Exit codes: 0 ok, 1 error (retry once when stderr has "retryable": true),
|
|
97
|
+
2 invalid arguments, 3 not logged in, 4 rate limited.
|
|
98
|
+
|
|
99
|
+
Environment:
|
|
100
|
+
FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
|
|
101
|
+
FORTRIP_API_URL API base URL (default https://fortrip.ai)
|
|
102
|
+
FORTRIP_CURRENCY Default --currency for search and rates
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
29
105
|
|
|
30
|
-
// src/
|
|
31
|
-
var
|
|
32
|
-
var
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
106
|
+
// src/commands.ts
|
|
107
|
+
var COMMA_LISTS = /* @__PURE__ */ new Set(["hotelIds", "childrenAges", "boardCodes"]);
|
|
108
|
+
var UPPER_CASE = /* @__PURE__ */ new Set(["nationality", "boardCodes", "currency"]);
|
|
109
|
+
var TURNS_OFF = /* @__PURE__ */ new Set(["bookableOnly"]);
|
|
110
|
+
var POSTS = {
|
|
111
|
+
search: API.search,
|
|
112
|
+
rates: API.rates,
|
|
113
|
+
book: API.bookings
|
|
114
|
+
};
|
|
115
|
+
function parseOptions(spec, argv) {
|
|
116
|
+
const options = {};
|
|
117
|
+
for (const option of [...spec.flags, ...spec.options ?? []]) {
|
|
118
|
+
options[option.flag] = {
|
|
119
|
+
type: option.type === "boolean" ? "boolean" : "string",
|
|
120
|
+
...option.short ? { short: option.short } : {},
|
|
121
|
+
..."multiple" in option && option.multiple ? { multiple: true } : {}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let parsed;
|
|
39
125
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
...token ? { authorization: `Bearer ${token}` } : {}
|
|
47
|
-
},
|
|
48
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
49
|
-
// A redirect to another origin would drop the Authorization header and
|
|
50
|
-
// look like a logout; surface it instead.
|
|
51
|
-
redirect: "manual",
|
|
52
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
126
|
+
parsed = parseArgs({
|
|
127
|
+
args: argv,
|
|
128
|
+
options,
|
|
129
|
+
allowPositionals: true,
|
|
130
|
+
strict: true,
|
|
131
|
+
tokens: true
|
|
53
132
|
});
|
|
54
133
|
} catch (error) {
|
|
55
|
-
throw
|
|
56
|
-
`Could not reach ${apiUrl}: ${error.message}`,
|
|
57
|
-
EXIT.error
|
|
58
|
-
);
|
|
134
|
+
throw usageError(error.message);
|
|
59
135
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
136
|
+
const seen = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const token of parsed.tokens) {
|
|
138
|
+
if (token.kind !== "option" || options[token.name]?.multiple) continue;
|
|
139
|
+
if (seen.has(token.name)) {
|
|
140
|
+
throw usageError(`--${token.name} was given more than once`);
|
|
141
|
+
}
|
|
142
|
+
seen.add(token.name);
|
|
66
143
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
144
|
+
if (!spec.positionals && parsed.positionals.length) {
|
|
145
|
+
throw usageError(
|
|
146
|
+
`${spec.name} takes no arguments, got "${parsed.positionals[0]}"`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return { values: parsed.values, positionals: parsed.positionals };
|
|
73
150
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
const text = typeof error === "string" ? error : typeof message === "string" ? message : `Request failed with status ${status}`;
|
|
79
|
-
const details = { status, ...rest };
|
|
80
|
-
if (status === 401) {
|
|
81
|
-
throw new CliError(`${text}. ${LOGIN_HINT}`, EXIT.unauthenticated, details);
|
|
151
|
+
function parseCommand(spec, argv, env) {
|
|
152
|
+
const post = POSTS[spec.name];
|
|
153
|
+
if (!post && spec.name !== "hotel") {
|
|
154
|
+
throw new Error(`${spec.name} sends no request`);
|
|
82
155
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
156
|
+
const { values, positionals } = parseOptions(spec, argv);
|
|
157
|
+
const input = baseBody(values.body);
|
|
158
|
+
for (const flag of spec.flags) {
|
|
159
|
+
const raw = values[flag.flag];
|
|
160
|
+
if (raw !== void 0) setField(input, flag.field, readFlag(flag, raw));
|
|
89
161
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
162
|
+
if (spec.name === "hotel") {
|
|
163
|
+
const [hotelId, ...extra] = positionals;
|
|
164
|
+
const parsed = Number(hotelId);
|
|
165
|
+
if (!hotelId || extra.length || !/^\d+$/.test(hotelId) || !Number.isSafeInteger(parsed)) {
|
|
166
|
+
throw usageError(`Usage: ${usageLine(spec)}`);
|
|
167
|
+
}
|
|
168
|
+
return { path: API.hotel(parsed, input.includeRooms === true) };
|
|
97
169
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// src/args.ts
|
|
102
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
103
|
-
import { parseArgs } from "util";
|
|
104
|
-
var BODY_OPTION = { body: { type: "string" } };
|
|
105
|
-
var CURRENCY_OPTION = { currency: { type: "string" } };
|
|
106
|
-
function parse(argv, options) {
|
|
107
|
-
try {
|
|
108
|
-
return parseArgs({ args: argv, options, allowPositionals: true, strict: true });
|
|
109
|
-
} catch (error) {
|
|
110
|
-
throw usageError(error.message);
|
|
170
|
+
if (spec.flags.some((flag) => flag.field === "currency") && input.currency === void 0) {
|
|
171
|
+
const fallback = env.FORTRIP_CURRENCY?.trim().toUpperCase();
|
|
172
|
+
if (fallback) input.currency = fallback;
|
|
111
173
|
}
|
|
174
|
+
const occupancy = spec.flags.filter(
|
|
175
|
+
(flag) => flag.field === "rooms" || flag.field === "adults"
|
|
176
|
+
);
|
|
177
|
+
const missing = occupancy.filter((flag) => input[flag.field] === void 0);
|
|
178
|
+
if (occupancy.length === 2 && missing.length) {
|
|
179
|
+
throw usageError(
|
|
180
|
+
`${spec.name} needs ${missing.map((flag) => `--${flag.flag}`).join(" and ")}, per room and the same for every room (2 rooms for 4 adults is --rooms 2 --adults 2)`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { body: input, path: post };
|
|
112
184
|
}
|
|
113
|
-
function baseBody(
|
|
114
|
-
const raw = values.body;
|
|
185
|
+
function baseBody(raw) {
|
|
115
186
|
if (typeof raw !== "string") return {};
|
|
116
187
|
let text = raw;
|
|
117
188
|
if (raw.startsWith("@")) {
|
|
118
189
|
try {
|
|
119
|
-
text =
|
|
190
|
+
text = readFileSync(raw.slice(1), "utf8");
|
|
120
191
|
} catch (error) {
|
|
121
192
|
throw usageError(`Cannot read --body file: ${error.message}`);
|
|
122
193
|
}
|
|
@@ -130,151 +201,55 @@ function baseBody(values) {
|
|
|
130
201
|
}
|
|
131
202
|
throw usageError("--body must be a JSON object");
|
|
132
203
|
}
|
|
204
|
+
function setField(body, field, value) {
|
|
205
|
+
const path = field.split(".");
|
|
206
|
+
const last = path.pop();
|
|
207
|
+
let node = body;
|
|
208
|
+
for (const part of path) {
|
|
209
|
+
const next = node[part];
|
|
210
|
+
node[part] = next && typeof next === "object" && !Array.isArray(next) ? next : {};
|
|
211
|
+
node = node[part];
|
|
212
|
+
}
|
|
213
|
+
node[last] = value;
|
|
214
|
+
}
|
|
215
|
+
function readFlag(flag, raw) {
|
|
216
|
+
if (flag.type === "boolean") return !TURNS_OFF.has(flag.field);
|
|
217
|
+
const given = Array.isArray(raw) ? raw : [raw];
|
|
218
|
+
if (flag.field === "near") return parseNear(given[0]);
|
|
219
|
+
if (flag.field === "guests") return given.map(parseGuest);
|
|
220
|
+
const parts = COMMA_LISTS.has(flag.field) ? given.flatMap((value) => value.split(",")).map((part) => part.trim()) : given;
|
|
221
|
+
const values = parts.map((part) => {
|
|
222
|
+
const text = UPPER_CASE.has(flag.field) ? part.trim().toUpperCase() : part;
|
|
223
|
+
if (flag.field === "currency" && text === "") {
|
|
224
|
+
throw usageError(`--${flag.flag} must not be empty`);
|
|
225
|
+
}
|
|
226
|
+
if (flag.type === "number") return num(flag.flag, text);
|
|
227
|
+
return flag.type === "id" ? id(flag.flag, text) : text;
|
|
228
|
+
});
|
|
229
|
+
return flag.multiple ? values : values[0];
|
|
230
|
+
}
|
|
133
231
|
function num(flag, value) {
|
|
134
|
-
if (value === void 0) return void 0;
|
|
135
232
|
const parsed = Number(value);
|
|
136
233
|
if (value.trim() === "" || !Number.isFinite(parsed)) {
|
|
137
234
|
throw usageError(`--${flag} must be a number, got "${value}"`);
|
|
138
235
|
}
|
|
139
236
|
return parsed;
|
|
140
237
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
function str(value) {
|
|
146
|
-
return typeof value === "string" ? value : void 0;
|
|
147
|
-
}
|
|
148
|
-
function list(value) {
|
|
149
|
-
return Array.isArray(value) && value.length ? value : void 0;
|
|
150
|
-
}
|
|
151
|
-
function currency(values, base, env) {
|
|
152
|
-
const flag = str(values.currency)?.trim().toUpperCase();
|
|
153
|
-
if (flag === "") throw usageError("--currency must not be empty");
|
|
154
|
-
if (flag) return flag;
|
|
155
|
-
if (base.currency !== void 0) return void 0;
|
|
156
|
-
return env.FORTRIP_CURRENCY?.trim().toUpperCase() || void 0;
|
|
157
|
-
}
|
|
158
|
-
function merge(base, overrides) {
|
|
159
|
-
const body = { ...base };
|
|
160
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
161
|
-
if (value !== void 0) body[key] = value;
|
|
162
|
-
}
|
|
163
|
-
return body;
|
|
164
|
-
}
|
|
165
|
-
var SEARCH_OPTIONS = {
|
|
166
|
-
...BODY_OPTION,
|
|
167
|
-
...CURRENCY_OPTION,
|
|
168
|
-
query: { type: "string", short: "q" },
|
|
169
|
-
"city-code": { type: "string" },
|
|
170
|
-
near: { type: "string" },
|
|
171
|
-
"check-in": { type: "string" },
|
|
172
|
-
"check-out": { type: "string" },
|
|
173
|
-
"star-min": { type: "string" },
|
|
174
|
-
"star-max": { type: "string" },
|
|
175
|
-
facility: { type: "string", multiple: true },
|
|
176
|
-
"price-min": { type: "string" },
|
|
177
|
-
"price-max": { type: "string" },
|
|
178
|
-
sort: { type: "string" },
|
|
179
|
-
page: { type: "string" },
|
|
180
|
-
"page-size": { type: "string" }
|
|
181
|
-
};
|
|
182
|
-
function searchBody(values, env = process.env) {
|
|
183
|
-
const base = baseBody(values);
|
|
184
|
-
let near;
|
|
185
|
-
const nearRaw = str(values.near);
|
|
186
|
-
if (nearRaw !== void 0) {
|
|
187
|
-
const [lat, lng, radiusKm] = nearRaw.split(",").map((part) => part.trim());
|
|
188
|
-
if (!lat || !lng || !radiusKm) {
|
|
189
|
-
throw usageError("--near must be lat,lng,radiusKm (e.g. 13.75,100.5,5)");
|
|
190
|
-
}
|
|
191
|
-
near = {
|
|
192
|
-
lat: num("near", lat),
|
|
193
|
-
lng: num("near", lng),
|
|
194
|
-
radiusKm: num("near", radiusKm)
|
|
195
|
-
};
|
|
238
|
+
function id(flag, value) {
|
|
239
|
+
const parsed = Number(value.trim());
|
|
240
|
+
if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed)) {
|
|
241
|
+
throw usageError(`--${flag} must be a numeric id, got "${value}"`);
|
|
196
242
|
}
|
|
197
|
-
return
|
|
198
|
-
query: str(values.query),
|
|
199
|
-
cityCode: num("city-code", str(values["city-code"])),
|
|
200
|
-
near,
|
|
201
|
-
checkIn: str(values["check-in"]),
|
|
202
|
-
checkOut: str(values["check-out"]),
|
|
203
|
-
starMin: num("star-min", str(values["star-min"])),
|
|
204
|
-
starMax: num("star-max", str(values["star-max"])),
|
|
205
|
-
facilities: list(values.facility),
|
|
206
|
-
priceMin: num("price-min", str(values["price-min"])),
|
|
207
|
-
priceMax: num("price-max", str(values["price-max"])),
|
|
208
|
-
sortBy: str(values.sort),
|
|
209
|
-
page: num("page", str(values.page)),
|
|
210
|
-
pageSize: num("page-size", str(values["page-size"])),
|
|
211
|
-
currency: currency(values, base, env)
|
|
212
|
-
});
|
|
243
|
+
return parsed;
|
|
213
244
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const [id, ...extra] = positionals;
|
|
219
|
-
if (!id || extra.length || !/^\d+$/.test(id)) {
|
|
220
|
-
throw usageError("Usage: fortrip hotel <hotelId> [--with-rooms]");
|
|
245
|
+
function parseNear(value) {
|
|
246
|
+
const parts = value.split(",").map((part) => part.trim());
|
|
247
|
+
if (parts.length !== 3 || parts.some((part) => !part)) {
|
|
248
|
+
throw usageError("--near must be lat,lng,radiusKm (e.g. 13.75,100.5,5)");
|
|
221
249
|
}
|
|
222
|
-
|
|
250
|
+
const [lat, lng, radiusKm] = parts.map((part) => num("near", part));
|
|
251
|
+
return { lat, lng, radiusKm };
|
|
223
252
|
}
|
|
224
|
-
var STAY_OPTIONS = {
|
|
225
|
-
"check-in": { type: "string" },
|
|
226
|
-
"check-out": { type: "string" },
|
|
227
|
-
rooms: { type: "string" },
|
|
228
|
-
adults: { type: "string" },
|
|
229
|
-
"child-age": { type: "string", multiple: true }
|
|
230
|
-
};
|
|
231
|
-
function stayFields(values, base) {
|
|
232
|
-
return {
|
|
233
|
-
checkIn: str(values["check-in"]),
|
|
234
|
-
checkOut: str(values["check-out"]),
|
|
235
|
-
rooms: num("rooms", str(values.rooms)) ?? (base.rooms === void 0 ? 1 : void 0),
|
|
236
|
-
adults: num("adults", str(values.adults)) ?? (base.adults === void 0 ? 2 : void 0),
|
|
237
|
-
childrenAges: nums("child-age", list(values["child-age"]))
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
var RATES_OPTIONS = {
|
|
241
|
-
...BODY_OPTION,
|
|
242
|
-
...CURRENCY_OPTION,
|
|
243
|
-
...STAY_OPTIONS,
|
|
244
|
-
hotel: { type: "string", multiple: true },
|
|
245
|
-
nationality: { type: "string" },
|
|
246
|
-
board: { type: "string", multiple: true },
|
|
247
|
-
"free-cancellation": { type: "boolean" },
|
|
248
|
-
"include-unbookable": { type: "boolean" },
|
|
249
|
-
"max-rooms": { type: "string" }
|
|
250
|
-
};
|
|
251
|
-
function ratesBody(values, env = process.env) {
|
|
252
|
-
const base = baseBody(values);
|
|
253
|
-
return merge(base, {
|
|
254
|
-
hotelIds: nums("hotel", list(values.hotel)),
|
|
255
|
-
...stayFields(values, base),
|
|
256
|
-
nationality: str(values.nationality)?.toUpperCase(),
|
|
257
|
-
boardCodes: list(values.board)?.flatMap(
|
|
258
|
-
(code) => code.split(",").map((part) => part.trim().toUpperCase())
|
|
259
|
-
),
|
|
260
|
-
freeCancellation: values["free-cancellation"] ? true : void 0,
|
|
261
|
-
bookableOnly: values["include-unbookable"] ? false : void 0,
|
|
262
|
-
maxRoomsPerHotel: num("max-rooms", str(values["max-rooms"])),
|
|
263
|
-
currency: currency(values, base, env)
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
var BOOK_OPTIONS = {
|
|
267
|
-
...BODY_OPTION,
|
|
268
|
-
...STAY_OPTIONS,
|
|
269
|
-
hotel: { type: "string" },
|
|
270
|
-
"room-id": { type: "string" },
|
|
271
|
-
"rate-plan": { type: "string" },
|
|
272
|
-
guest: { type: "string", multiple: true },
|
|
273
|
-
"contact-name": { type: "string" },
|
|
274
|
-
"contact-phone": { type: "string" },
|
|
275
|
-
"contact-email": { type: "string" },
|
|
276
|
-
"special-requests": { type: "string" }
|
|
277
|
-
};
|
|
278
253
|
function parseGuest(value) {
|
|
279
254
|
const match = value.trim().match(/^(.+?)(?::(\d+))?$/);
|
|
280
255
|
const words = match?.[1]?.trim().split(/\s+/) ?? [];
|
|
@@ -289,390 +264,98 @@ function parseGuest(value) {
|
|
|
289
264
|
roomNo: match?.[2] ? Number(match[2]) : 1
|
|
290
265
|
};
|
|
291
266
|
}
|
|
292
|
-
function bookBody(values) {
|
|
293
|
-
const base = baseBody(values);
|
|
294
|
-
return merge(base, {
|
|
295
|
-
hotelId: num("hotel", str(values.hotel)),
|
|
296
|
-
roomId: str(values["room-id"]),
|
|
297
|
-
ratePlanCode: str(values["rate-plan"]),
|
|
298
|
-
...stayFields(values, base),
|
|
299
|
-
guests: list(values.guest)?.map(parseGuest),
|
|
300
|
-
contactName: str(values["contact-name"]),
|
|
301
|
-
contactPhone: str(values["contact-phone"]),
|
|
302
|
-
contactEmail: str(values["contact-email"]),
|
|
303
|
-
specialRequests: str(values["special-requests"])
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// src/auth.ts
|
|
308
|
-
import { hostname } from "os";
|
|
309
|
-
import { setTimeout as sleep } from "timers/promises";
|
|
310
|
-
|
|
311
|
-
// src/browser.ts
|
|
312
|
-
import { spawn } from "child_process";
|
|
313
|
-
function openBrowser(url) {
|
|
314
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
|
|
315
|
-
try {
|
|
316
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
317
|
-
child.on("error", () => {
|
|
318
|
-
});
|
|
319
|
-
child.unref();
|
|
320
|
-
} catch {
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/credentials.ts
|
|
325
|
-
import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
326
|
-
import { homedir } from "os";
|
|
327
|
-
import { dirname, join } from "path";
|
|
328
|
-
var DEFAULT_API_URL = "https://fortrip.ai";
|
|
329
|
-
function credentialsPath(env = process.env) {
|
|
330
|
-
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
331
|
-
return join(base, "fortrip", "credentials.json");
|
|
332
|
-
}
|
|
333
|
-
function resolveApiUrl(env = process.env) {
|
|
334
|
-
const raw = env.FORTRIP_API_URL?.trim() || DEFAULT_API_URL;
|
|
335
|
-
let url;
|
|
336
|
-
try {
|
|
337
|
-
url = new URL(raw);
|
|
338
|
-
} catch {
|
|
339
|
-
throw usageError(`FORTRIP_API_URL is not a valid URL: ${raw}`);
|
|
340
|
-
}
|
|
341
|
-
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
342
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
|
|
343
|
-
throw usageError("FORTRIP_API_URL must use https (http only for localhost)");
|
|
344
|
-
}
|
|
345
|
-
return url.origin;
|
|
346
|
-
}
|
|
347
|
-
async function readCredentials(path) {
|
|
348
|
-
try {
|
|
349
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
350
|
-
return { tokens: parsed?.tokens ?? {}, pending: parsed?.pending ?? {} };
|
|
351
|
-
} catch {
|
|
352
|
-
return { tokens: {}, pending: {} };
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
async function writeCredentials(path, file) {
|
|
356
|
-
if (Object.keys(file.tokens).length === 0 && Object.keys(file.pending).length === 0) {
|
|
357
|
-
await rm(path, { force: true });
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
361
|
-
await writeFile(path, `${JSON.stringify(file, null, 2)}
|
|
362
|
-
`, { mode: 384 });
|
|
363
|
-
await chmod(path, 384);
|
|
364
|
-
}
|
|
365
|
-
async function loadStoredToken(apiUrl, env = process.env) {
|
|
366
|
-
const file = await readCredentials(credentialsPath(env));
|
|
367
|
-
return file.tokens[apiUrl]?.accessToken ?? null;
|
|
368
|
-
}
|
|
369
|
-
async function resolveToken(apiUrl, env = process.env) {
|
|
370
|
-
const fromEnv = env.FORTRIP_TOKEN?.trim();
|
|
371
|
-
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
372
|
-
const stored = await loadStoredToken(apiUrl, env);
|
|
373
|
-
return stored ? { token: stored, source: "file" } : null;
|
|
374
|
-
}
|
|
375
|
-
async function saveToken(apiUrl, accessToken, env = process.env) {
|
|
376
|
-
const path = credentialsPath(env);
|
|
377
|
-
const file = await readCredentials(path);
|
|
378
|
-
file.tokens[apiUrl] = { accessToken, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
379
|
-
delete file.pending[apiUrl];
|
|
380
|
-
await writeCredentials(path, file);
|
|
381
|
-
}
|
|
382
|
-
async function deleteToken(apiUrl, env = process.env) {
|
|
383
|
-
const path = credentialsPath(env);
|
|
384
|
-
const file = await readCredentials(path);
|
|
385
|
-
delete file.tokens[apiUrl];
|
|
386
|
-
await writeCredentials(path, file);
|
|
387
|
-
}
|
|
388
|
-
async function loadPendingLogin(apiUrl, env = process.env) {
|
|
389
|
-
const pending = (await readCredentials(credentialsPath(env))).pending[apiUrl];
|
|
390
|
-
return pending && Date.parse(pending.expiresAt) > Date.now() ? pending : null;
|
|
391
|
-
}
|
|
392
|
-
async function savePendingLogin(apiUrl, pending, env = process.env) {
|
|
393
|
-
const path = credentialsPath(env);
|
|
394
|
-
const file = await readCredentials(path);
|
|
395
|
-
if (pending) file.pending[apiUrl] = pending;
|
|
396
|
-
else delete file.pending[apiUrl];
|
|
397
|
-
await writeCredentials(path, file);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// src/auth.ts
|
|
401
|
-
async function requireToken(apiUrl) {
|
|
402
|
-
const resolved = await resolveToken(apiUrl);
|
|
403
|
-
if (resolved) return resolved.token;
|
|
404
|
-
const pending = await loadPendingLogin(apiUrl);
|
|
405
|
-
if (pending) {
|
|
406
|
-
const result = await exchange(apiUrl, pending.deviceCode);
|
|
407
|
-
if (result.status === "ok") {
|
|
408
|
-
await saveToken(apiUrl, result.accessToken);
|
|
409
|
-
return result.accessToken;
|
|
410
|
-
}
|
|
411
|
-
if (result.status === "authorization_pending" || result.status === "slow_down") {
|
|
412
|
-
throw loginRequired(apiUrl, pending);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
const start = await startDeviceLogin(apiUrl);
|
|
416
|
-
const fresh = {
|
|
417
|
-
deviceCode: start.deviceCode,
|
|
418
|
-
userCode: start.userCode,
|
|
419
|
-
loginUrl: start.verificationUriComplete,
|
|
420
|
-
expiresAt: new Date(Date.now() + start.expiresIn * 1e3).toISOString()
|
|
421
|
-
};
|
|
422
|
-
await savePendingLogin(apiUrl, fresh);
|
|
423
|
-
throw loginRequired(apiUrl, fresh);
|
|
424
|
-
}
|
|
425
|
-
async function login({
|
|
426
|
-
apiUrl,
|
|
427
|
-
openBrowser: shouldOpen,
|
|
428
|
-
log,
|
|
429
|
-
wait = sleep
|
|
430
|
-
}) {
|
|
431
|
-
const start = await startDeviceLogin(apiUrl);
|
|
432
|
-
log(`Confirm this code in your browser: ${start.userCode}`);
|
|
433
|
-
log(`If the browser doesn't open, visit: ${start.verificationUriComplete}`);
|
|
434
|
-
if (shouldOpen) openBrowser(start.verificationUriComplete);
|
|
435
|
-
log("Waiting for approval\u2026");
|
|
436
|
-
let interval = start.interval;
|
|
437
|
-
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
438
|
-
while (Date.now() < deadline) {
|
|
439
|
-
await wait(interval * 1e3);
|
|
440
|
-
const result = await exchange(apiUrl, start.deviceCode);
|
|
441
|
-
switch (result.status) {
|
|
442
|
-
case "ok":
|
|
443
|
-
await saveToken(apiUrl, result.accessToken);
|
|
444
|
-
log("Logged in.");
|
|
445
|
-
return whoami(apiUrl, result.accessToken);
|
|
446
|
-
case "authorization_pending":
|
|
447
|
-
continue;
|
|
448
|
-
case "slow_down":
|
|
449
|
-
interval = result.interval ?? interval + 5;
|
|
450
|
-
continue;
|
|
451
|
-
case "access_denied":
|
|
452
|
-
throw new CliError("Login was denied in the browser.", EXIT.error);
|
|
453
|
-
case "expired_token":
|
|
454
|
-
throw new CliError(
|
|
455
|
-
"The login code expired. Run `login` again.",
|
|
456
|
-
EXIT.error
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
throw new CliError("The login code expired. Run `login` again.", EXIT.error);
|
|
461
|
-
}
|
|
462
|
-
async function startDeviceLogin(apiUrl) {
|
|
463
|
-
return unwrap(
|
|
464
|
-
await request(apiUrl, "/api/cli/auth/device", {
|
|
465
|
-
method: "POST",
|
|
466
|
-
body: { label: deviceLabel(), platform: process.platform }
|
|
467
|
-
})
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
async function exchange(apiUrl, deviceCode) {
|
|
471
|
-
const poll = await request(apiUrl, "/api/cli/auth/token", {
|
|
472
|
-
method: "POST",
|
|
473
|
-
body: { deviceCode }
|
|
474
|
-
});
|
|
475
|
-
if (poll.status === 200 && poll.body.success === true) {
|
|
476
|
-
const { accessToken } = poll.body.data;
|
|
477
|
-
return { status: "ok", accessToken };
|
|
478
|
-
}
|
|
479
|
-
switch (poll.body.error) {
|
|
480
|
-
case "authorization_pending":
|
|
481
|
-
case "access_denied":
|
|
482
|
-
case "expired_token":
|
|
483
|
-
return { status: poll.body.error };
|
|
484
|
-
case "slow_down":
|
|
485
|
-
return {
|
|
486
|
-
status: "slow_down",
|
|
487
|
-
interval: typeof poll.body.interval === "number" ? poll.body.interval : void 0
|
|
488
|
-
};
|
|
489
|
-
default:
|
|
490
|
-
unwrap(poll);
|
|
491
|
-
throw new CliError(`Unexpected login response ${poll.status}`, EXIT.error);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
function loginRequired(apiUrl, pending) {
|
|
495
|
-
return new CliError(
|
|
496
|
-
`Not logged in to ${apiUrl}. Ask the user to open loginUrl and approve, then rerun this command (or run \`npx fortrip login\` in a terminal).`,
|
|
497
|
-
EXIT.unauthenticated,
|
|
498
|
-
{
|
|
499
|
-
loginUrl: pending.loginUrl,
|
|
500
|
-
userCode: pending.userCode,
|
|
501
|
-
expiresInSeconds: Math.max(
|
|
502
|
-
0,
|
|
503
|
-
Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
async function logout(apiUrl) {
|
|
509
|
-
const token = await loadStoredToken(apiUrl);
|
|
510
|
-
if (!token) {
|
|
511
|
-
throw new CliError(`Not logged in to ${apiUrl}.`, EXIT.unauthenticated);
|
|
512
|
-
}
|
|
513
|
-
const response = await request(apiUrl, "/api/cli/auth/revoke", {
|
|
514
|
-
method: "POST",
|
|
515
|
-
token
|
|
516
|
-
});
|
|
517
|
-
if (response.status !== 401) unwrap(response);
|
|
518
|
-
await deleteToken(apiUrl);
|
|
519
|
-
return { loggedOut: true, apiUrl };
|
|
520
|
-
}
|
|
521
|
-
async function whoami(apiUrl, token) {
|
|
522
|
-
const resolved = token ?? await requireToken(apiUrl);
|
|
523
|
-
const data = unwrap(
|
|
524
|
-
await request(apiUrl, "/api/cli/auth/whoami", { token: resolved })
|
|
525
|
-
);
|
|
526
|
-
return { ...data, apiUrl };
|
|
527
|
-
}
|
|
528
|
-
function deviceLabel() {
|
|
529
|
-
return hostname().replace(/[^\w .@-]/g, "").slice(0, 64);
|
|
530
|
-
}
|
|
531
267
|
|
|
532
268
|
// src/guide.ts
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
main: "../AGENT_GUIDE.md",
|
|
536
|
-
planning: "../guides/planning.md"
|
|
537
|
-
};
|
|
538
|
-
async function readGuide(topic = "main") {
|
|
539
|
-
const path = GUIDES[topic];
|
|
540
|
-
if (!path) throw usageError("Usage: fortrip skill [planning]");
|
|
541
|
-
const guide = await readFile2(new URL(path, import.meta.url), "utf8");
|
|
542
|
-
return guide.replaceAll("fortrip@latest", `fortrip@${VERSION}`);
|
|
269
|
+
function readGuide(topic) {
|
|
270
|
+
return renderGuide("cli", topic).replaceAll("fortrip@latest", `fortrip@${VERSION}`);
|
|
543
271
|
}
|
|
544
272
|
|
|
545
273
|
// src/cli.ts
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
logout Revoke and forget the saved token
|
|
553
|
-
whoami Show the signed-in account
|
|
554
|
-
search Find hotels with indicative prices
|
|
555
|
-
hotel <hotelId> Static hotel detail [--with-rooms]
|
|
556
|
-
rates Live rooms and rates for up to 15 hotels
|
|
557
|
-
book Create an order and get a payment link
|
|
558
|
-
skill [planning] Print the guide for AI agents (Markdown)
|
|
559
|
-
|
|
560
|
-
search: --query <text> | --city-code <n> | --near <lat,lng,km>
|
|
561
|
-
--check-in YYYY-MM-DD --check-out YYYY-MM-DD
|
|
562
|
-
[--star-min n] [--star-max n] [--facility <name>]... [--price-min n]
|
|
563
|
-
[--price-max n] [--sort recommended|price-asc|price-desc|star-desc|distance]
|
|
564
|
-
[--page n] [--page-size n] [--currency CNY|USD]
|
|
565
|
-
rates: --hotel <id>... --check-in --check-out [--rooms 1] [--adults 2]
|
|
566
|
-
[--child-age n]... [--nationality CN] [--board BB]...
|
|
567
|
-
[--free-cancellation] [--include-unbookable] [--max-rooms n]
|
|
568
|
-
[--currency CNY|USD]
|
|
569
|
-
book: --hotel <id> --room-id <id> --rate-plan <code> --check-in --check-out
|
|
570
|
-
[--rooms 1] [--adults 2] [--child-age n]...
|
|
571
|
-
--guest "First Last[:roomNo]"... --contact-name <name>
|
|
572
|
-
--contact-phone <phone> [--contact-email <email>]
|
|
573
|
-
[--special-requests <english text>]
|
|
574
|
-
|
|
575
|
-
search, rates and book also take --body '<json>' (or --body @file.json) with
|
|
576
|
-
the raw request body; flags override its fields.
|
|
577
|
-
|
|
578
|
-
search and rates ask for prices in --currency (CNY by default, or USD),
|
|
579
|
-
converted at Stripe's rate or a fixed fallback rate; the response's currency
|
|
580
|
-
and fx fields say which. book still charges the currency it returns (usually
|
|
581
|
-
USD).
|
|
582
|
-
|
|
583
|
-
Output is JSON on stdout; errors are JSON on stderr.
|
|
584
|
-
Exit codes: 0 ok, 1 error, 2 invalid arguments, 3 not logged in, 4 rate limited.
|
|
585
|
-
|
|
586
|
-
Environment:
|
|
587
|
-
FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
|
|
588
|
-
FORTRIP_API_URL API base URL (default https://fortrip.ai)
|
|
589
|
-
FORTRIP_CURRENCY Default --currency for search and rates
|
|
590
|
-
`;
|
|
274
|
+
function globalFlag(name) {
|
|
275
|
+
const option = GLOBAL_FLAGS.find((candidate) => candidate.flag === name);
|
|
276
|
+
return option ? [`--${option.flag}`, ...option.short ? [`-${option.short}`] : []] : [];
|
|
277
|
+
}
|
|
278
|
+
var HELP_FLAGS = globalFlag("help");
|
|
279
|
+
var VERSION_FLAGS = globalFlag("version");
|
|
591
280
|
async function run(argv) {
|
|
592
281
|
const [command, ...rest] = argv;
|
|
593
|
-
if (!command || command === "help" || command
|
|
594
|
-
process.stdout.write(
|
|
282
|
+
if (!command || command === "help" || HELP_FLAGS.includes(command)) {
|
|
283
|
+
process.stdout.write(overallHelp());
|
|
595
284
|
return void 0;
|
|
596
285
|
}
|
|
597
|
-
if (command
|
|
286
|
+
if (VERSION_FLAGS.includes(command)) {
|
|
598
287
|
process.stdout.write(`${VERSION}
|
|
599
288
|
`);
|
|
600
289
|
return void 0;
|
|
601
290
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
291
|
+
const spec = COMMANDS.find((candidate) => candidate.name === command);
|
|
292
|
+
if (!spec) {
|
|
293
|
+
throw usageError(`Unknown command "${command}". Run \`fortrip --help\`.`);
|
|
294
|
+
}
|
|
295
|
+
if (wantsHelp(rest)) {
|
|
296
|
+
process.stdout.write(commandHelp(spec));
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
if (spec.name === "guide") {
|
|
300
|
+
const { positionals } = parseOptions(spec, rest);
|
|
301
|
+
const topic = positionals.join(" ");
|
|
302
|
+
if (topic !== "" && topic !== "planning") {
|
|
303
|
+
throw usageError("Usage: fortrip guide [planning]");
|
|
304
|
+
}
|
|
305
|
+
process.stdout.write(readGuide(topic || "main"));
|
|
605
306
|
return void 0;
|
|
606
307
|
}
|
|
607
308
|
const apiUrl = resolveApiUrl();
|
|
608
309
|
const log = (line) => process.stderr.write(`${line}
|
|
609
310
|
`);
|
|
610
|
-
switch (
|
|
311
|
+
switch (spec.name) {
|
|
611
312
|
case "login": {
|
|
612
|
-
const { values } =
|
|
313
|
+
const { values } = parseOptions(spec, rest);
|
|
613
314
|
if (process.env.FORTRIP_TOKEN) {
|
|
614
315
|
log("Note: FORTRIP_TOKEN is set and takes precedence over this login.");
|
|
615
316
|
}
|
|
616
317
|
return login({ apiUrl, openBrowser: !values["no-browser"], log });
|
|
617
318
|
}
|
|
618
319
|
case "logout":
|
|
619
|
-
|
|
320
|
+
parseOptions(spec, rest);
|
|
620
321
|
return logout(apiUrl);
|
|
621
322
|
case "whoami":
|
|
622
|
-
|
|
323
|
+
parseOptions(spec, rest);
|
|
623
324
|
return whoami(apiUrl);
|
|
624
|
-
case "
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
case "hotel": {
|
|
629
|
-
const { values, positionals } = parse(rest, HOTEL_OPTIONS);
|
|
630
|
-
return call(apiUrl, "GET", hotelPath(positionals, values));
|
|
325
|
+
case "mcp": {
|
|
326
|
+
parseOptions(spec, rest);
|
|
327
|
+
const { runMcpBridge } = await import("./mcp-bridge-VEJ33LYD.js");
|
|
328
|
+
return runMcpBridge(apiUrl);
|
|
631
329
|
}
|
|
632
|
-
|
|
633
|
-
const {
|
|
634
|
-
return call(apiUrl, "POST"
|
|
330
|
+
default: {
|
|
331
|
+
const { body, path } = parseCommand(spec, rest, process.env);
|
|
332
|
+
return call(apiUrl, body ? "POST" : "GET", path, body);
|
|
635
333
|
}
|
|
636
|
-
case "book": {
|
|
637
|
-
const { values } = parse(rest, BOOK_OPTIONS);
|
|
638
|
-
const data = await call(
|
|
639
|
-
apiUrl,
|
|
640
|
-
"POST",
|
|
641
|
-
"/api/huizhi/bookings",
|
|
642
|
-
bookBody(values)
|
|
643
|
-
);
|
|
644
|
-
return absolutize(apiUrl, data, ["orderUrl", "statusUrl"]);
|
|
645
|
-
}
|
|
646
|
-
default:
|
|
647
|
-
throw usageError(`Unknown command "${command}". Run \`fortrip --help\`.`);
|
|
648
334
|
}
|
|
649
335
|
}
|
|
650
336
|
async function call(apiUrl, method, path, body) {
|
|
651
|
-
|
|
652
|
-
return unwrap(await request(apiUrl, path, { method, token, body }));
|
|
337
|
+
return unwrap(await authorizedRequest(apiUrl, path, { method, body }));
|
|
653
338
|
}
|
|
654
|
-
function
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (typeof value === "string" && value.startsWith("/")) {
|
|
659
|
-
out[key] = `${apiUrl}${value}`;
|
|
660
|
-
}
|
|
339
|
+
function wantsHelp(args) {
|
|
340
|
+
for (const arg of args) {
|
|
341
|
+
if (arg === "--") return false;
|
|
342
|
+
if (HELP_FLAGS.includes(arg)) return true;
|
|
661
343
|
}
|
|
662
|
-
return
|
|
344
|
+
return false;
|
|
663
345
|
}
|
|
664
346
|
async function main() {
|
|
665
347
|
try {
|
|
666
348
|
const result = await run(process.argv.slice(2));
|
|
667
349
|
if (result !== void 0) {
|
|
668
|
-
|
|
350
|
+
const output = result && typeof result === "object" && !Array.isArray(result) ? { ...result, ...notices } : result;
|
|
351
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
669
352
|
`);
|
|
670
353
|
}
|
|
671
354
|
return EXIT.ok;
|
|
672
355
|
} catch (error) {
|
|
673
356
|
const failure = error instanceof CliError ? error : new CliError(error?.message ?? String(error), EXIT.error);
|
|
674
357
|
process.stderr.write(
|
|
675
|
-
`${JSON.stringify({ error: failure.message, exitCode: failure.exitCode, ...failure.details }, null, 2)}
|
|
358
|
+
`${JSON.stringify({ error: failure.message, exitCode: failure.exitCode, ...failure.details, ...notices }, null, 2)}
|
|
676
359
|
`
|
|
677
360
|
);
|
|
678
361
|
return failure.exitCode;
|