itamatrix 0.1.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.
Files changed (78) hide show
  1. package/DESIGN.md +247 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/dist/browser/batch.d.ts +8 -0
  5. package/dist/browser/batch.js +87 -0
  6. package/dist/browser/batch.js.map +1 -0
  7. package/dist/browser/batch.test.d.ts +1 -0
  8. package/dist/browser/batch.test.js +38 -0
  9. package/dist/browser/batch.test.js.map +1 -0
  10. package/dist/browser/forms.d.ts +26 -0
  11. package/dist/browser/forms.js +233 -0
  12. package/dist/browser/forms.js.map +1 -0
  13. package/dist/browser/session.d.ts +20 -0
  14. package/dist/browser/session.js +126 -0
  15. package/dist/browser/session.js.map +1 -0
  16. package/dist/cache.d.ts +21 -0
  17. package/dist/cache.js +71 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/cache.test.d.ts +1 -0
  20. package/dist/cache.test.js +79 -0
  21. package/dist/cache.test.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.js +154 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands/calendar.d.ts +19 -0
  26. package/dist/commands/calendar.js +47 -0
  27. package/dist/commands/calendar.js.map +1 -0
  28. package/dist/commands/calendar.test.d.ts +1 -0
  29. package/dist/commands/calendar.test.js +58 -0
  30. package/dist/commands/calendar.test.js.map +1 -0
  31. package/dist/commands/multicity.d.ts +17 -0
  32. package/dist/commands/multicity.js +50 -0
  33. package/dist/commands/multicity.js.map +1 -0
  34. package/dist/commands/multicity.test.d.ts +1 -0
  35. package/dist/commands/multicity.test.js +54 -0
  36. package/dist/commands/multicity.test.js.map +1 -0
  37. package/dist/commands/search.d.ts +20 -0
  38. package/dist/commands/search.js +43 -0
  39. package/dist/commands/search.js.map +1 -0
  40. package/dist/commands/search.test.d.ts +1 -0
  41. package/dist/commands/search.test.js +124 -0
  42. package/dist/commands/search.test.js.map +1 -0
  43. package/dist/commands/shared.d.ts +44 -0
  44. package/dist/commands/shared.js +77 -0
  45. package/dist/commands/shared.js.map +1 -0
  46. package/dist/model/spec.d.ts +63 -0
  47. package/dist/model/spec.js +29 -0
  48. package/dist/model/spec.js.map +1 -0
  49. package/dist/model/spec.test.d.ts +1 -0
  50. package/dist/model/spec.test.js +55 -0
  51. package/dist/model/spec.test.js.map +1 -0
  52. package/dist/model/types.d.ts +22080 -0
  53. package/dist/model/types.js +100 -0
  54. package/dist/model/types.js.map +1 -0
  55. package/dist/model/types.test.d.ts +1 -0
  56. package/dist/model/types.test.js +35 -0
  57. package/dist/model/types.test.js.map +1 -0
  58. package/dist/render/calendar.d.ts +21 -0
  59. package/dist/render/calendar.js +89 -0
  60. package/dist/render/calendar.js.map +1 -0
  61. package/dist/render/calendar.render.test.d.ts +1 -0
  62. package/dist/render/calendar.render.test.js +66 -0
  63. package/dist/render/calendar.render.test.js.map +1 -0
  64. package/dist/render/json.d.ts +2 -0
  65. package/dist/render/json.js +4 -0
  66. package/dist/render/json.js.map +1 -0
  67. package/dist/render/normalize.d.ts +32 -0
  68. package/dist/render/normalize.js +44 -0
  69. package/dist/render/normalize.js.map +1 -0
  70. package/dist/render/render.test.d.ts +1 -0
  71. package/dist/render/render.test.js +129 -0
  72. package/dist/render/render.test.js.map +1 -0
  73. package/dist/render/table.d.ts +2 -0
  74. package/dist/render/table.js +49 -0
  75. package/dist/render/table.js.map +1 -0
  76. package/docs/ROUTING_CODES.md +137 -0
  77. package/package.json +68 -0
  78. package/skills/itamatrix/SKILL.md +173 -0
