tripit 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.
package/index.ts ADDED
@@ -0,0 +1,1024 @@
1
+ #!/usr/bin/env bun
2
+ import { Command, program } from "commander";
3
+ import { TripIt } from "./src/tripit";
4
+
5
+ function requiredEnv(name: string): string {
6
+ const val = process.env[name];
7
+ if (!val) throw new Error(`Missing required env var: ${name}`);
8
+ return val;
9
+ }
10
+
11
+ function createClient(): TripIt {
12
+ return new TripIt({
13
+ clientId: requiredEnv("TRIPIT_CLIENT_ID"),
14
+ clientSecret: requiredEnv("TRIPIT_CLIENT_SECRET"),
15
+ username: requiredEnv("TRIPIT_USERNAME"),
16
+ password: requiredEnv("TRIPIT_PASSWORD"),
17
+ });
18
+ }
19
+
20
+ function output(data: any, format: string, textFn: (data: any) => string) {
21
+ if (format === "json") {
22
+ console.log(JSON.stringify(data, null, 2));
23
+ } else {
24
+ console.log(textFn(data));
25
+ }
26
+ }
27
+
28
+ function normalizeArray(node: any): any[] {
29
+ if (!node) return [];
30
+ if (Array.isArray(node)) return node;
31
+ return [node];
32
+ }
33
+
34
+ function formatDeleteResult(resource: string, id: string, data: any): string {
35
+ const lines = [`Deleted ${resource}: ${id}`];
36
+ if (data?.timestamp) lines.push(`Timestamp: ${data.timestamp}`);
37
+ return lines.join("\n");
38
+ }
39
+
40
+ function configureHelpOnError(cmd: Command): Command {
41
+ cmd.showHelpAfterError(true);
42
+ cmd.configureOutput({
43
+ outputError: (str, write) => {
44
+ write(`\n${str}`);
45
+ },
46
+ });
47
+ return cmd;
48
+ }
49
+
50
+ program
51
+ .name("tripit")
52
+ .description("CLI for managing TripIt trips")
53
+ .version("0.1.0");
54
+
55
+ program
56
+ .command("login")
57
+ .description("Authenticate with TripIt and cache the token")
58
+ .option("-o, --output <format>", "Output format (text or json)", "text")
59
+ .action(async (options) => {
60
+ const client = createClient();
61
+ await client.authenticate();
62
+ output(
63
+ { message: "Authenticated successfully. Token cached." },
64
+ options.output,
65
+ (data) => data.message,
66
+ );
67
+ });
68
+
69
+ // === Trips ===
70
+
71
+ const trips = configureHelpOnError(
72
+ new Command("trips").description("Manage trips"),
73
+ );
74
+
75
+ trips
76
+ .command("list")
77
+ .description("List all trips")
78
+ .option("-o, --output <format>", "Output format (text or json)", "text")
79
+ .option("--page-size <size>", "Page size", "100")
80
+ .option("--page-num <num>", "Page number", "1")
81
+ .option("--past", "Show past trips instead of current/future", false)
82
+ .action(async (options) => {
83
+ const client = createClient();
84
+ await client.authenticate();
85
+ const result = await client.listTrips(
86
+ Number.parseInt(options.pageSize, 10),
87
+ Number.parseInt(options.pageNum, 10),
88
+ options.past,
89
+ );
90
+ output(result, options.output, (data) => {
91
+ const trips = normalizeArray(data.Trip);
92
+ if (!trips.length) return "No trips found.";
93
+ const lines = trips.map((t, i) => {
94
+ const name = t.display_name || "Unnamed";
95
+ const loc = t.primary_location || "";
96
+ const start = t.start_date || "?";
97
+ const end = t.end_date || "?";
98
+ return `${i + 1}. ${name} ${start} - ${end} ${loc}\n UUID: ${t.uuid}`;
99
+ });
100
+ return lines.join("\n\n");
101
+ });
102
+ });
103
+
104
+ trips
105
+ .command("get")
106
+ .description("Get trip by ID")
107
+ .argument("<id>", "Trip ID or UUID")
108
+ .option("-o, --output <format>", "Output format (text or json)", "text")
109
+ .action(async (id, options) => {
110
+ const client = createClient();
111
+ await client.authenticate();
112
+ const result = await client.getTrip(id);
113
+ output(result, options.output, (data) => {
114
+ const trip = data.Trip;
115
+ const lines: string[] = [];
116
+ lines.push(`${trip.display_name}`);
117
+ lines.push(
118
+ `${trip.start_date} - ${trip.end_date} ${trip.primary_location || ""}`,
119
+ );
120
+ lines.push(`UUID: ${trip.uuid}`);
121
+
122
+ const flights = normalizeArray(data.AirObject);
123
+ if (flights.length) {
124
+ lines.push("");
125
+ lines.push("Flights:");
126
+ for (const flight of flights) {
127
+ const segments = normalizeArray(flight.Segment);
128
+ for (const seg of segments) {
129
+ const from = `${seg.start_city_name} (${seg.start_airport_code})`;
130
+ const to = `${seg.end_city_name} (${seg.end_airport_code})`;
131
+ const airline = seg.marketing_airline_code || "";
132
+ const num = seg.marketing_flight_number || "";
133
+ const date = seg.StartDateTime?.date || "";
134
+ const dep = seg.StartDateTime?.time?.slice(0, 5) || "";
135
+ const arr = seg.EndDateTime?.time?.slice(0, 5) || "";
136
+ const dur = seg.duration || "";
137
+ lines.push(
138
+ ` ${airline}${num} ${from} -> ${to} ${date} ${dep}-${arr} ${dur}`,
139
+ );
140
+ }
141
+ }
142
+ }
143
+
144
+ const hotels = normalizeArray(data.LodgingObject);
145
+ if (hotels.length) {
146
+ lines.push("");
147
+ lines.push("Hotels:");
148
+ for (const h of hotels) {
149
+ const checkin = h.StartDateTime?.date || "";
150
+ const checkout = h.EndDateTime?.date || "";
151
+ lines.push(
152
+ ` ${h.display_name || h.supplier_name} ${checkin} - ${checkout}`,
153
+ );
154
+ if (h.Address?.address) lines.push(` ${h.Address.address}`);
155
+ }
156
+ }
157
+
158
+ const transports = normalizeArray(data.TransportObject);
159
+ if (transports.length) {
160
+ lines.push("");
161
+ lines.push("Transport:");
162
+ for (const t of transports) {
163
+ const seg = normalizeArray(t.Segment)[0];
164
+ const date = seg?.StartDateTime?.date || "";
165
+ const dep = seg?.StartDateTime?.time?.slice(0, 5) || "";
166
+ const arr = seg?.EndDateTime?.time?.slice(0, 5) || "";
167
+ lines.push(` ${t.display_name} ${date} ${dep}-${arr}`);
168
+ }
169
+ }
170
+
171
+ const activities = normalizeArray(data.ActivityObject);
172
+ if (activities.length) {
173
+ lines.push("");
174
+ lines.push("Activities:");
175
+ for (const a of activities) {
176
+ const date = a.StartDateTime?.date || "";
177
+ const time = a.StartDateTime?.time?.slice(0, 5) || "";
178
+ lines.push(` ${a.display_name} ${date} ${time}`);
179
+ }
180
+ }
181
+
182
+ return lines.join("\n");
183
+ });
184
+ });
185
+
186
+ trips
187
+ .command("create")
188
+ .description("Create a new trip")
189
+ .requiredOption("--name <name>", "Trip name")
190
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
191
+ .option("--end <date>", "End date (YYYY-MM-DD)")
192
+ .option("--location <location>", "Primary location")
193
+ .option("-o, --output <format>", "Output format (text or json)", "text")
194
+ .action(async (options) => {
195
+ const today = new Date().toISOString().slice(0, 10);
196
+ const client = createClient();
197
+ await client.authenticate();
198
+ const result = await client.createTrip({
199
+ displayName: options.name,
200
+ startDate: options.start || today,
201
+ endDate: options.end || options.start || today,
202
+ primaryLocation: options.location,
203
+ });
204
+ output(result, options.output, (data) => {
205
+ const trip = data.Trip;
206
+ return `Created: ${trip.display_name} ${trip.start_date} - ${trip.end_date} ${trip.primary_location || ""}\nUUID: ${trip.uuid}`;
207
+ });
208
+ });
209
+
210
+ trips
211
+ .command("update")
212
+ .description("Update a trip")
213
+ .argument("<id>", "Trip ID or UUID")
214
+ .option("--name <name>", "Trip name")
215
+ .option("--start <date>", "Start date (YYYY-MM-DD)")
216
+ .option("--end <date>", "End date (YYYY-MM-DD)")
217
+ .option("--location <location>", "Primary location")
218
+ .option("--description <description>", "Description")
219
+ .option("-o, --output <format>", "Output format (text or json)", "text")
220
+ .action(async (id, options) => {
221
+ const client = createClient();
222
+ await client.authenticate();
223
+ const result = await client.updateTrip({
224
+ id,
225
+ displayName: options.name,
226
+ startDate: options.start,
227
+ endDate: options.end,
228
+ primaryLocation: options.location,
229
+ description: options.description,
230
+ });
231
+ output(result, options.output, (data) => {
232
+ const trip = data.Trip;
233
+ return `Updated: ${trip.display_name} ${trip.start_date} - ${trip.end_date} ${trip.primary_location || ""}\nUUID: ${trip.uuid}`;
234
+ });
235
+ });
236
+
237
+ trips
238
+ .command("delete")
239
+ .description("Delete a trip")
240
+ .argument("<id>", "Trip ID or UUID")
241
+ .option("-o, --output <format>", "Output format (text or json)", "text")
242
+ .action(async (id, options) => {
243
+ const client = createClient();
244
+ await client.authenticate();
245
+ const result = await client.deleteTrip(id);
246
+ output(result, options.output, (data) =>
247
+ formatDeleteResult("trip", id, data),
248
+ );
249
+ });
250
+
251
+ program.addCommand(trips);
252
+
253
+ // === Hotels ===
254
+
255
+ const hotels = configureHelpOnError(
256
+ new Command("hotels").description("Manage hotel bookings"),
257
+ );
258
+
259
+ hotels
260
+ .command("get")
261
+ .description("Get hotel by ID")
262
+ .argument("<id>", "Hotel ID or UUID")
263
+ .option("-o, --output <format>", "Output format (text or json)", "text")
264
+ .action(async (id, options) => {
265
+ const client = createClient();
266
+ await client.authenticate();
267
+ const result = await client.getHotel(id);
268
+ output(result, options.output, (data) => {
269
+ const h = data.LodgingObject;
270
+ const lines = [
271
+ `${h.display_name || h.supplier_name}`,
272
+ `${h.StartDateTime?.date || ""} ${h.StartDateTime?.time?.slice(0, 5) || ""} - ${h.EndDateTime?.date || ""} ${h.EndDateTime?.time?.slice(0, 5) || ""}`,
273
+ `UUID: ${h.uuid}`,
274
+ ];
275
+ if (h.Address?.address) lines.push(`Address: ${h.Address.address}`);
276
+ if (h.notes) lines.push(`Notes: ${h.notes}`);
277
+ return lines.join("\n");
278
+ });
279
+ });
280
+
281
+ hotels
282
+ .command("create")
283
+ .description("Add a hotel to a trip")
284
+ .requiredOption("--trip <uuid>", "Trip UUID")
285
+ .requiredOption("--name <name>", "Hotel name")
286
+ .requiredOption("--checkin <date>", "Check-in date (YYYY-MM-DD)")
287
+ .requiredOption("--checkout <date>", "Check-out date (YYYY-MM-DD)")
288
+ .option("--checkin-time <time>", "Check-in time (HH:MM)", "15:00")
289
+ .option("--checkout-time <time>", "Check-out time (HH:MM)", "11:00")
290
+ .option("--timezone <tz>", "Timezone", "UTC")
291
+ .requiredOption("--address <address>", "Street address")
292
+ .requiredOption("--city <city>", "City")
293
+ .requiredOption("--country <country>", "Country code (e.g. JP, US)")
294
+ .option("--state <state>", "State/province")
295
+ .option("--zip <zip>", "Postal code")
296
+ .option("--confirmation <num>", "Confirmation number")
297
+ .option("--rate <rate>", "Booking rate")
298
+ .option("--notes <notes>", "Notes")
299
+ .option("--cost <cost>", "Total cost")
300
+ .option("-o, --output <format>", "Output format (text or json)", "text")
301
+ .action(async (options) => {
302
+ const client = createClient();
303
+ await client.authenticate();
304
+ const result = await client.createHotel({
305
+ tripId: options.trip,
306
+ hotelName: options.name,
307
+ checkInDate: options.checkin,
308
+ checkInTime: options.checkinTime,
309
+ checkOutDate: options.checkout,
310
+ checkOutTime: options.checkoutTime,
311
+ timezone: options.timezone,
312
+ street: options.address,
313
+ city: options.city,
314
+ country: options.country,
315
+ state: options.state,
316
+ zip: options.zip,
317
+ supplierConfNum: options.confirmation,
318
+ bookingRate: options.rate,
319
+ notes: options.notes,
320
+ totalCost: options.cost,
321
+ });
322
+ output(result, options.output, (data) => {
323
+ const h = data.LodgingObject;
324
+ return `Created: ${h.supplier_name} ${h.StartDateTime?.date} - ${h.EndDateTime?.date}\nUUID: ${h.uuid}`;
325
+ });
326
+ });
327
+
328
+ hotels
329
+ .command("update")
330
+ .description("Update a hotel")
331
+ .argument("<id>", "Hotel ID or UUID")
332
+ .option("--trip <uuid>", "Trip UUID")
333
+ .option("--name <name>", "Hotel name")
334
+ .option("--checkin <date>", "Check-in date (YYYY-MM-DD)")
335
+ .option("--checkout <date>", "Check-out date (YYYY-MM-DD)")
336
+ .option("--checkin-time <time>", "Check-in time (HH:MM)")
337
+ .option("--checkout-time <time>", "Check-out time (HH:MM)")
338
+ .option("--timezone <tz>", "Timezone")
339
+ .option("--address <address>", "Street address")
340
+ .option("--city <city>", "City")
341
+ .option("--country <country>", "Country code (e.g. JP, US)")
342
+ .option("--state <state>", "State/province")
343
+ .option("--zip <zip>", "Postal code")
344
+ .option("--confirmation <num>", "Confirmation number")
345
+ .option("--rate <rate>", "Booking rate")
346
+ .option("--notes <notes>", "Notes")
347
+ .option("--cost <cost>", "Total cost")
348
+ .option("-o, --output <format>", "Output format (text or json)", "text")
349
+ .action(async (id, options) => {
350
+ const client = createClient();
351
+ await client.authenticate();
352
+ const result = await client.updateHotel({
353
+ id,
354
+ tripId: options.trip,
355
+ hotelName: options.name,
356
+ checkInDate: options.checkin,
357
+ checkInTime: options.checkinTime,
358
+ checkOutDate: options.checkout,
359
+ checkOutTime: options.checkoutTime,
360
+ timezone: options.timezone,
361
+ street: options.address,
362
+ city: options.city,
363
+ country: options.country,
364
+ state: options.state,
365
+ zip: options.zip,
366
+ supplierConfNum: options.confirmation,
367
+ bookingRate: options.rate,
368
+ notes: options.notes,
369
+ totalCost: options.cost,
370
+ });
371
+ output(result, options.output, (data) => {
372
+ const h = data.LodgingObject;
373
+ return `Updated: ${h.supplier_name} ${h.StartDateTime?.date} - ${h.EndDateTime?.date}\nUUID: ${h.uuid}`;
374
+ });
375
+ });
376
+
377
+ hotels
378
+ .command("delete")
379
+ .description("Delete a hotel")
380
+ .argument("<id>", "Hotel ID or UUID")
381
+ .option("-o, --output <format>", "Output format (text or json)", "text")
382
+ .action(async (id, options) => {
383
+ const client = createClient();
384
+ await client.authenticate();
385
+ const result = await client.deleteHotel(id);
386
+ output(result, options.output, (data) =>
387
+ formatDeleteResult("hotel", id, data),
388
+ );
389
+ });
390
+
391
+ hotels
392
+ .command("attach-document")
393
+ .description("Attach a document to a hotel reservation")
394
+ .argument("<id>", "Hotel ID or UUID")
395
+ .requiredOption("--file <path>", "Path to file")
396
+ .option("--name <name>", "Document name/caption")
397
+ .option("--mime-type <mime>", "Override MIME type")
398
+ .option("-o, --output <format>", "Output format (text or json)", "text")
399
+ .action(async (id, options) => {
400
+ const client = createClient();
401
+ await client.authenticate();
402
+ const result = await client.attachDocument({
403
+ objectType: "lodging",
404
+ objectId: id,
405
+ filePath: options.file,
406
+ caption: options.name,
407
+ mimeType: options.mimeType,
408
+ });
409
+ output(result, options.output, (data) => {
410
+ const h = data.LodgingObject;
411
+ return `Attached document to hotel: ${h.display_name || h.supplier_name}\nUUID: ${h.uuid}`;
412
+ });
413
+ });
414
+
415
+ hotels
416
+ .command("remove-document")
417
+ .description("Remove a document from a hotel reservation")
418
+ .argument("<id>", "Hotel ID or UUID")
419
+ .option("--uuid <uuid>", "Attachment UUID to remove")
420
+ .option("--image-uuid <uuid>", "Deprecated alias for --uuid")
421
+ .option("--url <url>", "Document URL to remove")
422
+ .option("--caption <caption>", "Remove first document with matching caption")
423
+ .option("--index <number>", "1-based document index to remove")
424
+ .option("--all", "Remove all documents", false)
425
+ .option("-o, --output <format>", "Output format (text or json)", "text")
426
+ .action(async (id, options) => {
427
+ const uuid = options.uuid ?? options.imageUuid;
428
+ const selectorCount = [
429
+ Boolean(uuid),
430
+ Boolean(options.url),
431
+ Boolean(options.caption),
432
+ options.index !== undefined,
433
+ Boolean(options.all),
434
+ ].filter(Boolean).length;
435
+ if (selectorCount !== 1) {
436
+ throw new Error(
437
+ "Provide exactly one selector: --uuid, --url, --caption, --index, or --all",
438
+ );
439
+ }
440
+
441
+ const client = createClient();
442
+ await client.authenticate();
443
+ const parsedIndex =
444
+ options.index !== undefined
445
+ ? Number.parseInt(options.index, 10)
446
+ : undefined;
447
+ if (
448
+ parsedIndex !== undefined &&
449
+ (!Number.isFinite(parsedIndex) || parsedIndex < 1)
450
+ ) {
451
+ throw new Error("--index must be a positive integer");
452
+ }
453
+ const result = await client.removeDocument({
454
+ objectType: "lodging",
455
+ objectId: id,
456
+ imageUuid: uuid,
457
+ imageUrl: options.url,
458
+ caption: options.caption,
459
+ index: parsedIndex,
460
+ removeAll: options.all,
461
+ });
462
+ output(result, options.output, (data) => {
463
+ const h = data.LodgingObject;
464
+ const images = normalizeArray(h.Image);
465
+ return `Removed document(s) from hotel: ${h.display_name || h.supplier_name}\nUUID: ${h.uuid}\nRemaining documents: ${images.length}`;
466
+ });
467
+ });
468
+
469
+ program.addCommand(hotels);
470
+
471
+ // === Flights ===
472
+
473
+ const flights = configureHelpOnError(
474
+ new Command("flights").description("Manage flights"),
475
+ );
476
+
477
+ flights
478
+ .command("get")
479
+ .description("Get flight by ID")
480
+ .argument("<id>", "Flight ID or UUID")
481
+ .option("-o, --output <format>", "Output format (text or json)", "text")
482
+ .action(async (id, options) => {
483
+ const client = createClient();
484
+ await client.authenticate();
485
+ const result = await client.getFlight(id);
486
+ output(result, options.output, (data) => {
487
+ const f = data.AirObject;
488
+ const seg = normalizeArray(f.Segment)[0];
489
+ const from = `${seg?.start_city_name || ""} (${seg?.start_airport_code || ""})`;
490
+ const to = `${seg?.end_city_name || ""} (${seg?.end_airport_code || ""})`;
491
+ return [
492
+ `${f.display_name || "Flight"}`,
493
+ `${from} -> ${to}`,
494
+ `${seg?.StartDateTime?.date || ""} ${seg?.StartDateTime?.time?.slice(0, 5) || ""} - ${seg?.EndDateTime?.time?.slice(0, 5) || ""}`,
495
+ `UUID: ${f.uuid}`,
496
+ ].join("\n");
497
+ });
498
+ });
499
+
500
+ flights
501
+ .command("create")
502
+ .description("Add a flight to a trip")
503
+ .requiredOption("--trip <uuid>", "Trip UUID")
504
+ .requiredOption("--name <name>", "Display name")
505
+ .requiredOption("--airline <name>", "Airline name")
506
+ .requiredOption("--from <city>", "Departure city")
507
+ .requiredOption("--from-code <code>", "Departure country code")
508
+ .requiredOption("--to <city>", "Arrival city")
509
+ .requiredOption("--to-code <code>", "Arrival country code")
510
+ .requiredOption("--airline-code <code>", "Airline code (e.g. NH, JL)")
511
+ .requiredOption("--flight-num <num>", "Flight number")
512
+ .requiredOption("--depart-date <date>", "Departure date (YYYY-MM-DD)")
513
+ .requiredOption("--depart-time <time>", "Departure time (HH:MM)")
514
+ .requiredOption("--depart-tz <tz>", "Departure timezone")
515
+ .requiredOption("--arrive-date <date>", "Arrival date (YYYY-MM-DD)")
516
+ .requiredOption("--arrive-time <time>", "Arrival time (HH:MM)")
517
+ .requiredOption("--arrive-tz <tz>", "Arrival timezone")
518
+ .option("--aircraft <type>", "Aircraft type")
519
+ .option("--class <class>", "Service class")
520
+ .option("--confirmation <num>", "Confirmation number")
521
+ .option("--notes <notes>", "Notes")
522
+ .option("--cost <cost>", "Total cost")
523
+ .option("-o, --output <format>", "Output format (text or json)", "text")
524
+ .action(async (options) => {
525
+ const client = createClient();
526
+ await client.authenticate();
527
+ const result = await client.createFlight({
528
+ tripId: options.trip,
529
+ displayName: options.name,
530
+ supplierName: options.airline,
531
+ supplierConfNum: options.confirmation,
532
+ notes: options.notes,
533
+ totalCost: options.cost,
534
+ segments: [
535
+ {
536
+ startDate: options.departDate,
537
+ startTime: options.departTime,
538
+ startTimezone: options.departTz,
539
+ endDate: options.arriveDate,
540
+ endTime: options.arriveTime,
541
+ endTimezone: options.arriveTz,
542
+ startCityName: options.from,
543
+ startCountryCode: options.fromCode,
544
+ endCityName: options.to,
545
+ endCountryCode: options.toCode,
546
+ marketingAirline: options.airlineCode,
547
+ marketingFlightNumber: options.flightNum,
548
+ aircraft: options.aircraft,
549
+ serviceClass: options.class,
550
+ },
551
+ ],
552
+ });
553
+ output(result, options.output, (data) => {
554
+ const f = data.AirObject;
555
+ const seg = normalizeArray(f.Segment)[0];
556
+ return `Created: ${f.display_name} ${seg?.start_city_name} -> ${seg?.end_city_name}\nUUID: ${f.uuid}`;
557
+ });
558
+ });
559
+
560
+ flights
561
+ .command("update")
562
+ .description("Update a flight")
563
+ .argument("<id>", "Flight ID or UUID")
564
+ .option("--trip <uuid>", "Trip UUID")
565
+ .option("--name <name>", "Display name")
566
+ .option("--airline <name>", "Airline name")
567
+ .option("--from <city>", "Departure city")
568
+ .option("--from-code <code>", "Departure country code")
569
+ .option("--to <city>", "Arrival city")
570
+ .option("--to-code <code>", "Arrival country code")
571
+ .option("--airline-code <code>", "Airline code (e.g. NH, JL)")
572
+ .option("--flight-num <num>", "Flight number")
573
+ .option("--depart-date <date>", "Departure date (YYYY-MM-DD)")
574
+ .option("--depart-time <time>", "Departure time (HH:MM)")
575
+ .option("--depart-tz <tz>", "Departure timezone")
576
+ .option("--arrive-date <date>", "Arrival date (YYYY-MM-DD)")
577
+ .option("--arrive-time <time>", "Arrival time (HH:MM)")
578
+ .option("--arrive-tz <tz>", "Arrival timezone")
579
+ .option("--aircraft <type>", "Aircraft type")
580
+ .option("--class <class>", "Service class")
581
+ .option("--confirmation <num>", "Confirmation number")
582
+ .option("--notes <notes>", "Notes")
583
+ .option("--cost <cost>", "Total cost")
584
+ .option("-o, --output <format>", "Output format (text or json)", "text")
585
+ .action(async (id, options) => {
586
+ const client = createClient();
587
+ await client.authenticate();
588
+ const result = await client.updateFlight({
589
+ id,
590
+ tripId: options.trip,
591
+ displayName: options.name,
592
+ supplierName: options.airline,
593
+ supplierConfNum: options.confirmation,
594
+ notes: options.notes,
595
+ totalCost: options.cost,
596
+ segment: {
597
+ startDate: options.departDate,
598
+ startTime: options.departTime,
599
+ startTimezone: options.departTz,
600
+ endDate: options.arriveDate,
601
+ endTime: options.arriveTime,
602
+ endTimezone: options.arriveTz,
603
+ startCityName: options.from,
604
+ startCountryCode: options.fromCode,
605
+ endCityName: options.to,
606
+ endCountryCode: options.toCode,
607
+ marketingAirline: options.airlineCode,
608
+ marketingFlightNumber: options.flightNum,
609
+ aircraft: options.aircraft,
610
+ serviceClass: options.class,
611
+ },
612
+ });
613
+ output(result, options.output, (data) => {
614
+ const f = data.AirObject;
615
+ const seg = normalizeArray(f.Segment)[0];
616
+ return `Updated: ${f.display_name} ${seg?.start_city_name} -> ${seg?.end_city_name}\nUUID: ${f.uuid}`;
617
+ });
618
+ });
619
+
620
+ flights
621
+ .command("delete")
622
+ .description("Delete a flight")
623
+ .argument("<id>", "Flight ID or UUID")
624
+ .option("-o, --output <format>", "Output format (text or json)", "text")
625
+ .action(async (id, options) => {
626
+ const client = createClient();
627
+ await client.authenticate();
628
+ const result = await client.deleteFlight(id);
629
+ output(result, options.output, (data) =>
630
+ formatDeleteResult("flight", id, data),
631
+ );
632
+ });
633
+
634
+ program.addCommand(flights);
635
+
636
+ // === Transport ===
637
+
638
+ const transport = configureHelpOnError(
639
+ new Command("transport").description("Manage transport"),
640
+ );
641
+
642
+ transport
643
+ .command("get")
644
+ .description("Get transport by ID")
645
+ .argument("<id>", "Transport ID or UUID")
646
+ .option("-o, --output <format>", "Output format (text or json)", "text")
647
+ .action(async (id, options) => {
648
+ const client = createClient();
649
+ await client.authenticate();
650
+ const result = await client.getTransport(id);
651
+ output(result, options.output, (data) => {
652
+ const t = data.TransportObject;
653
+ const seg = normalizeArray(t.Segment)[0];
654
+ return [
655
+ `${t.display_name || "Transport"}`,
656
+ `${seg?.StartDateTime?.date || ""} ${seg?.StartDateTime?.time?.slice(0, 5) || ""} - ${seg?.EndDateTime?.time?.slice(0, 5) || ""}`,
657
+ `From: ${seg?.StartLocationAddress?.address || ""}`,
658
+ `To: ${seg?.EndLocationAddress?.address || ""}`,
659
+ `UUID: ${t.uuid}`,
660
+ ].join("\n");
661
+ });
662
+ });
663
+
664
+ transport
665
+ .command("create")
666
+ .description("Add transport to a trip")
667
+ .requiredOption("--trip <uuid>", "Trip UUID")
668
+ .requiredOption("--from <address>", "Start address")
669
+ .requiredOption("--to <address>", "End address")
670
+ .requiredOption("--depart-date <date>", "Departure date (YYYY-MM-DD)")
671
+ .requiredOption("--depart-time <time>", "Departure time (HH:MM)")
672
+ .requiredOption("--arrive-date <date>", "Arrival date (YYYY-MM-DD)")
673
+ .requiredOption("--arrive-time <time>", "Arrival time (HH:MM)")
674
+ .requiredOption("--timezone <tz>", "Timezone")
675
+ .option("--from-name <name>", "Start location name")
676
+ .option("--to-name <name>", "End location name")
677
+ .option("--name <name>", "Display name")
678
+ .option("--vehicle <desc>", "Vehicle description")
679
+ .option("--carrier <name>", "Carrier name")
680
+ .option("--confirmation <num>", "Confirmation number")
681
+ .option("-o, --output <format>", "Output format (text or json)", "text")
682
+ .action(async (options) => {
683
+ const client = createClient();
684
+ await client.authenticate();
685
+ const result = await client.createTransport({
686
+ tripId: options.trip,
687
+ startAddress: options.from,
688
+ endAddress: options.to,
689
+ startDate: options.departDate,
690
+ startTime: options.departTime,
691
+ endDate: options.arriveDate,
692
+ endTime: options.arriveTime,
693
+ timezone: options.timezone,
694
+ startLocationName: options.fromName,
695
+ endLocationName: options.toName,
696
+ displayName: options.name,
697
+ vehicleDescription: options.vehicle,
698
+ carrierName: options.carrier,
699
+ confirmationNum: options.confirmation,
700
+ });
701
+ output(result, options.output, (data) => {
702
+ const t = data.TransportObject;
703
+ return `Created: ${t.display_name}\nUUID: ${t.uuid}`;
704
+ });
705
+ });
706
+
707
+ transport
708
+ .command("update")
709
+ .description("Update transport")
710
+ .argument("<id>", "Transport ID or UUID")
711
+ .option("--trip <uuid>", "Trip UUID")
712
+ .option("--from <address>", "Start address")
713
+ .option("--to <address>", "End address")
714
+ .option("--depart-date <date>", "Departure date (YYYY-MM-DD)")
715
+ .option("--depart-time <time>", "Departure time (HH:MM)")
716
+ .option("--arrive-date <date>", "Arrival date (YYYY-MM-DD)")
717
+ .option("--arrive-time <time>", "Arrival time (HH:MM)")
718
+ .option("--timezone <tz>", "Timezone")
719
+ .option("--from-name <name>", "Start location name")
720
+ .option("--to-name <name>", "End location name")
721
+ .option("--name <name>", "Display name")
722
+ .option("--vehicle <desc>", "Vehicle description")
723
+ .option("--carrier <name>", "Carrier name")
724
+ .option("--confirmation <num>", "Confirmation number")
725
+ .option("-o, --output <format>", "Output format (text or json)", "text")
726
+ .action(async (id, options) => {
727
+ const client = createClient();
728
+ await client.authenticate();
729
+ const result = await client.updateTransport({
730
+ id,
731
+ tripId: options.trip,
732
+ startAddress: options.from,
733
+ endAddress: options.to,
734
+ startDate: options.departDate,
735
+ startTime: options.departTime,
736
+ endDate: options.arriveDate,
737
+ endTime: options.arriveTime,
738
+ timezone: options.timezone,
739
+ startLocationName: options.fromName,
740
+ endLocationName: options.toName,
741
+ displayName: options.name,
742
+ vehicleDescription: options.vehicle,
743
+ carrierName: options.carrier,
744
+ confirmationNum: options.confirmation,
745
+ });
746
+ output(result, options.output, (data) => {
747
+ const t = data.TransportObject;
748
+ return `Updated: ${t.display_name}\nUUID: ${t.uuid}`;
749
+ });
750
+ });
751
+
752
+ transport
753
+ .command("delete")
754
+ .description("Delete transport")
755
+ .argument("<id>", "Transport ID or UUID")
756
+ .option("-o, --output <format>", "Output format (text or json)", "text")
757
+ .action(async (id, options) => {
758
+ const client = createClient();
759
+ await client.authenticate();
760
+ const result = await client.deleteTransport(id);
761
+ output(result, options.output, (data) =>
762
+ formatDeleteResult("transport", id, data),
763
+ );
764
+ });
765
+
766
+ program.addCommand(transport);
767
+
768
+ // === Activities ===
769
+
770
+ const activities = configureHelpOnError(
771
+ new Command("activities").description("Manage activities"),
772
+ );
773
+
774
+ activities
775
+ .command("get")
776
+ .description("Get activity by ID")
777
+ .argument("<id>", "Activity ID or UUID")
778
+ .option("-o, --output <format>", "Output format (text or json)", "text")
779
+ .action(async (id, options) => {
780
+ const client = createClient();
781
+ await client.authenticate();
782
+ const result = await client.getActivity(id);
783
+ output(result, options.output, (data) => {
784
+ const a = data.ActivityObject;
785
+ return [
786
+ `${a.display_name || "Activity"}`,
787
+ `${a.StartDateTime?.date || ""} ${a.StartDateTime?.time?.slice(0, 5) || ""} - ${a.EndDateTime?.time?.slice(0, 5) || ""}`,
788
+ `Location: ${a.location_name || ""}`,
789
+ `Address: ${a.Address?.address || ""}`,
790
+ `UUID: ${a.uuid}`,
791
+ ].join("\n");
792
+ });
793
+ });
794
+
795
+ activities
796
+ .command("create")
797
+ .description("Add an activity to a trip")
798
+ .requiredOption("--trip <uuid>", "Trip UUID")
799
+ .requiredOption("--name <name>", "Activity name")
800
+ .requiredOption("--start-date <date>", "Start date (YYYY-MM-DD)")
801
+ .requiredOption("--start-time <time>", "Start time (HH:MM)")
802
+ .requiredOption("--end-date <date>", "End date (YYYY-MM-DD)")
803
+ .requiredOption("--end-time <time>", "End time (HH:MM)")
804
+ .requiredOption("--timezone <tz>", "Timezone")
805
+ .requiredOption("--address <address>", "Address")
806
+ .requiredOption("--location-name <name>", "Location/venue name")
807
+ .option("--city <city>", "City")
808
+ .option("--state <state>", "State/province")
809
+ .option("--zip <zip>", "Postal code")
810
+ .option("--country <country>", "Country code")
811
+ .option("-o, --output <format>", "Output format (text or json)", "text")
812
+ .action(async (options) => {
813
+ const client = createClient();
814
+ await client.authenticate();
815
+ const result = await client.createActivity({
816
+ tripId: options.trip,
817
+ displayName: options.name,
818
+ startDate: options.startDate,
819
+ startTime: options.startTime,
820
+ endDate: options.endDate,
821
+ endTime: options.endTime,
822
+ timezone: options.timezone,
823
+ address: options.address,
824
+ locationName: options.locationName,
825
+ city: options.city,
826
+ state: options.state,
827
+ zip: options.zip,
828
+ country: options.country,
829
+ });
830
+ output(result, options.output, (data) => {
831
+ const a = data.ActivityObject;
832
+ return `Created: ${a.display_name} ${a.StartDateTime?.date} ${a.StartDateTime?.time?.slice(0, 5)}\nUUID: ${a.uuid}`;
833
+ });
834
+ });
835
+
836
+ activities
837
+ .command("update")
838
+ .description("Update an activity")
839
+ .argument("<id>", "Activity ID or UUID")
840
+ .option("--trip <uuid>", "Trip UUID")
841
+ .option("--name <name>", "Activity name")
842
+ .option("--start-date <date>", "Start date (YYYY-MM-DD)")
843
+ .option("--start-time <time>", "Start time (HH:MM)")
844
+ .option("--end-date <date>", "End date (YYYY-MM-DD)")
845
+ .option("--end-time <time>", "End time (HH:MM)")
846
+ .option("--timezone <tz>", "Timezone")
847
+ .option("--address <address>", "Address")
848
+ .option("--location-name <name>", "Location/venue name")
849
+ .option("--city <city>", "City")
850
+ .option("--state <state>", "State/province")
851
+ .option("--zip <zip>", "Postal code")
852
+ .option("--country <country>", "Country code")
853
+ .option("--notes <notes>", "Notes")
854
+ .option("-o, --output <format>", "Output format (text or json)", "text")
855
+ .action(async (id, options) => {
856
+ const client = createClient();
857
+ await client.authenticate();
858
+ const result = await client.updateActivity({
859
+ id,
860
+ tripId: options.trip,
861
+ displayName: options.name,
862
+ startDate: options.startDate,
863
+ startTime: options.startTime,
864
+ endDate: options.endDate,
865
+ endTime: options.endTime,
866
+ timezone: options.timezone,
867
+ address: options.address,
868
+ locationName: options.locationName,
869
+ city: options.city,
870
+ state: options.state,
871
+ zip: options.zip,
872
+ country: options.country,
873
+ notes: options.notes,
874
+ });
875
+ output(result, options.output, (data) => {
876
+ const a = data.ActivityObject;
877
+ return `Updated: ${a.display_name} ${a.StartDateTime?.date} ${a.StartDateTime?.time?.slice(0, 5)}\nUUID: ${a.uuid}`;
878
+ });
879
+ });
880
+
881
+ activities
882
+ .command("delete")
883
+ .description("Delete an activity")
884
+ .argument("<id>", "Activity ID or UUID")
885
+ .option("-o, --output <format>", "Output format (text or json)", "text")
886
+ .action(async (id, options) => {
887
+ const client = createClient();
888
+ await client.authenticate();
889
+ const result = await client.deleteActivity(id);
890
+ output(result, options.output, (data) =>
891
+ formatDeleteResult("activity", id, data),
892
+ );
893
+ });
894
+
895
+ program.addCommand(activities);
896
+
897
+ // === Documents ===
898
+
899
+ const documents = configureHelpOnError(
900
+ new Command("documents").description("Manage document attachments"),
901
+ );
902
+
903
+ const validTypes = ["lodging", "activity", "air", "transport"] as const;
904
+
905
+ documents
906
+ .command("attach")
907
+ .description("Attach a document to a trip object")
908
+ .argument("<id>", "UUID of the object to attach to")
909
+ .option(
910
+ "--type <type>",
911
+ "Object type (lodging, activity, air, transport) — auto-detected if omitted",
912
+ )
913
+ .requiredOption("--file <path>", "Path to file")
914
+ .option("--caption <name>", "Document caption (defaults to filename)")
915
+ .option("--mime-type <type>", "Override MIME type (auto-detected from file)")
916
+ .option("-o, --output <format>", "Output format (text or json)", "text")
917
+ .action(async (id, options) => {
918
+ if (options.type && !validTypes.includes(options.type)) {
919
+ console.error(
920
+ `Invalid type "${options.type}". Must be one of: ${validTypes.join(", ")}`,
921
+ );
922
+ process.exit(1);
923
+ }
924
+ const client = createClient();
925
+ await client.authenticate();
926
+ const result = await client.attachDocument({
927
+ objectType: options.type,
928
+ objectId: id,
929
+ filePath: options.file,
930
+ caption: options.caption,
931
+ mimeType: options.mimeType,
932
+ });
933
+ const keyToType: Record<string, string> = {
934
+ LodgingObject: "lodging",
935
+ ActivityObject: "activity",
936
+ AirObject: "air",
937
+ TransportObject: "transport",
938
+ };
939
+ output(result, options.output, (data) => {
940
+ const objKey = Object.keys(keyToType).find(
941
+ (k) => data[k as keyof typeof data],
942
+ );
943
+ const detectedType = objKey
944
+ ? keyToType[objKey]
945
+ : options.type || "object";
946
+ const obj = objKey ? data[objKey as keyof typeof data] : undefined;
947
+ const name =
948
+ (obj as Record<string, string>)?.display_name ||
949
+ (obj as Record<string, string>)?.supplier_name ||
950
+ detectedType;
951
+ return `Attached document to ${detectedType}: ${name}\nUUID: ${(obj as Record<string, string>)?.uuid || id}`;
952
+ });
953
+ });
954
+
955
+ documents
956
+ .command("remove")
957
+ .description("Remove a document from a trip object")
958
+ .argument("<id>", "UUID of the object to remove the document from")
959
+ .option(
960
+ "--type <type>",
961
+ "Object type (lodging, activity, air, transport) — auto-detected if omitted",
962
+ )
963
+ .option("--image-uuid <uuid>", "UUID of the image to remove")
964
+ .option("--image-url <url>", "URL of the image to remove")
965
+ .option("--caption <caption>", "Caption of the image to remove")
966
+ .option("--index <index>", "1-based index of the image to remove")
967
+ .option("--all", "Remove all documents")
968
+ .option("-o, --output <format>", "Output format (text or json)", "text")
969
+ .action(async (id, options) => {
970
+ if (options.type && !validTypes.includes(options.type)) {
971
+ console.error(
972
+ `Invalid type "${options.type}". Must be one of: ${validTypes.join(", ")}`,
973
+ );
974
+ process.exit(1);
975
+ }
976
+ const selectors = [
977
+ options.imageUuid,
978
+ options.imageUrl,
979
+ options.caption,
980
+ options.index,
981
+ options.all,
982
+ ].filter(Boolean).length;
983
+ if (selectors !== 1) {
984
+ console.error(
985
+ "Provide exactly one selector: --image-uuid, --image-url, --caption, --index, or --all",
986
+ );
987
+ process.exit(1);
988
+ }
989
+ const client = createClient();
990
+ await client.authenticate();
991
+ const result = await client.removeDocument({
992
+ objectType: options.type,
993
+ objectId: id,
994
+ imageUuid: options.imageUuid,
995
+ imageUrl: options.imageUrl,
996
+ caption: options.caption,
997
+ index: options.index ? Number.parseInt(options.index, 10) : undefined,
998
+ removeAll: options.all,
999
+ });
1000
+ const keyToType: Record<string, string> = {
1001
+ LodgingObject: "lodging",
1002
+ ActivityObject: "activity",
1003
+ AirObject: "air",
1004
+ TransportObject: "transport",
1005
+ };
1006
+ output(result, options.output, (data) => {
1007
+ const objKey = Object.keys(keyToType).find(
1008
+ (k) => data[k as keyof typeof data],
1009
+ );
1010
+ const detectedType = objKey
1011
+ ? keyToType[objKey]
1012
+ : options.type || "object";
1013
+ const obj = objKey ? data[objKey as keyof typeof data] : undefined;
1014
+ const name =
1015
+ (obj as Record<string, string>)?.display_name ||
1016
+ (obj as Record<string, string>)?.supplier_name ||
1017
+ detectedType;
1018
+ return `Removed document from ${detectedType}: ${name}\nUUID: ${(obj as Record<string, string>)?.uuid || id}`;
1019
+ });
1020
+ });
1021
+
1022
+ program.addCommand(documents);
1023
+
1024
+ program.parse();