fortrip 2.0.0 → 3.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/dist/cli.js CHANGED
@@ -4,20 +4,28 @@ import {
4
4
  COMMANDS,
5
5
  COMMAND_GROUPS,
6
6
  CliError,
7
+ DEFAULT_API_URL,
8
+ DEFAULT_MCP_URL,
7
9
  EXIT,
8
10
  GLOBAL_FLAGS,
11
+ LIMITS,
12
+ TOOLS,
9
13
  VERSION,
10
14
  authorizedRequest,
11
15
  commandInvocation,
16
+ fieldLines,
17
+ formatError,
12
18
  login,
13
19
  logout,
14
20
  notices,
15
21
  renderGuide,
22
+ renderToolResult,
16
23
  resolveApiUrl,
24
+ resolveMcpUrl,
17
25
  unwrap,
18
26
  usageError,
19
27
  whoami
20
- } from "./chunk-53656LB2.js";
28
+ } from "./chunk-26IHFJYT.js";
21
29
 
22
30
  // src/commands.ts
23
31
  import { readFileSync } from "fs";
@@ -25,7 +33,6 @@ import { parseArgs } from "util";
25
33
 
26
34
  // src/help.ts
27
35
  var WIDTH = 78;
28
- var OPTIONAL_POSITIONALS = /* @__PURE__ */ new Set(["topic"]);
29
36
  function wrap(text, indent) {
30
37
  const pad = " ".repeat(indent);
31
38
  const lines = [];
@@ -43,7 +50,7 @@ function wrap(text, indent) {
43
50
  }
44
51
  function positionals(spec) {
45
52
  return (spec.positionals ?? []).map(
46
- (name) => OPTIONAL_POSITIONALS.has(name) ? `[${name}]` : `<${name}>`
53
+ ({ name, optional }) => optional ? `[${name}]` : `<${name}>`
47
54
  );
48
55
  }
49
56
  function label(spec) {
@@ -53,13 +60,13 @@ function usageLine(spec) {
53
60
  return [commandInvocation(spec), ...positionals(spec), "[options]"].join(" ");
54
61
  }
55
62
  function names(option, aligned) {
56
- const short = option.short ? `-${option.short}, ` : aligned ? " " : "";
63
+ const short = "short" in option && option.short ? `-${option.short}, ` : aligned ? " " : "";
57
64
  const placeholder = option.placeholder ? ` ${option.placeholder}` : "";
58
65
  const repeats = "multiple" in option && option.multiple ? "..." : "";
59
66
  return `${short}--${option.flag}${placeholder}${repeats}`;
60
67
  }
61
68
  function optionList(options) {
62
- const aligned = options.some((option) => option.short);
69
+ const aligned = options.some((option) => "short" in option && option.short);
63
70
  return options.map(
64
71
  (option) => [` ${names(option, aligned)}`, ...wrap(option.description, 8)].join("\n")
65
72
  ).join("\n");
@@ -98,7 +105,7 @@ ${optionList(HELP_FLAG)}
98
105
  `;
99
106
  }
100
107
  var CURRENCY_COMMANDS = COMMANDS.filter(
101
- (spec) => spec.flags.some((flag) => flag.field === "currency")
108
+ (spec) => spec.flags.some((flag) => flag.field === "displayCurrency")
102
109
  ).map((spec) => commandInvocation(spec).slice("fortrip ".length)).join(" and ");
103
110
  function overallHelp() {
104
111
  const rows = [
@@ -118,32 +125,57 @@ ${commandList(rows)}
118
125
  Options:
119
126
  ${optionList(GLOBAL_FLAGS)}
120
127
 
121
- Output is JSON on stdout; errors are JSON on stderr.
122
- Exit codes: 0 ok, 1 error (retry once when stderr has "retryable": true),
128
+ Output is compact text on stdout; --json prints it as one line of JSON.
129
+ Errors go to stderr, as text or, with --json, as JSON.
130
+ Exit codes: 0 ok, 1 error (retry once when the error says it is retryable),
123
131
  2 invalid arguments, 3 not logged in, 4 rate limited.
124
132
 
125
133
  Environment:
126
- FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
127
- FORTRIP_API_URL API base URL (default https://fortrip.ai)
128
- FORTRIP_CURRENCY Default --currency for ${CURRENCY_COMMANDS}
134
+ FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
135
+ FORTRIP_API_URL API base URL (default ${DEFAULT_API_URL})
136
+ FORTRIP_MCP_URL MCP server URL for fortrip mcp (default ${DEFAULT_MCP_URL})
137
+ FORTRIP_DISPLAY_CURRENCY Default --display-currency for ${CURRENCY_COMMANDS}
129
138
  `;
130
139
  }
131
140
 
132
141
  // src/commands.ts
133
- var COMMA_LISTS = /* @__PURE__ */ new Set(["hotelIds", "childrenAges", "boardCodes"]);
134
- var UPPER_CASE = /* @__PURE__ */ new Set(["nationality", "boardCodes", "currency"]);
135
- var TURNS_OFF = /* @__PURE__ */ new Set(["bookableOnly"]);
136
- var POSTS = {
137
- search_hotels: API.search,
138
- get_hotel_rates: API.rates,
139
- book_hotel: API.bookings
142
+ var UPPER_CASE = /* @__PURE__ */ new Set(["displayCurrency", "destination.countryCode"]);
143
+ var FROM_ENV = { displayCurrency: "FORTRIP_DISPLAY_CURRENCY" };
144
+ var ONE_ITEM_LISTS = /* @__PURE__ */ new Set(["items"]);
145
+ var GUEST_FIELD = "items.hotel.rooms.guests";
146
+ var ROOMS_FIELD = "items.hotel.rooms";
147
+ var locale = (input) => input.locale;
148
+ var ROUTES = {
149
+ search_hotels: { method: "POST", path: () => API.hotelSearch },
150
+ get_hotel: {
151
+ method: "GET",
152
+ path: (input) => API.hotel(input.hotelId, { rooms: input.include === "rooms", locale: locale(input) })
153
+ },
154
+ get_hotel_offers: { method: "POST", path: () => API.hotelOffers },
155
+ book_hotel: { method: "POST", path: () => API.orders },
156
+ get_order: {
157
+ method: "GET",
158
+ path: (input) => API.order(input.orderId, { locale: locale(input) })
159
+ },
160
+ list_orders: {
161
+ method: "GET",
162
+ path: (input) => {
163
+ const query = new URLSearchParams();
164
+ for (const name of ["cursor", "limit", "locale"]) {
165
+ if (input[name] !== void 0) query.set(name, String(input[name]));
166
+ }
167
+ const text = query.toString();
168
+ return text ? `${API.orders}?${text}` : API.orders;
169
+ }
170
+ }
140
171
  };
172
+ var PATH_FIELDS = /* @__PURE__ */ new Set(["hotelId", "orderId"]);
141
173
  function parseOptions(spec, argv) {
142
174
  const options = {};
143
175
  for (const option of [...spec.flags, ...spec.options ?? []]) {
144
176
  options[option.flag] = {
145
177
  type: option.type === "boolean" ? "boolean" : "string",
146
- ...option.short ? { short: option.short } : {},
178
+ ..."short" in option && option.short ? { short: option.short } : {},
147
179
  ..."multiple" in option && option.multiple ? { multiple: true } : {}
148
180
  };
149
181
  }
@@ -175,38 +207,83 @@ function parseOptions(spec, argv) {
175
207
  return { values: parsed.values, positionals: parsed.positionals };
176
208
  }
177
209
  function parseCommand(spec, argv, env) {
178
- const post = spec.tool ? POSTS[spec.tool] : void 0;
179
- if (!post && spec.tool !== "get_hotel") {
180
- throw new Error(`${commandInvocation(spec)} sends no request`);
181
- }
210
+ const route = spec.tool ? ROUTES[spec.tool] : void 0;
211
+ if (!route) throw new Error(`${commandInvocation(spec)} sends no request`);
182
212
  const { values, positionals: positionals2 } = parseOptions(spec, argv);
183
213
  const input = baseBody(values.body);
184
- for (const flag of spec.flags) {
185
- const raw = values[flag.flag];
186
- if (raw !== void 0) setField(input, flag.field, readFlag(flag, raw));
214
+ const wanted = spec.positionals ?? [];
215
+ if (positionals2.length !== wanted.length) throw usageError(`Usage: ${usageLine(spec)}`);
216
+ wanted.forEach((positional, index) => {
217
+ if (positional.field) input[positional.field] = positionals2[index];
218
+ });
219
+ setFlags(spec, values, input);
220
+ const fromEnv = /* @__PURE__ */ new Map();
221
+ for (const [field, name] of Object.entries(FROM_ENV)) {
222
+ const fallback = env[name]?.trim().toUpperCase();
223
+ if (fallback && input[field] === void 0 && spec.flags.some((flag) => flag.field === field)) {
224
+ input[field] = fallback;
225
+ fromEnv.set(`/${field}`, name);
226
+ }
187
227
  }
188
- if (spec.tool === "get_hotel") {
189
- const [hotelId, ...extra] = positionals2;
190
- const parsed = Number(hotelId);
191
- if (!hotelId || extra.length || !/^\d+$/.test(hotelId) || !Number.isSafeInteger(parsed)) {
192
- throw usageError(`Usage: ${usageLine(spec)}`);
228
+ check(spec, input, route.method === "GET", fromEnv);
229
+ const path = route.path(input);
230
+ return route.method === "POST" ? { method: "POST", path, body: input } : { method: "GET", path };
231
+ }
232
+ function check(spec, input, isGet, fromEnv) {
233
+ const schema = TOOLS.find((tool) => tool.name === spec.tool)?.input;
234
+ const result = schema?.safeParse(input);
235
+ if (!result || result.success) return;
236
+ const pointer = (path) => {
237
+ const keys = path.map((key) => String(key).replaceAll("~", "~0").replaceAll("/", "~1"));
238
+ const where = isGet && keys[0] !== void 0 ? [PATH_FIELDS.has(keys[0]) ? "path" : "query"] : [];
239
+ return [...where, ...keys].map((key) => `/${key}`).join("");
240
+ };
241
+ const issues = result.error.issues.flatMap(
242
+ (issue) => issue.code === "unrecognized_keys" ? issue.keys.map((key) => ({
243
+ pointer: pointer([...issue.path, key]),
244
+ message: `Unknown field: ${key}`
245
+ })) : [{ pointer: pointer(issue.path), message: issue.message }]
246
+ ).map(({ pointer: pointer2, message }) => {
247
+ const name = fromEnv.get(pointer2);
248
+ return { pointer: pointer2, message: name ? `${name}: ${message}` : message };
249
+ });
250
+ throw new CliError(
251
+ `Not sent: ${commandInvocation(spec)} was given an invalid request.`,
252
+ EXIT.usage,
253
+ { details: { issues } }
254
+ );
255
+ }
256
+ function setFlags(spec, values, input) {
257
+ const given = spec.flags.filter((flag) => values[flag.flag] !== void 0);
258
+ const picked = /* @__PURE__ */ new Map();
259
+ for (const flag of given) {
260
+ for (const [field, value] of Object.entries(flag.sets ?? {})) {
261
+ const earlier = picked.get(field);
262
+ if (earlier && earlier.value !== value) {
263
+ throw usageError(
264
+ `--${earlier.flag} and --${flag.flag} can't be given together: they set ${field} to "${earlier.value}" and "${value}"`
265
+ );
266
+ }
267
+ if (!earlier) picked.set(field, { value, flag: flag.flag });
193
268
  }
194
- return { path: API.hotel(parsed, input.includeRooms === true) };
195
269
  }
196
- if (spec.flags.some((flag) => flag.field === "currency") && input.currency === void 0) {
197
- const fallback = env.FORTRIP_CURRENCY?.trim().toUpperCase();
198
- if (fallback) input.currency = fallback;
270
+ for (const [field, { value }] of picked) {
271
+ const parent = field.split(".").slice(0, -1).join(".");
272
+ const key = field.split(".").at(-1);
273
+ const existing = parent ? getField(input, parent) : input;
274
+ if (isObject(existing) && existing[key] !== void 0 && existing[key] !== value) {
275
+ setField(input, parent, {});
276
+ }
277
+ setField(input, field, value);
199
278
  }
200
- const occupancy = spec.flags.filter(
201
- (flag) => flag.field === "rooms" || flag.field === "adults"
202
- );
203
- const missing = occupancy.filter((flag) => input[flag.field] === void 0);
204
- if (occupancy.length === 2 && missing.length) {
205
- throw usageError(
206
- `${commandInvocation(spec)} 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)`
207
- );
279
+ for (const flag of given) {
280
+ const raw = values[flag.flag];
281
+ if (flag.field === GUEST_FIELD) {
282
+ setField(input, ROOMS_FIELD, withGuests(getField(input, ROOMS_FIELD), parseGuests(raw)));
283
+ } else {
284
+ setField(input, flag.field, readFlag(flag, raw));
285
+ }
208
286
  }
209
- return { body: input, path: post };
210
287
  }
211
288
  function baseBody(raw) {
212
289
  if (typeof raw !== "string") return {};
@@ -220,76 +297,125 @@ function baseBody(raw) {
220
297
  }
221
298
  try {
222
299
  const parsed = JSON.parse(text);
223
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
224
- return parsed;
225
- }
300
+ if (isObject(parsed)) return parsed;
226
301
  } catch {
227
302
  }
228
303
  throw usageError("--body must be a JSON object");
229
304
  }
230
- function setField(body, field, value) {
305
+ function isObject(value) {
306
+ return typeof value === "object" && value !== null && !Array.isArray(value);
307
+ }
308
+ function step(node, part, create) {
309
+ if (ONE_ITEM_LISTS.has(part)) {
310
+ let list = node[part];
311
+ if (!Array.isArray(list)) {
312
+ if (!create) return void 0;
313
+ list = node[part] = [];
314
+ }
315
+ const items = list;
316
+ if (!isObject(items[0])) {
317
+ if (!create) return void 0;
318
+ items[0] = {};
319
+ }
320
+ return items[0];
321
+ }
322
+ if (!isObject(node[part])) {
323
+ if (!create) return void 0;
324
+ node[part] = {};
325
+ }
326
+ return node[part];
327
+ }
328
+ function getField(body, field) {
329
+ let node = body;
231
330
  const path = field.split(".");
232
331
  const last = path.pop();
233
- let node = body;
234
332
  for (const part of path) {
235
- const next = node[part];
236
- node[part] = next && typeof next === "object" && !Array.isArray(next) ? next : {};
237
- node = node[part];
333
+ node = node && step(node, part, false);
238
334
  }
335
+ return node?.[last];
336
+ }
337
+ function setField(body, field, value) {
338
+ const path = field.split(".");
339
+ const last = path.pop();
340
+ let node = body;
341
+ for (const part of path) node = step(node, part, true);
239
342
  node[last] = value;
240
343
  }
241
344
  function readFlag(flag, raw) {
242
- if (flag.type === "boolean") return !TURNS_OFF.has(flag.field);
345
+ if (flag.type === "boolean") return flag.value ?? true;
243
346
  const given = Array.isArray(raw) ? raw : [raw];
244
- if (flag.field === "near") return parseNear(given[0]);
245
- if (flag.field === "guests") return given.map(parseGuest);
246
- const parts = COMMA_LISTS.has(flag.field) ? given.flatMap((value) => value.split(",")).map((part) => part.trim()) : given;
247
- const values = parts.map((part) => {
248
- const text = UPPER_CASE.has(flag.field) ? part.trim().toUpperCase() : part;
249
- if (flag.field === "currency" && text === "") {
250
- throw usageError(`--${flag.flag} must not be empty`);
251
- }
252
- if (flag.type === "number") return num(flag.flag, text);
253
- return flag.type === "id" ? id(flag.flag, text) : text;
347
+ switch (flag.field) {
348
+ case "destination.location":
349
+ return parsePoint(given[0]);
350
+ case "stay.rooms":
351
+ return given.map(parseRoom);
352
+ case "filters.price.maxPerRoomNight":
353
+ return { amount: given[0].trim(), currency: "USD" };
354
+ }
355
+ const parsed = given.map((value) => {
356
+ if (flag.type === "number") return num(flag.flag, value);
357
+ return UPPER_CASE.has(flag.field) ? value.trim().toUpperCase() : value;
254
358
  });
255
- return flag.multiple ? values : values[0];
359
+ return flag.multiple ? parsed : parsed[0];
256
360
  }
257
361
  function num(flag, value) {
258
- const parsed = Number(value);
259
- if (value.trim() === "" || !Number.isFinite(parsed)) {
362
+ const text = value.trim();
363
+ if (!/^[+-]?\d+(\.\d+)?$/.test(text)) {
260
364
  throw usageError(`--${flag} must be a number, got "${value}"`);
261
365
  }
262
- return parsed;
263
- }
264
- function id(flag, value) {
265
- const parsed = Number(value.trim());
266
- if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed)) {
267
- throw usageError(`--${flag} must be a numeric id, got "${value}"`);
268
- }
269
- return parsed;
366
+ return Number(text);
270
367
  }
271
- function parseNear(value) {
368
+ function parsePoint(value) {
272
369
  const parts = value.split(",").map((part) => part.trim());
273
- if (parts.length !== 3 || parts.some((part) => !part)) {
274
- throw usageError("--near must be lat,lng,radiusKm (e.g. 13.75,100.5,5)");
370
+ if (parts.length !== 2 || parts.some((part) => !/^[+-]?\d+(\.\d+)?$/.test(part))) {
371
+ throw usageError(`--point must be lat,lng (e.g. 13.75,100.5), got "${value}"`);
275
372
  }
276
- const [lat, lng, radiusKm] = parts.map((part) => num("near", part));
277
- return { lat, lng, radiusKm };
373
+ const [lat, lng] = parts.map(Number);
374
+ return { lat, lng };
278
375
  }
279
- function parseGuest(value) {
280
- const match = value.trim().match(/^(.+?)(?::(\d+))?$/);
281
- const words = match?.[1]?.trim().split(/\s+/) ?? [];
282
- if (words.length < 2) {
376
+ function parseRoom(value) {
377
+ const match = value.replace(/\s+/g, "").match(/^(\d+)(?::(\d+(?:,\d+)*))?$/);
378
+ if (!match) {
283
379
  throw usageError(
284
- `--guest must be "First Last" or "First Last:roomNo", got "${value}"`
380
+ `--room must be adults or adults:age,age with each child's age (e.g. 2 or 2:6,9), got "${value}"`
285
381
  );
286
382
  }
287
383
  return {
288
- firstName: words.slice(0, -1).join(" "),
289
- lastName: words.at(-1),
290
- roomNo: match?.[2] ? Number(match[2]) : 1
384
+ adults: Number(match[1]),
385
+ childrenAges: match[2] ? match[2].split(",").map(Number) : []
291
386
  };
292
387
  }
388
+ function parseGuests(values) {
389
+ const rooms = /* @__PURE__ */ new Map();
390
+ for (const value of values) {
391
+ const parts = value.split(":").map((part) => part.trim());
392
+ const [room, firstName, lastName] = parts;
393
+ const number = Number(room);
394
+ if (parts.length !== 3 || !/^\d+$/.test(room ?? "")) {
395
+ throw usageError(`--guest must be room:first:last (e.g. 1:John:Smith), got "${value}"`);
396
+ }
397
+ if (number < 1 || number > LIMITS.rooms) {
398
+ throw usageError(`--guest room must be 1 to ${LIMITS.rooms}, got "${value}"`);
399
+ }
400
+ rooms.set(number, [...rooms.get(number) ?? [], { firstName, lastName }]);
401
+ }
402
+ return rooms;
403
+ }
404
+ function withGuests(fromBody, guests) {
405
+ const rooms = Array.isArray(fromBody) ? [...fromBody] : [];
406
+ for (const [number, list] of guests) {
407
+ const room = rooms[number - 1];
408
+ rooms[number - 1] = { ...isObject(room) ? room : {}, guests: list };
409
+ }
410
+ for (let index = 0; index < rooms.length; index++) {
411
+ if (rooms[index] === void 0) {
412
+ throw usageError(
413
+ `--guest rooms are numbered from 1 with none skipped: no guest for room ${index + 1}`
414
+ );
415
+ }
416
+ }
417
+ return rooms;
418
+ }
293
419
 
294
420
  // src/guide.ts
295
421
  function readGuide(topic) {
@@ -303,8 +429,11 @@ function globalFlag(name) {
303
429
  }
304
430
  var HELP_FLAGS = globalFlag("help");
305
431
  var VERSION_FLAGS = globalFlag("version");
432
+ var JSON_FLAG = "--json";
306
433
  var RENAMED = /* @__PURE__ */ new Map([
307
- ["hotel", { group: "hotels", name: "get" }]
434
+ ["hotel", { group: "hotels", name: "get" }],
435
+ ["rates", { group: "hotels", name: "offers" }],
436
+ ["hotels rates", { group: "hotels", name: "offers" }]
308
437
  ]);
309
438
  function findCommand(argv) {
310
439
  const [word, ...afterWord] = argv;
@@ -318,8 +447,9 @@ function findCommand(argv) {
318
447
  (candidate) => candidate.group === group.name && candidate.name === name
319
448
  );
320
449
  if (!spec2) {
450
+ const renamed2 = RENAMED.get(`${group.name} ${name}`);
321
451
  throw usageError(
322
- `Unknown command "${group.name} ${name}". Run \`fortrip ${group.name} --help\`.`
452
+ renamed2 ? `Unknown command "${group.name} ${name}". Did you mean \`${commandInvocation(renamed2)}\`?` : `Unknown command "${group.name} ${name}". Run \`fortrip ${group.name} --help\`.`
323
453
  );
324
454
  }
325
455
  return { spec: spec2, rest };
@@ -377,26 +507,39 @@ async function run(argv) {
377
507
  if (process.env.FORTRIP_TOKEN) {
378
508
  log("Note: FORTRIP_TOKEN is set and takes precedence over this login.");
379
509
  }
380
- return login({ apiUrl, openBrowser: !values["no-browser"], log });
510
+ return account(await login({ apiUrl, openBrowser: !values["no-browser"], log }));
381
511
  }
382
- case "logout":
512
+ case "logout": {
383
513
  parseOptions(spec, rest);
384
- return logout(apiUrl);
514
+ const data2 = await logout(apiUrl);
515
+ return { data: data2, text: `Logged out of ${apiUrl}.` };
516
+ }
385
517
  case "whoami":
386
518
  parseOptions(spec, rest);
387
- return whoami(apiUrl);
519
+ return account(await whoami(apiUrl));
388
520
  case "mcp": {
389
521
  parseOptions(spec, rest);
390
- const { runMcpBridge } = await import("./mcp-bridge-LPQBHWLD.js");
391
- return runMcpBridge(apiUrl);
522
+ const mcpUrl = resolveMcpUrl(apiUrl);
523
+ const { runMcpBridge } = await import("./mcp-bridge-3WLWV2KY.js");
524
+ await runMcpBridge(mcpUrl, apiUrl);
525
+ return void 0;
392
526
  }
393
527
  }
394
528
  }
395
- const { body, path } = parseCommand(spec, rest, process.env);
396
- return call(apiUrl, body ? "POST" : "GET", path, body);
529
+ const { method, path, body } = parseCommand(spec, rest, process.env);
530
+ const data = unwrap(await authorizedRequest(apiUrl, path, { method, body }));
531
+ return { data, text: renderToolResult(spec.tool, data) };
397
532
  }
398
- async function call(apiUrl, method, path, body) {
399
- return unwrap(await authorizedRequest(apiUrl, path, { method, body }));
533
+ function account(data) {
534
+ const { apiUrl, userId, email, name } = data;
535
+ const given = (value) => value ?? "not given";
536
+ return {
537
+ data,
538
+ text: [
539
+ `Signed in to ${apiUrl}.`,
540
+ ...fieldLines({ userId, email: given(email), name: given(name) })
541
+ ].join("\n")
542
+ };
400
543
  }
401
544
  function wantsHelp(args) {
402
545
  for (const arg of args) {
@@ -405,21 +548,33 @@ function wantsHelp(args) {
405
548
  }
406
549
  return false;
407
550
  }
551
+ function takeJson(argv) {
552
+ const end = argv.indexOf("--");
553
+ const before = end === -1 ? argv : argv.slice(0, end);
554
+ const after = end === -1 ? [] : argv.slice(end);
555
+ const kept = before.filter((arg) => arg !== JSON_FLAG);
556
+ return { json: kept.length < before.length, argv: [...kept, ...after] };
557
+ }
408
558
  async function main() {
559
+ const { json, argv } = takeJson(process.argv.slice(2));
409
560
  try {
410
- const result = await run(process.argv.slice(2));
411
- if (result !== void 0) {
412
- const output = result && typeof result === "object" && !Array.isArray(result) ? { ...result, ...notices } : result;
413
- process.stdout.write(`${JSON.stringify(output, null, 2)}
561
+ const answer = await run(argv);
562
+ if (answer !== void 0) {
563
+ const { data, text } = answer;
564
+ if (json) {
565
+ const output = data && typeof data === "object" && !Array.isArray(data) ? { ...data, ...notices } : data;
566
+ process.stdout.write(`${JSON.stringify(output)}
567
+ `);
568
+ } else {
569
+ process.stdout.write(`${[text, ...fieldLines(notices)].join("\n")}
414
570
  `);
571
+ }
415
572
  }
416
573
  return EXIT.ok;
417
574
  } catch (error) {
418
575
  const failure = error instanceof CliError ? error : new CliError(error?.message ?? String(error), EXIT.error);
419
- process.stderr.write(
420
- `${JSON.stringify({ error: failure.message, exitCode: failure.exitCode, ...failure.details, ...notices }, null, 2)}
421
- `
422
- );
576
+ process.stderr.write(`${formatError(failure, json)}
577
+ `);
423
578
  return failure.exitCode;
424
579
  }
425
580
  }