package/dist/cli.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { Command, InvalidArgumentError } from "commander";
4
+ import { runSearchCommand, } from "./commands/search.js";
5
+ import { runMultiCityCommand } from "./commands/multicity.js";
6
+ import { runCalendarCommand } from "./commands/calendar.js";
7
+ import { carriersToRouting } from "./commands/shared.js";
8
+ function collect(value, prev) {
9
+ return [...prev, value];
10
+ }
11
+ /** Commander parser that accepts only whole integers (rejects "1.5", "2foo"). */
12
+ function intArg(flag) {
13
+ return (value) => {
14
+ if (!/^-?\d+$/.test(value.trim())) {
15
+ throw new InvalidArgumentError(`${flag} must be an integer, got "${value}"`);
16
+ }
17
+ return parseInt(value, 10);
18
+ };
19
+ }
20
+ function run(action) {
21
+ return action()
22
+ .then((out) => {
23
+ process.stdout.write(out + "\n");
24
+ })
25
+ .catch((err) => {
26
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
27
+ process.exitCode = 1;
28
+ });
29
+ }
30
+ function resolveFormat(json, table) {
31
+ if (json)
32
+ return "json";
33
+ if (table)
34
+ return "table";
35
+ // Auto-detect: TTY → table, piped → json.
36
+ return process.stdout.isTTY ? "table" : "json";
37
+ }
38
+ export const program = new Command();
39
+ program
40
+ .name("itamatrix")
41
+ .description("CLI for ITA Matrix flight search")
42
+ .version("0.1.0")
43
+ .option("--json", "force JSON output")
44
+ .option("--table", "force table output")
45
+ .option("--no-cache", "bypass the result cache (always query live)")
46
+ .option("--cache-ttl <minutes>", "max age of a cached result in minutes (default 60)", intArg("--cache-ttl"));
47
+ program
48
+ .command("search")
49
+ .description("Search one-way or round-trip flights")
50
+ .argument("<origin>", "origin airport/city code")
51
+ .argument("<dest>", "destination airport/city code")
52
+ .requiredOption("--depart <date>", "departure date (YYYY-MM-DD)")
53
+ .option("--return <date>", "return date (YYYY-MM-DD) → round-trip")
54
+ .option("--one-way", "force one-way (ignore --return)")
55
+ .option("--adults <n>", "number of adults", intArg("--adults"), 1)
56
+ .option("--limit <n>", "max results, 1-25 (one Matrix page)", intArg("--limit"), 25)
57
+ .option("--cabin <cabin>", "cheapest | premium-economy | business | first")
58
+ .option("--stops <stops>", "any | none | 1 | 2")
59
+ .option("--extra-stops <stops>", "any | none | 1 | 2")
60
+ .option("--carriers <list>", "comma-separated carriers, e.g. UA,AA (sugar for --routing)")
61
+ .option("--routing <codes>", "ITA routing codes (path) — see docs/ROUTING_CODES.md")
62
+ .option("--ext <codes>", "ITA extension codes (faring/filters) — see docs/ROUTING_CODES.md")
63
+ .option("--headful", "show the browser window (debug)")
64
+ .action(async (origin, dest, cmdOpts, command) => {
65
+ const globals = command.parent.opts();
66
+ const opts = {
67
+ depart: cmdOpts.depart,
68
+ return: cmdOpts.return,
69
+ oneWay: cmdOpts.oneWay,
70
+ adults: cmdOpts.adults,
71
+ limit: cmdOpts.limit,
72
+ cabin: cmdOpts.cabin,
73
+ stops: cmdOpts.stops,
74
+ extraStops: cmdOpts.extraStops,
75
+ carriers: cmdOpts.carriers,
76
+ routing: cmdOpts.routing,
77
+ ext: cmdOpts.ext,
78
+ headful: cmdOpts.headful,
79
+ format: resolveFormat(globals.json, globals.table),
80
+ cache: globals.cache,
81
+ cacheTtlMinutes: globals.cacheTtl,
82
+ };
83
+ await run(() => runSearchCommand(origin, dest, opts));
84
+ });
85
+ program
86
+ .command("multicity")
87
+ .description("Search a multi-city itinerary (N legs)")
88
+ .requiredOption("--leg <ORIGIN:DEST:DATE>", "a leg, e.g. JFK:NRT:2026-08-10 (repeatable, >= 2)", collect, [])
89
+ .option("--adults <n>", "number of adults", intArg("--adults"), 1)
90
+ .option("--limit <n>", "max results, 1-25 (one Matrix page)", intArg("--limit"), 25)
91
+ .option("--cabin <cabin>", "cheapest | premium-economy | business | first")
92
+ .option("--stops <stops>", "any | none | 1 | 2")
93
+ .option("--extra-stops <stops>", "any | none | 1 | 2")
94
+ .option("--carriers <list>", "comma-separated carriers, e.g. UA,AA (sugar for --routing)")
95
+ .option("--routing <codes>", "ITA routing codes applied to every leg")
96
+ .option("--ext <codes>", "ITA extension codes applied to every leg")
97
+ .option("--headful", "show the browser window (debug)")
98
+ .action(async (cmdOpts, command) => {
99
+ const globals = command.parent.opts();
100
+ await run(() => runMultiCityCommand({
101
+ legs: cmdOpts.leg,
102
+ adults: cmdOpts.adults,
103
+ limit: cmdOpts.limit,
104
+ cabin: cmdOpts.cabin,
105
+ stops: cmdOpts.stops,
106
+ extraStops: cmdOpts.extraStops,
107
+ routing: cmdOpts.routing ?? carriersToRouting(cmdOpts.carriers),
108
+ ext: cmdOpts.ext,
109
+ format: resolveFormat(globals.json, globals.table),
110
+ headful: cmdOpts.headful,
111
+ cache: globals.cache,
112
+ cacheTtlMinutes: globals.cacheTtl,
113
+ }));
114
+ });
115
+ program
116
+ .command("calendar")
117
+ .description("Price calendar: lowest fare per departure date over a range")
118
+ .argument("<origin>", "origin airport/city code")
119
+ .argument("<dest>", "destination airport/city code")
120
+ .requiredOption("--depart-range <start:end>", "YYYY-MM-DD:YYYY-MM-DD")
121
+ .option("--trip-length <nights>", "round-trip nights; omit for one-way", intArg("--trip-length"))
122
+ .option("--adults <n>", "number of adults", intArg("--adults"), 1)
123
+ .option("--limit <n>", "show only the N cheapest dates", intArg("--limit"), 25)
124
+ .option("--cabin <cabin>", "cheapest | premium-economy | business | first")
125
+ .option("--stops <stops>", "any | none | 1 | 2")
126
+ .option("--extra-stops <stops>", "any | none | 1 | 2")
127
+ .option("--carriers <list>", "comma-separated carriers, e.g. UA,AA (sugar for --routing)")
128
+ .option("--routing <codes>", "ITA routing codes")
129
+ .option("--ext <codes>", "ITA extension codes")
130
+ .option("--headful", "show the browser window (debug)")
131
+ .action(async (origin, dest, cmdOpts, command) => {
132
+ const globals = command.parent.opts();
133
+ await run(() => runCalendarCommand(origin, dest, {
134
+ departRange: cmdOpts.departRange,
135
+ tripLength: cmdOpts.tripLength,
136
+ adults: cmdOpts.adults,
137
+ limit: cmdOpts.limit,
138
+ cabin: cmdOpts.cabin,
139
+ stops: cmdOpts.stops,
140
+ extraStops: cmdOpts.extraStops,
141
+ routing: cmdOpts.routing ?? carriersToRouting(cmdOpts.carriers),
142
+ ext: cmdOpts.ext,
143
+ format: resolveFormat(globals.json, globals.table),
144
+ headful: cmdOpts.headful,
145
+ cache: globals.cache,
146
+ cacheTtlMinutes: globals.cacheTtl,
147
+ }));
148
+ });
149
+ // Parse only when run as the CLI entrypoint, so tools can import `program` to
150
+ // introspect commands/options (see scripts/gen-skill.ts) without triggering a run.
151
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
152
+ program.parseAsync(process.argv);
153
+ }
154
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type CacheControlOptions, type OutputFormat } from "./shared.js";
2
+ export interface CalendarCommandOptions extends CacheControlOptions {
3
+ departRange: string;
4
+ tripLength?: number;
5
+ adults: number;
6
+ limit: number;
7
+ cabin?: string;
8
+ stops?: string;
9
+ extraStops?: string;
10
+ routing?: string;
11
+ ext?: string;
12
+ format: OutputFormat;
13
+ headful?: boolean;
14
+ }
15
+ export declare function runCalendarCommand(origin: string, dest: string, opts: CalendarCommandOptions): Promise<string>;
16
+ export declare function parseDepartRange(range: string): {
17
+ from: string;
18
+ to: string;
19
+ };
@@ -0,0 +1,47 @@
1
+ import { runCalendar } from "../browser/session.js";
2
+ import { withCache } from "../cache.js";
3
+ import { normalizeCalendar, renderCalendarJson, renderCalendarTable, } from "../render/calendar.js";
4
+ import { requireIsoDate, resolveCacheOptions, validateTripControls, } from "./shared.js";
5
+ export async function runCalendarCommand(origin, dest, opts) {
6
+ const { from, to } = parseDepartRange(opts.departRange);
7
+ validate(origin, dest, opts, from, to);
8
+ const spec = {
9
+ origin: origin.toUpperCase(),
10
+ dest: dest.toUpperCase(),
11
+ departFrom: from,
12
+ departTo: to,
13
+ tripLength: opts.tripLength,
14
+ adults: opts.adults,
15
+ limit: opts.limit,
16
+ cabin: opts.cabin,
17
+ stops: opts.stops,
18
+ extraStops: opts.extraStops,
19
+ routing: opts.routing,
20
+ ext: opts.ext,
21
+ };
22
+ const response = await withCache("calendar", spec, resolveCacheOptions(opts), () => runCalendar(spec, { headful: opts.headful }));
23
+ const cal = normalizeCalendar(response, opts.limit);
24
+ return opts.format === "json" ? renderCalendarJson(cal) : renderCalendarTable(cal);
25
+ }
26
+ export function parseDepartRange(range) {
27
+ const parts = (range ?? "").split(":");
28
+ if (parts.length !== 2) {
29
+ throw new Error(`--depart-range must be START:END (YYYY-MM-DD:YYYY-MM-DD), got "${range}"`);
30
+ }
31
+ const from = parts[0].trim();
32
+ const to = parts[1].trim();
33
+ requireIsoDate(from, "--depart-range start");
34
+ requireIsoDate(to, "--depart-range end");
35
+ return { from, to };
36
+ }
37
+ function validate(origin, dest, opts, from, to) {
38
+ if (!origin || !dest)
39
+ throw new Error("origin and destination are required");
40
+ if (from > to)
41
+ throw new Error("--depart-range start must be on or before end");
42
+ if (opts.tripLength != null && (!Number.isInteger(opts.tripLength) || opts.tripLength < 0)) {
43
+ throw new Error("--trip-length must be an integer >= 0");
44
+ }
45
+ validateTripControls(opts);
46
+ }
47
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/commands/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { parseDepartRange, runCalendarCommand, } from "./calendar.js";
3
+ const captured = [];
4
+ vi.mock("../browser/session.js", () => ({
5
+ runCalendar: (spec) => {
6
+ captured.push(spec);
7
+ return Promise.resolve({ calendarSliceList: { days: [] } });
8
+ },
9
+ }));
10
+ const opts = (over = {}) => ({
11
+ departRange: "2026-08-01:2026-08-31",
12
+ tripLength: 7,
13
+ adults: 1,
14
+ limit: 25,
15
+ format: "json",
16
+ cache: false,
17
+ ...over,
18
+ });
19
+ describe("parseDepartRange", () => {
20
+ it("splits START:END into trimmed dates", () => {
21
+ expect(parseDepartRange("2026-08-01:2026-08-31")).toEqual({
22
+ from: "2026-08-01",
23
+ to: "2026-08-31",
24
+ });
25
+ });
26
+ it("rejects a range without exactly two parts", () => {
27
+ expect(() => parseDepartRange("2026-08-01")).toThrow(/START:END/);
28
+ });
29
+ it("rejects a malformed date in the range", () => {
30
+ expect(() => parseDepartRange("2026/08/01:2026-08-31")).toThrow(/must be a valid date/);
31
+ });
32
+ });
33
+ describe("runCalendarCommand validation/wiring", () => {
34
+ it("builds a calendar spec and uppercases airports", async () => {
35
+ captured.length = 0;
36
+ await runCalendarCommand("bos", "lax", opts({ tripLength: 5 }));
37
+ expect(captured[0]).toMatchObject({
38
+ origin: "BOS",
39
+ dest: "LAX",
40
+ departFrom: "2026-08-01",
41
+ departTo: "2026-08-31",
42
+ tripLength: 5,
43
+ });
44
+ });
45
+ it("rejects a start after the end", async () => {
46
+ await expect(runCalendarCommand("BOS", "LAX", opts({ departRange: "2026-08-31:2026-08-01" }))).rejects.toThrow(/start must be on or before end/);
47
+ });
48
+ it("rejects a negative --trip-length", async () => {
49
+ await expect(runCalendarCommand("BOS", "LAX", opts({ tripLength: -1 }))).rejects.toThrow(/--trip-length must be an integer >= 0/);
50
+ });
51
+ it("rejects a non-numeric --trip-length (NaN)", async () => {
52
+ await expect(runCalendarCommand("BOS", "LAX", opts({ tripLength: NaN }))).rejects.toThrow(/--trip-length must be an integer >= 0/);
53
+ });
54
+ it("rejects an invalid --limit", async () => {
55
+ await expect(runCalendarCommand("BOS", "LAX", opts({ limit: 0 }))).rejects.toThrow(/--limit must be an integer >= 1/);
56
+ });
57
+ });
58
+ //# sourceMappingURL=calendar.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.test.js","sourceRoot":"","sources":["../../src/commands/calendar.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type Slice } from "../model/spec.js";
2
+ import { type CacheControlOptions, type OutputFormat } from "./shared.js";
3
+ export interface MultiCityCommandOptions extends CacheControlOptions {
4
+ legs: string[];
5
+ adults: number;
6
+ limit: number;
7
+ cabin?: string;
8
+ stops?: string;
9
+ extraStops?: string;
10
+ routing?: string;
11
+ ext?: string;
12
+ format: OutputFormat;
13
+ headful?: boolean;
14
+ }
15
+ export declare function runMultiCityCommand(opts: MultiCityCommandOptions): Promise<string>;
16
+ /** Parses `--leg ORIGIN:DEST:DATE` flags into slices; `--routing`/`--ext` apply to all. */
17
+ export declare function parseLegs(opts: MultiCityCommandOptions): Slice[];
@@ -0,0 +1,50 @@
1
+ import { runMultiCity } from "../browser/session.js";
2
+ import { withCache } from "../cache.js";
3
+ import { normalize } from "../render/normalize.js";
4
+ import { renderJson } from "../render/json.js";
5
+ import { renderTable } from "../render/table.js";
6
+ import { requireIsoDate, requirePageLimit, resolveCacheOptions, validateTripControls, } from "./shared.js";
7
+ export async function runMultiCityCommand(opts) {
8
+ const slices = parseLegs(opts);
9
+ validateTripControls(opts);
10
+ requirePageLimit(opts.limit);
11
+ const spec = {
12
+ slices,
13
+ adults: opts.adults,
14
+ limit: opts.limit,
15
+ cabin: opts.cabin,
16
+ stops: opts.stops,
17
+ extraStops: opts.extraStops,
18
+ };
19
+ const response = await withCache("multicity", spec, resolveCacheOptions(opts), () => runMultiCity(spec, { headful: opts.headful }));
20
+ const result = normalize(response, opts.limit);
21
+ return opts.format === "json" ? renderJson(result) : renderTable(result);
22
+ }
23
+ /** Parses `--leg ORIGIN:DEST:DATE` flags into slices; `--routing`/`--ext` apply to all. */
24
+ export function parseLegs(opts) {
25
+ if (!opts.legs || opts.legs.length < 2) {
26
+ throw new Error("multicity needs at least 2 --leg ORIGIN:DEST:DATE values");
27
+ }
28
+ return opts.legs.map((raw, i) => toSlice(raw, i, opts));
29
+ }
30
+ function toSlice(raw, index, opts) {
31
+ const parts = raw.split(":");
32
+ if (parts.length !== 3) {
33
+ throw new Error(`--leg #${index + 1} must be ORIGIN:DEST:YYYY-MM-DD, got "${raw}"`);
34
+ }
35
+ const origin = parts[0].trim();
36
+ const dest = parts[1].trim();
37
+ const date = parts[2].trim();
38
+ if (!origin || !dest) {
39
+ throw new Error(`--leg #${index + 1} is missing an origin or destination`);
40
+ }
41
+ requireIsoDate(date, `--leg #${index + 1} date`);
42
+ return {
43
+ origin: origin.toUpperCase(),
44
+ dest: dest.toUpperCase(),
45
+ departDate: date,
46
+ routing: opts.routing,
47
+ ext: opts.ext,
48
+ };
49
+ }
50
+ //# sourceMappingURL=multicity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multicity.js","sourceRoot":"","sources":["../../src/commands/multicity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { parseLegs, runMultiCityCommand, } from "./multicity.js";
3
+ const captured = [];
4
+ vi.mock("../browser/session.js", () => ({
5
+ runMultiCity: (spec) => {
6
+ captured.push(spec);
7
+ return Promise.resolve({ solutionList: { solutions: [] } });
8
+ },
9
+ }));
10
+ const opts = (over = {}) => ({
11
+ legs: ["JFK:NRT:2026-08-10", "NRT:SIN:2026-08-15"],
12
+ adults: 1,
13
+ limit: 25,
14
+ format: "json",
15
+ cache: false,
16
+ ...over,
17
+ });
18
+ describe("parseLegs", () => {
19
+ it("parses and uppercases ORIGIN:DEST:DATE legs", () => {
20
+ const slices = parseLegs(opts({ legs: ["jfk:nrt:2026-08-10", "nrt:sin:2026-08-15"] }));
21
+ expect(slices).toEqual([
22
+ { origin: "JFK", dest: "NRT", departDate: "2026-08-10", routing: undefined, ext: undefined },
23
+ { origin: "NRT", dest: "SIN", departDate: "2026-08-15", routing: undefined, ext: undefined },
24
+ ]);
25
+ });
26
+ it("applies global --routing/--ext to every leg", () => {
27
+ const slices = parseLegs(opts({ routing: "UA+", ext: "-REDEYES" }));
28
+ expect(slices.every((s) => s.routing === "UA+" && s.ext === "-REDEYES")).toBe(true);
29
+ });
30
+ it("rejects fewer than 2 legs", () => {
31
+ expect(() => parseLegs(opts({ legs: ["JFK:NRT:2026-08-10"] }))).toThrow(/at least 2/);
32
+ });
33
+ it("rejects a leg without three colon-separated parts", () => {
34
+ expect(() => parseLegs(opts({ legs: ["JFK-NRT", "NRT:SIN:2026-08-15"] }))).toThrow(/ORIGIN:DEST/);
35
+ });
36
+ it("rejects a leg with a malformed date", () => {
37
+ expect(() => parseLegs(opts({ legs: ["JFK:NRT:08/10", "NRT:SIN:2026-08-15"] }))).toThrow(/must be a valid date/);
38
+ });
39
+ });
40
+ describe("runMultiCityCommand spec wiring", () => {
41
+ it("builds an N-slice spec with shared controls", async () => {
42
+ captured.length = 0;
43
+ await runMultiCityCommand(opts({ cabin: "business", stops: "1" }));
44
+ expect(captured[0]).toMatchObject({
45
+ cabin: "business",
46
+ stops: "1",
47
+ slices: [{ origin: "JFK" }, { origin: "NRT" }],
48
+ });
49
+ });
50
+ it("rejects an unknown --cabin before launching the browser", async () => {
51
+ await expect(runMultiCityCommand(opts({ cabin: "economy" }))).rejects.toThrow(/--cabin must be one of/);
52
+ });
53
+ });
54
+ //# sourceMappingURL=multicity.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multicity.test.js","sourceRoot":"","sources":["../../src/commands/multicity.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type CacheControlOptions, type OutputFormat } from "./shared.js";
2
+ export type { OutputFormat };
3
+ export interface SearchCommandOptions extends CacheControlOptions {
4
+ depart: string;
5
+ return?: string;
6
+ oneWay?: boolean;
7
+ adults: number;
8
+ limit: number;
9
+ cabin?: string;
10
+ stops?: string;
11
+ extraStops?: string;
12
+ carriers?: string;
13
+ routing?: string;
14
+ ext?: string;
15
+ format: OutputFormat;
16
+ headful?: boolean;
17
+ }
18
+ export declare function runSearchCommand(origin: string, dest: string, opts: SearchCommandOptions): Promise<string>;
19
+ /** `--routing` wins; otherwise `--carriers UA,AA` becomes the routing `UA,AA+`. */
20
+ export declare function resolveRouting(opts: SearchCommandOptions): string | undefined;
@@ -0,0 +1,43 @@
1
+ import { runSearch } from "../browser/session.js";
2
+ import { withCache } from "../cache.js";
3
+ import { normalize } from "../render/normalize.js";
4
+ import { renderJson } from "../render/json.js";
5
+ import { renderTable } from "../render/table.js";
6
+ import { carriersToRouting, requireIsoDate, requirePageLimit, resolveCacheOptions, validateTripControls, } from "./shared.js";
7
+ export async function runSearchCommand(origin, dest, opts) {
8
+ validate(origin, dest, opts);
9
+ const spec = {
10
+ origin: origin.toUpperCase(),
11
+ dest: dest.toUpperCase(),
12
+ departDate: opts.depart,
13
+ returnDate: opts.oneWay ? undefined : opts.return,
14
+ adults: opts.adults,
15
+ limit: opts.limit,
16
+ cabin: opts.cabin,
17
+ stops: opts.stops,
18
+ extraStops: opts.extraStops,
19
+ routing: resolveRouting(opts),
20
+ ext: opts.ext,
21
+ };
22
+ const response = await withCache("search", spec, resolveCacheOptions(opts), () => runSearch(spec, { headful: opts.headful }));
23
+ const result = normalize(response, opts.limit);
24
+ return opts.format === "json" ? renderJson(result) : renderTable(result);
25
+ }
26
+ /** `--routing` wins; otherwise `--carriers UA,AA` becomes the routing `UA,AA+`. */
27
+ export function resolveRouting(opts) {
28
+ return opts.routing ?? carriersToRouting(opts.carriers);
29
+ }
30
+ function validate(origin, dest, opts) {
31
+ if (!origin || !dest)
32
+ throw new Error("origin and destination are required");
33
+ requireIsoDate(opts.depart, "--depart");
34
+ if (opts.return && !opts.oneWay) {
35
+ requireIsoDate(opts.return, "--return");
36
+ if (opts.return < opts.depart) {
37
+ throw new Error("--return must be on or after --depart");
38
+ }
39
+ }
40
+ validateTripControls(opts);
41
+ requirePageLimit(opts.limit);
42
+ }
43
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,GAGrB,MAAM,aAAa,CAAC;AAoBrB,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,IAAY,EACZ,IAA0B;IAE1B,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAE7B,MAAM,IAAI,GAAe;QACvB,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE;QAC5B,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;QACxB,UAAU,EAAE,IAAI,CAAC,MAAM;QACvB,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM;QACjD,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAA0B;QACtC,KAAK,EAAE,IAAI,CAAC,KAA8B;QAC1C,UAAU,EAAE,IAAI,CAAC,UAAmC;QACpD,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;QAC7B,GAAG,EAAE,IAAI,CAAC,GAAG;KACd,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,mBAAmB,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAC/E,SAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAC3C,CAAC;IACF,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AAC3E,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,IAA0B;IACvD,OAAO,IAAI,CAAC,OAAO,IAAI,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,MAAc,EAAE,IAAY,EAAE,IAA0B;IACxE,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAC7E,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC3B,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};