@tks/wayfinder 0.5.0 → 0.6.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/README.md +6 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -0
- package/src/format.ts +6 -0
- package/src/parse.ts +32 -0
- package/src/serpapi.ts +35 -1
- package/src/types.ts +5 -0
package/README.md
CHANGED
|
@@ -70,6 +70,12 @@ Search with filters:
|
|
|
70
70
|
wayfinder flights --from LAX --to SEA --date 2026-04-10 --airline AS --max-stops 0 --max-price 250 --depart-after 06:00 --depart-before 12:00
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Search by cabin:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
wayfinder flights --from JFK --to HND --date 2026-06-15 --cabin premium-economy
|
|
77
|
+
```
|
|
78
|
+
|
|
73
79
|
Exclude basic economy fares:
|
|
74
80
|
|
|
75
81
|
```bash
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -43,6 +43,7 @@ Flights required:
|
|
|
43
43
|
|
|
44
44
|
Flights optional filters:
|
|
45
45
|
--airline <IATA> Airline code, example UA
|
|
46
|
+
--cabin <CABIN> Cabin: economy, premium-economy, preeco, business, first
|
|
46
47
|
--max-stops <0|1|2> Maximum number of stops
|
|
47
48
|
--max-price <USD> Max price in USD
|
|
48
49
|
--depart-after <HH:MM> Start of departure window
|
package/src/format.ts
CHANGED
|
@@ -14,6 +14,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
14
14
|
arrive: option.arrivalTime,
|
|
15
15
|
duration: formatDuration(option.durationMinutes),
|
|
16
16
|
stops: String(option.stops),
|
|
17
|
+
cabin: option.cabin ?? "n/a",
|
|
17
18
|
}));
|
|
18
19
|
|
|
19
20
|
const headers = {
|
|
@@ -23,6 +24,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
23
24
|
arrive: "ARRIVE",
|
|
24
25
|
duration: "DURATION",
|
|
25
26
|
stops: "STOPS",
|
|
27
|
+
cabin: "CABIN",
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
const widths = {
|
|
@@ -32,6 +34,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
32
34
|
arrive: maxWidth(rows, "arrive", headers.arrive),
|
|
33
35
|
duration: maxWidth(rows, "duration", headers.duration),
|
|
34
36
|
stops: maxWidth(rows, "stops", headers.stops),
|
|
37
|
+
cabin: maxWidth(rows, "cabin", headers.cabin),
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
const lines: string[] = [];
|
|
@@ -44,6 +47,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
44
47
|
headers.arrive.padEnd(widths.arrive),
|
|
45
48
|
headers.duration.padEnd(widths.duration),
|
|
46
49
|
headers.stops.padEnd(widths.stops),
|
|
50
|
+
headers.cabin.padEnd(widths.cabin),
|
|
47
51
|
].join(" "),
|
|
48
52
|
);
|
|
49
53
|
|
|
@@ -55,6 +59,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
55
59
|
"-".repeat(widths.arrive),
|
|
56
60
|
"-".repeat(widths.duration),
|
|
57
61
|
"-".repeat(widths.stops),
|
|
62
|
+
"-".repeat(widths.cabin),
|
|
58
63
|
].join(" "),
|
|
59
64
|
);
|
|
60
65
|
|
|
@@ -67,6 +72,7 @@ export function renderFlightTable(options: FlightOption[]): string {
|
|
|
67
72
|
row.arrive.padEnd(widths.arrive),
|
|
68
73
|
row.duration.padEnd(widths.duration),
|
|
69
74
|
row.stops.padEnd(widths.stops),
|
|
75
|
+
row.cabin.padEnd(widths.cabin),
|
|
70
76
|
].join(" "),
|
|
71
77
|
);
|
|
72
78
|
}
|
package/src/parse.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CliError } from "./errors";
|
|
|
2
2
|
import {
|
|
3
3
|
ExitCode,
|
|
4
4
|
FlightBookingQuery,
|
|
5
|
+
CabinClass,
|
|
5
6
|
FlightsQuery,
|
|
6
7
|
HotelClass,
|
|
7
8
|
HotelQuery,
|
|
@@ -16,6 +17,7 @@ interface FlightRawOptions {
|
|
|
16
17
|
to?: string;
|
|
17
18
|
dates: string[];
|
|
18
19
|
airline?: string;
|
|
20
|
+
cabin?: string;
|
|
19
21
|
maxStops?: string;
|
|
20
22
|
maxPrice?: string;
|
|
21
23
|
departAfter?: string;
|
|
@@ -140,6 +142,9 @@ function parseFlightsArgs(args: string[]): ParsedArgs {
|
|
|
140
142
|
case "--airline":
|
|
141
143
|
raw.airline = value;
|
|
142
144
|
break;
|
|
145
|
+
case "--cabin":
|
|
146
|
+
raw.cabin = value;
|
|
147
|
+
break;
|
|
143
148
|
case "--max-stops":
|
|
144
149
|
raw.maxStops = value;
|
|
145
150
|
break;
|
|
@@ -428,9 +433,14 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
|
|
|
428
433
|
}
|
|
429
434
|
|
|
430
435
|
const airlineCode = raw.airline ? normalizeAirlineCode(raw.airline) : undefined;
|
|
436
|
+
const cabin = raw.cabin ? normalizeCabin(raw.cabin) : undefined;
|
|
431
437
|
const maxStops = raw.maxStops ? normalizeMaxStops(raw.maxStops) : undefined;
|
|
432
438
|
const maxPrice = raw.maxPrice ? normalizePrice(raw.maxPrice, "--max-price") : undefined;
|
|
433
439
|
|
|
440
|
+
if (raw.excludeBasic && cabin && cabin !== "economy") {
|
|
441
|
+
throw new CliError("--exclude-basic can only be used with --cabin economy", ExitCode.InvalidInput);
|
|
442
|
+
}
|
|
443
|
+
|
|
434
444
|
const hasDepartAfter = typeof raw.departAfter === "string";
|
|
435
445
|
const hasDepartBefore = typeof raw.departBefore === "string";
|
|
436
446
|
|
|
@@ -460,6 +470,7 @@ function buildFlightQuery(raw: FlightRawOptions): FlightsQuery {
|
|
|
460
470
|
origin,
|
|
461
471
|
destination,
|
|
462
472
|
airlineCode,
|
|
473
|
+
cabin,
|
|
463
474
|
maxStops,
|
|
464
475
|
maxPrice,
|
|
465
476
|
departureAfterMinutes,
|
|
@@ -639,6 +650,27 @@ function normalizeAirlineCode(value: string): string {
|
|
|
639
650
|
return upper;
|
|
640
651
|
}
|
|
641
652
|
|
|
653
|
+
function normalizeCabin(value: string): CabinClass {
|
|
654
|
+
const normalized = value.trim().toLowerCase();
|
|
655
|
+
if (normalized === "preeco") {
|
|
656
|
+
return "premium-economy";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (
|
|
660
|
+
normalized === "economy" ||
|
|
661
|
+
normalized === "premium-economy" ||
|
|
662
|
+
normalized === "business" ||
|
|
663
|
+
normalized === "first"
|
|
664
|
+
) {
|
|
665
|
+
return normalized;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw new CliError(
|
|
669
|
+
"--cabin must be one of: economy, premium-economy, preeco, business, first",
|
|
670
|
+
ExitCode.InvalidInput,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
642
674
|
function normalizeLocation(value: string): string {
|
|
643
675
|
const trimmed = value.trim();
|
|
644
676
|
if (trimmed.length === 0) {
|
package/src/serpapi.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "./errors";
|
|
2
2
|
import {
|
|
3
|
+
CabinClass,
|
|
3
4
|
ExitCode,
|
|
4
5
|
FlightDateResult,
|
|
5
6
|
FlightBookingQuery,
|
|
@@ -22,6 +23,7 @@ interface SerpApiAirport {
|
|
|
22
23
|
|
|
23
24
|
interface SerpApiSegment {
|
|
24
25
|
airline?: string;
|
|
26
|
+
travel_class?: string;
|
|
25
27
|
duration?: number;
|
|
26
28
|
departure_airport?: SerpApiAirport;
|
|
27
29
|
arrival_airport?: SerpApiAirport;
|
|
@@ -148,6 +150,7 @@ export async function searchFlightsMultiDate(
|
|
|
148
150
|
destination: query.destination,
|
|
149
151
|
departureDate: date,
|
|
150
152
|
airlineCode: query.airlineCode,
|
|
153
|
+
cabin: query.cabin,
|
|
151
154
|
maxStops: query.maxStops,
|
|
152
155
|
maxPrice: query.maxPrice,
|
|
153
156
|
departureAfterMinutes: query.departureAfterMinutes,
|
|
@@ -291,6 +294,9 @@ function shapeItinerary(itinerary: SerpApiItinerary): FlightOption | null {
|
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
const uniqueAirlines = [...new Set(segments.map((segment) => segment.airline).filter(Boolean))];
|
|
297
|
+
const cabinClasses = [
|
|
298
|
+
...new Set(segments.map((segment) => segment.travel_class).filter(isNonEmptyString)),
|
|
299
|
+
];
|
|
294
300
|
|
|
295
301
|
const option: FlightOption = {
|
|
296
302
|
price: itinerary.price as number,
|
|
@@ -301,6 +307,10 @@ function shapeItinerary(itinerary: SerpApiItinerary): FlightOption | null {
|
|
|
301
307
|
stops: Math.max(0, segments.length - 1),
|
|
302
308
|
};
|
|
303
309
|
|
|
310
|
+
if (cabinClasses.length > 0) {
|
|
311
|
+
option.cabin = cabinClasses.join(", ");
|
|
312
|
+
}
|
|
313
|
+
|
|
304
314
|
if (typeof itinerary.booking_token === "string" && itinerary.booking_token.trim() !== "") {
|
|
305
315
|
option.bookingToken = itinerary.booking_token.trim();
|
|
306
316
|
}
|
|
@@ -351,6 +361,10 @@ function inferDurationMinutes(itinerary: SerpApiItinerary, segments: SerpApiSegm
|
|
|
351
361
|
return segmentDuration;
|
|
352
362
|
}
|
|
353
363
|
|
|
364
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
365
|
+
return typeof value === "string" && value.trim() !== "";
|
|
366
|
+
}
|
|
367
|
+
|
|
354
368
|
function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
|
|
355
369
|
const url = new URL("https://serpapi.com/search.json");
|
|
356
370
|
|
|
@@ -367,6 +381,10 @@ function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
|
|
|
367
381
|
url.searchParams.set("include_airlines", query.airlineCode);
|
|
368
382
|
}
|
|
369
383
|
|
|
384
|
+
if (query.cabin) {
|
|
385
|
+
url.searchParams.set("travel_class", toSerpApiTravelClass(query.cabin));
|
|
386
|
+
}
|
|
387
|
+
|
|
370
388
|
if (typeof query.maxStops === "number") {
|
|
371
389
|
url.searchParams.set("stops", toSerpApiStopsFilter(query.maxStops));
|
|
372
390
|
}
|
|
@@ -390,7 +408,7 @@ function buildFlightRequestUrl(query: FlightQuery, apiKey: string): string {
|
|
|
390
408
|
|
|
391
409
|
if (query.excludeBasic) {
|
|
392
410
|
url.searchParams.set("exclude_basic", "true");
|
|
393
|
-
url.searchParams.set("travel_class", "
|
|
411
|
+
url.searchParams.set("travel_class", toSerpApiTravelClass("economy"));
|
|
394
412
|
url.searchParams.set("gl", "us");
|
|
395
413
|
}
|
|
396
414
|
|
|
@@ -479,6 +497,22 @@ function toSerpApiStopsFilter(maxStops: number): string {
|
|
|
479
497
|
return "3";
|
|
480
498
|
}
|
|
481
499
|
|
|
500
|
+
function toSerpApiTravelClass(cabin: CabinClass): string {
|
|
501
|
+
if (cabin === "economy") {
|
|
502
|
+
return "1";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (cabin === "premium-economy") {
|
|
506
|
+
return "2";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (cabin === "business") {
|
|
510
|
+
return "3";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return "4";
|
|
514
|
+
}
|
|
515
|
+
|
|
482
516
|
function toSerpApiRating(rating: 3.5 | 4 | 4.5 | 5): string {
|
|
483
517
|
if (rating === 3.5) {
|
|
484
518
|
return "7";
|
package/src/types.ts
CHANGED
|
@@ -9,11 +9,14 @@ export const ExitCode = {
|
|
|
9
9
|
|
|
10
10
|
export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];
|
|
11
11
|
|
|
12
|
+
export type CabinClass = "economy" | "premium-economy" | "business" | "first";
|
|
13
|
+
|
|
12
14
|
export interface FlightQuery {
|
|
13
15
|
origin: string;
|
|
14
16
|
destination: string;
|
|
15
17
|
departureDate: string;
|
|
16
18
|
airlineCode?: string;
|
|
19
|
+
cabin?: CabinClass;
|
|
17
20
|
maxStops?: number;
|
|
18
21
|
maxPrice?: number;
|
|
19
22
|
departureAfterMinutes?: number;
|
|
@@ -26,6 +29,7 @@ export interface FlightMultiDateQuery {
|
|
|
26
29
|
destination: string;
|
|
27
30
|
departureDates: string[];
|
|
28
31
|
airlineCode?: string;
|
|
32
|
+
cabin?: CabinClass;
|
|
29
33
|
maxStops?: number;
|
|
30
34
|
maxPrice?: number;
|
|
31
35
|
departureAfterMinutes?: number;
|
|
@@ -123,6 +127,7 @@ export interface FlightOption {
|
|
|
123
127
|
arrivalTime: string;
|
|
124
128
|
durationMinutes: number;
|
|
125
129
|
stops: number;
|
|
130
|
+
cabin?: string;
|
|
126
131
|
bookingToken?: string;
|
|
127
132
|
}
|
|
128
133
|
|