@commandgarden/cli 2.4.0 → 2.5.1

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
11
  <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
12
12
  <title>commandGarden</title>
13
- <script type="module" crossorigin src="/assets/index--IDuJWmP.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DCptfmmK.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-qunCIeG9.css">
15
15
  </head>
16
16
  <body>
@@ -289,6 +289,7 @@ if (isEmail) {
289
289
  throw new Error('Could not open the room finder');
290
290
  }
291
291
  typeText(inputSel, room);
292
+ await sleep(1000);
292
293
 
293
294
  let label = null;
294
295
  for (let i = 0; i < 25 && !label; i++) {
@@ -191,8 +191,25 @@ function buildTimeline(items, dateIso) {
191
191
 
192
192
  const rawRooms = '${{ args.rooms }}'.trim();
193
193
  if (!rawRooms) throw new Error('Missing rooms argument — pass comma-separated room names or emails');
194
- const rooms = rawRooms.split(',').map(r => r.trim()).filter(r => r.length > 0);
195
- if (rooms.length === 0) throw new Error('No rooms provided after parsing');
194
+ const roomTokens = rawRooms.split(',').map(r => r.trim()).filter(r => r.length > 0);
195
+ if (roomTokens.length === 0) throw new Error('No rooms provided after parsing');
196
+
197
+ const parsedRooms = roomTokens.map(r => {
198
+ const colonIdx = r.lastIndexOf(':');
199
+ if (colonIdx > 0) {
200
+ const name = r.substring(0, colonIdx).trim();
201
+ const email = r.substring(colonIdx + 1).trim();
202
+ if (EMAIL_RE.test(email)) {
203
+ return { name, email: email.toLowerCase(), mode: 'pair' };
204
+ }
205
+ }
206
+ if (EMAIL_RE.test(r)) {
207
+ return { name: r, email: r.toLowerCase(), mode: 'email' };
208
+ }
209
+ return { name: r, email: null, mode: 'name' };
210
+ });
211
+
212
+ const canBatch = parsedRooms.every(r => r.mode === 'pair');
196
213
 
197
214
  let date = '${{ args.date | default("") }}'.trim();
198
215
  if (!date) date = todayLocalISO();
@@ -309,6 +326,8 @@ async function addRoomByName(name) {
309
326
  throw new Error('Could not open the room finder');
310
327
  }
311
328
  typeText(inputSel, name);
329
+ // Wait for OWA to process the search and refresh suggestions.
330
+ await sleep(1500);
312
331
  }
313
332
 
314
333
  let label = null;
@@ -375,65 +394,154 @@ async function captureNewSchedule(knownIds) {
375
394
  return { newId, schedule, sawView, errMsg, items: [...itemsById.values()] };
376
395
  }
377
396
 
378
- // ── Sequential room processing ───────────────────────────────────────
397
+ /**
398
+ * Batch capture: wait for getSchedule responses covering all expected emails.
399
+ * Returns a Map of email (lowercased) -> { schedule, items, sawView, errMsg }.
400
+ */
401
+ async function captureBatchSchedules(expectedEmails, knownIds) {
402
+ const byEmail = new Map();
403
+
404
+ for (let i = 0; i < 80; i++) {
405
+ await sleep(100);
406
+ for (const pair of readCapture()) {
407
+ for (const [id, s] of schedulesFromPair(pair)) {
408
+ if (knownIds.has(id)) continue;
409
+ const idLower = id.toLowerCase();
410
+ if (!expectedEmails.has(idLower)) continue;
411
+ const items = (s.scheduleItems || []).filter(Boolean);
412
+ byEmail.set(idLower, {
413
+ schedule: s,
414
+ items,
415
+ sawView: !!(s.availabilityView && s.availabilityView.length),
416
+ errMsg: s.error ? (s.error.message || s.error.responseCode || 'unknown') : null,
417
+ });
418
+ }
419
+ }
420
+ if (expectedEmails.size > 0 && [...expectedEmails].every(e => byEmail.has(e))) break;
421
+ }
422
+
423
+ return byEmail;
424
+ }
425
+
426
+ // ── Room processing ──────────────────────────────────────────────────
379
427
 
380
428
  const allRows = [];
381
429
  const knownIds = new Set(seen);
382
430
 
383
- for (const room of rooms) {
384
- const isEmail = EMAIL_RE.test(room);
431
+ if (canBatch) {
432
+ // ── Batch mode: add all rooms via room finder, single capture ──────
433
+ const emailToName = new Map();
434
+ const expectedEmails = new Set();
435
+ for (const r of parsedRooms) {
436
+ emailToName.set(r.email, r.name);
437
+ expectedEmails.add(r.email);
438
+ }
385
439
 
386
- // Clear any pending captures before adding this room.
440
+ // Clear stale captures, then add all rooms without dismissing.
387
441
  readCapture();
388
442
 
389
- try {
390
- if (isEmail) {
391
- await addRoomByEmail(room);
392
- } else {
393
- await addRoomByName(room);
443
+ for (const r of parsedRooms) {
444
+ try {
445
+ await addRoomByName(r.name);
446
+ await sleep(300);
447
+ } catch (addErr) {
448
+ allRows.push({
449
+ roomName: r.name, roomEmail: r.email, date,
450
+ state: 'error', start: addErr.message, end: '', durationMin: 0,
451
+ });
452
+ expectedEmails.delete(r.email);
394
453
  }
395
- } catch (addErr) {
396
- allRows.push({
397
- roomName: room, roomEmail: '', date,
398
- state: 'error', start: addErr.message, end: '', durationMin: 0,
399
- });
400
- continue;
401
454
  }
402
455
 
403
- // Capture the getSchedule response immediately after adding (before dismiss).
404
- const result = await captureNewSchedule(knownIds);
405
-
406
- // Remove the room so "Add a room" reappears for the next room.
407
- await dismissRoom();
456
+ // Single batch capture for all successfully added rooms.
457
+ if (expectedEmails.size > 0) {
458
+ const results = await captureBatchSchedules(expectedEmails, knownIds);
459
+
460
+ for (const [email, data] of results) {
461
+ const name = emailToName.get(email);
462
+ knownIds.add(email);
463
+ if (data.errMsg && !data.sawView) {
464
+ allRows.push({
465
+ roomName: name, roomEmail: email, date,
466
+ state: 'error', start: `Free/busy error: ${data.errMsg}`,
467
+ end: '', durationMin: 0,
468
+ });
469
+ } else {
470
+ const timeline = buildTimeline(data.items, date);
471
+ for (const row of timeline) {
472
+ allRows.push({ roomName: name, roomEmail: email, ...row });
473
+ }
474
+ }
475
+ }
408
476
 
409
- if (!result.newId) {
410
- allRows.push({
411
- roomName: room, roomEmail: '', date,
412
- state: 'error', start: `No free/busy returned for "${room}" on ${date}`,
413
- end: '', durationMin: 0,
414
- });
415
- continue;
477
+ // Report rooms not found in any captured response.
478
+ for (const email of expectedEmails) {
479
+ if (!results.has(email)) {
480
+ allRows.push({
481
+ roomName: emailToName.get(email), roomEmail: email, date,
482
+ state: 'error', start: `No free/busy returned for "${emailToName.get(email)}" on ${date}`,
483
+ end: '', durationMin: 0,
484
+ });
485
+ }
486
+ }
416
487
  }
488
+ } else {
489
+ // ── Sequential mode (original behavior) ────────────────────────────
490
+ for (const r of parsedRooms) {
491
+ const isEmail = r.mode === 'email';
492
+
493
+ // Clear any pending captures before adding this room.
494
+ readCapture();
495
+
496
+ try {
497
+ if (isEmail) {
498
+ await addRoomByEmail(r.name);
499
+ } else {
500
+ await addRoomByName(r.name);
501
+ }
502
+ } catch (addErr) {
503
+ allRows.push({
504
+ roomName: r.name, roomEmail: r.email || '', date,
505
+ state: 'error', start: addErr.message, end: '', durationMin: 0,
506
+ });
507
+ continue;
508
+ }
417
509
 
418
- knownIds.add(result.newId);
510
+ // Capture the getSchedule response immediately after adding (before dismiss).
511
+ const result = await captureNewSchedule(knownIds);
419
512
 
420
- if (result.errMsg && !result.sawView) {
421
- allRows.push({
422
- roomName: room, roomEmail: result.newId, date,
423
- state: 'error', start: `Free/busy error: ${result.errMsg}`,
424
- end: '', durationMin: 0,
425
- });
426
- continue;
427
- }
513
+ // Remove the room so "Add a room" reappears for the next room.
514
+ await dismissRoom();
515
+
516
+ if (!result.newId) {
517
+ allRows.push({
518
+ roomName: r.name, roomEmail: r.email || '', date,
519
+ state: 'error', start: `No free/busy returned for "${r.name}" on ${date}`,
520
+ end: '', durationMin: 0,
521
+ });
522
+ continue;
523
+ }
428
524
 
429
- const timeline = buildTimeline(result.items, date);
430
- for (const r of timeline) {
431
- allRows.push({ roomName: room, roomEmail: result.newId, ...r });
525
+ knownIds.add(result.newId);
526
+
527
+ if (result.errMsg && !result.sawView) {
528
+ allRows.push({
529
+ roomName: r.name, roomEmail: result.newId, date,
530
+ state: 'error', start: `Free/busy error: ${result.errMsg}`,
531
+ end: '', durationMin: 0,
532
+ });
533
+ continue;
534
+ }
535
+
536
+ const timeline = buildTimeline(result.items, date);
537
+ for (const row of timeline) {
538
+ allRows.push({ roomName: r.name, roomEmail: result.newId, ...row });
539
+ }
432
540
  }
433
541
  }
434
542
 
435
543
  if (allRows.length === 0) {
436
- throw new Error(`No results for any of the ${rooms.length} room(s) on ${date}`);
544
+ throw new Error(`No results for any of the ${parsedRooms.length} room(s) on ${date}`);
437
545
  }
438
546
 
439
547
  return allRows;
@@ -15,7 +15,7 @@ args:
15
15
  - name: rooms
16
16
  type: string
17
17
  required: true
18
- help: "Comma-separated room names or emails (e.g. 'MBTMY The Vista,MBTMY The Cliffside' or 'RES-A@mb.com,RES-B@mb.com')"
18
+ help: "Comma-separated room names, emails, or name:email pairs (e.g. 'MBTMY The Vista:RES-RERE-M6VJ7ZUW@mercedes-benz.com,MBTMY The Cliffside:res-rere-m6vjl2tw@mercedes-benz.com' for batch mode, or 'MBTMY The Vista,res-rere-m6vj7zuw@mercedes-benz.com' for sequential)"
19
19
  - name: date
20
20
  type: string
21
21
  required: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commandgarden/cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Enterprise browser automation CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",